diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..50df215 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,154 @@ +name: PR Check + +on: + pull_request: + branches: [ main, develop ] + +jobs: + quality: + name: Code Quality & Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: | + cd frontend + pnpm install + pnpm add -D @biomejs/biome @next/bundle-analyzer + cp ../biome.json . + + - name: Run Biome lint + working-directory: frontend + run: | + echo "Node version: $(node -v)" + echo "Current directory: $(pwd)" + echo "Running Biome lint..." + pnpm exec biome ci . + + - name: Run Biome format check + working-directory: frontend + run: | + echo "Running Biome format check..." + pnpm exec biome check --files-ignore-unknown=true . + + - name: Type check + working-directory: frontend + run: pnpm exec tsc --noEmit + + - name: Run tests + working-directory: frontend + run: pnpm test || echo "No tests found" + + - name: Build application + working-directory: frontend + env: + NEXT_TELEMETRY_DISABLED: 1 + JWT_SECRET: ${{ secrets.JWT_SECRET || 'dummy-secret-for-ci' }} + NEXT_PUBLIC_WLD_APP_ID: ${{ secrets.NEXT_PUBLIC_WLD_APP_ID || 'app_staging_0' }} + run: pnpm build + + security: + name: Security Scan + runs-on: ubuntu-latest + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Install dependencies + run: | + cd frontend + pnpm install + + - name: Run security audit + working-directory: frontend + run: | + pnpm audit || echo "Security vulnerabilities found. Please review the report above." + + - name: Check for outdated dependencies + working-directory: frontend + run: | + pnpm outdated || echo "Outdated dependencies found. Please review the report above." + + bundle-analysis: + name: Bundle Analysis + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Install dependencies + run: | + cd frontend + pnpm install + pnpm add -D @next/bundle-analyzer + + - name: Build and analyze bundle + working-directory: frontend + env: + ANALYZE: true + NEXT_TELEMETRY_DISABLED: 1 + JWT_SECRET: ${{ secrets.JWT_SECRET || 'dummy-secret-for-ci' }} + NEXT_PUBLIC_WLD_APP_ID: ${{ secrets.NEXT_PUBLIC_WLD_APP_ID || 'app_staging_0' }} + run: | + pnpm build + + - name: Upload bundle analysis + if: success() + uses: actions/upload-artifact@v4 + with: + name: bundle-analysis + path: | + frontend/.next/analyze/client.html + frontend/.next/analyze/edge.html + frontend/.next/analyze/nodejs.html + compression-level: 9 + retention-days: 14 \ No newline at end of file diff --git a/.github/workflows/relyance-sci.yml b/.github/workflows/relyance-sci.yml deleted file mode 100644 index 1462102..0000000 --- a/.github/workflows/relyance-sci.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Relyance SCI Scan - -on: - schedule: - - cron: "0 20 * * *" - workflow_dispatch: - -jobs: - execute-relyance-sci: - name: Relyance SCI Job - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Pull and run SCI binary - run: |- - docker pull gcr.io/relyance-ext/compliance_inspector:release && \ - docker run --rm -v `pwd`:/repo --env API_KEY='${{ secrets.DPP_SCI_KEY }}' gcr.io/relyance-ext/compliance_inspector:release diff --git a/.gitignore b/.gitignore index 496ee2c..ba4c677 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,39 @@ -.DS_Store \ No newline at end of file +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +!.env.example +.env +.env.* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# IDE +.vscode +.idea \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..5255c1c --- /dev/null +++ b/biome.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": true, + "include": [ + "src/**/*.{ts,tsx}", + "src/**/*.{js,jsx}", + "src/**/*.json", + "*.config.{ts,js,mjs}", + "*.d.ts", + "eslint.config.mjs", + "next.config.mjs", + "tailwind.config.ts", + "postcss.config.js" + ], + "ignore": [ + "**/node_modules/**", + "**/.next/**", + "**/dist/**", + "**/build/**", + "**/*.test.{ts,tsx}", + "**/*.spec.{ts,tsx}", + "**/coverage/**", + "**/storybook-static/**", + "src/app/api-docs/**", + "src/components/ui/icons/**", + ".xata/**", + "**/generated/**" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80, + "formatWithErrors": false + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useImportType": "error", + "useNodejsImportProtocol": "error" + }, + "correctness": { + "noUnusedVariables": "error", + "noUndeclaredVariables": "error" + }, + "suspicious": { + "noExplicitAny": "error", + "noConsoleLog": "error" + } + } + }, + "javascript": { + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80, + "quoteStyle": "double", + "semicolons": "always" + } + } +} diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..2a09281 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,23 @@ +# Next Auth Configuration +NEXTAUTH_URL= +NEXTAUTH_SECRET= +JWT_SECRET= + +# Worldcoin Integration +WLD_CLIENT_ID= +WLD_CLIENT_SECRET= +NEXT_PUBLIC_WLD_APP_ID= +APP_ID= + +# API Keys +DEV_PORTAL_API_KEY= + +# Xata Database +XATA_BRANCH= +XATA_API_KEY= +XATA_DATABASE_URL= + +# Environment Configuration +NEXT_PUBLIC_PAYMENT_ADDRESS= +NEXT_PUBLIC_APP_ENV= +NODE_ENV= \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 5ef6a52..642bbe8 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -30,8 +30,10 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* +# env files +.env +.env.* +!.env.example # vercel .vercel diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 0000000..ad63c96 --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "include": [ + "src/app/**/route.ts", + "src/app/**/page.tsx", + "src/app/layout.tsx", + "src/app/not-found.tsx", + "src/app/types.ts", + "src/components/**/*.{ts,tsx}", + "src/hooks/**/*.{ts,tsx}", + "src/lib/**/*.{ts,tsx}", + "src/providers/**/*.{ts,tsx}", + "src/constants/**/*.{ts,tsx}", + "src/data/**/*.{ts,tsx}", + "src/middleware.ts", + "tailwind.config.ts", + "next.config.mjs", + "eslint.config.mjs", + "next-env.d.ts", + "postcss.config.js" + ], + "ignore": [ + "**/node_modules/**", + "**/.next/**", + "**/dist/**", + "**/build/**", + "**/*.test.{ts,tsx}", + "**/*.spec.{ts,tsx}", + "**/coverage/**", + "**/storybook-static/**", + "src/app/api-docs/**", + "src/components/ui/icons/**", + ".xata/**", + "**/generated/**" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80, + "formatWithErrors": false + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useImportType": "error", + "useNodejsImportProtocol": "error" + }, + "correctness": { + "noUnusedVariables": "error", + "noUndeclaredVariables": "error" + }, + "suspicious": { + "noExplicitAny": "error", + "noConsoleLog": "error" + } + } + }, + "javascript": { + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80, + "quoteStyle": "double", + "semicolons": "always" + } + } +} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index c85fb67..f5b76f3 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,5 +1,5 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; import { FlatCompat } from "@eslint/eslintrc"; const __filename = fileURLToPath(import.meta.url); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 0677dc5..e7497f6 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,14 +1,20 @@ +import bundleAnalyzer from "@next/bundle-analyzer"; + +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}); + /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone', + output: "standalone", swcMinify: true, reactStrictMode: true, poweredByHeader: false, images: { remotePatterns: [ { - protocol: 'https', - hostname: 'avatars.githubusercontent.com', + protocol: "https", + hostname: "avatars.githubusercontent.com", }, ], }, @@ -16,9 +22,8 @@ const nextConfig = { ignoreBuildErrors: false, }, eslint: { - dirs: ['src'], ignoreDuringBuilds: false, }, }; -export default nextConfig; \ No newline at end of file +export default withBundleAnalyzer(nextConfig); diff --git a/frontend/package.json b/frontend/package.json index 2ff8bdc..e7af535 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@biomejs/biome": "^1.9.4", "@eslint/eslintrc": "^3", "@types/jsonwebtoken": "^9.0.7", "@types/node": "^20.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index bb8dbb0..c9a76eb 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 '@eslint/eslintrc': specifier: ^3 version: 3.2.0 @@ -162,6 +165,59 @@ packages: resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==} engines: {node: '>=6.9.0'} + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@braintree/sanitize-url@7.0.4': resolution: {integrity: sha512-hPYRrKFoI+nuckPgDJfyYAkybFvheo4usS0Vw0HNAe+fmGBQA5Az37b/yStO284atBoqqdOUhKJ3d9Zw3PQkcQ==} @@ -2980,6 +3036,41 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + '@braintree/sanitize-url@7.0.4': {} '@eslint-community/eslint-utils@4.4.1(eslint@9.19.0(jiti@1.21.7))': diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index d54874f..cbfea5e 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,7 +1,7 @@ module.exports = { plugins: { - 'tailwindcss/nesting': {}, + "tailwindcss/nesting": {}, tailwindcss: {}, autoprefixer: {}, }, -} \ No newline at end of file +}; diff --git a/frontend/src/app/(home)/page.tsx b/frontend/src/app/(home)/page.tsx index d766041..f3306e3 100644 --- a/frontend/src/app/(home)/page.tsx +++ b/frontend/src/app/(home)/page.tsx @@ -1,95 +1,104 @@ "use client"; -import { useState, useEffect } from 'react' -import { LoadingSpinner } from "@/components/ui/LoadingSpinner" +import { AchievementButton } from "@/components/ui/AchievementButton"; +import { LeaderboardButton } from "@/components/ui/LeaderboardButton"; +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { ProfileCard } from "@/components/ui/ProfileCard"; -import QuizCard from "@/components/ui/QuizCard"; -import { AchievementButton } from "@/components/ui/AchievementButton" -import { LeaderboardButton } from "@/components/ui/LeaderboardButton" -import { useVerification } from '@/hooks/useVerification' -import { VerifyModal } from '@/components/ui/VerifyModal' -import { useRouter } from 'next/navigation' -import { clearVerificationSession } from '@/hooks/useVerification' -import { cn } from '@/lib/utils' -import { motion } from 'framer-motion' -import { Sun } from 'lucide-react' +import { QuizCard } from "@/components/ui/QuizCard"; +import { VerifyModal } from "@/components/ui/VerifyModal"; +import { useVerification } from "@/hooks/useVerification"; +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; +import { Sun } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; interface User { name: string; last_name: string; level: string; level_points: number; + points: number; maxPoints: number; + verified?: boolean; } +// Function to clear verification session +const clearVerificationSession = () => { + sessionStorage.removeItem("verify-modal-shown"); +}; + export default function Home() { - const router = useRouter() - const [loading, setLoading] = useState(true) - const [userData, setUserData] = useState(null) - const [showVerifyModal, setShowVerifyModal] = useState(false) - const { handleVerify } = useVerification() + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [userData, setUserData] = useState(null); + const [showVerifyModal, setShowVerifyModal] = useState(false); + const { handleVerify } = useVerification(); useEffect(() => { const fetchData = async () => { try { // Skip auth check if we're in the registration process - if (window.location.pathname.includes('/register')) { + if (window.location.pathname.includes("/register")) { setLoading(false); return; } - const response = await fetch('/api/home') + const response = await fetch("/api/home"); if (!response.ok) { - const errorData = await response.json() - console.error('API Error:', errorData) - + const errorData = await response.json(); + console.error("API Error:", errorData); + // If user not found, clear session and redirect to sign-in if (response.status === 404) { - console.log('User not found, redirecting to sign-in') - clearVerificationSession() - const logoutResponse = await fetch('/api/auth/logout', { - method: 'POST' - }) + // Handle user not found case silently + clearVerificationSession(); + const logoutResponse = await fetch("/api/auth/logout", { + method: "POST", + }); if (logoutResponse.ok) { - router.push('/sign-in') + router.push("/sign-in"); } - return + return; } - return + return; } - const data = await response.json() - console.log('Received user data:', data) - setUserData(data.user) - + const data = await response.json(); + setUserData(data.user); + // Check if user is verified - if (!data.user.verified) { - setShowVerifyModal(true) + if ( + !data.user.verified && + !sessionStorage.getItem("verify-modal-shown") + ) { + setShowVerifyModal(true); + sessionStorage.setItem("verify-modal-shown", "true"); } } catch (error) { - console.error('Error fetching home data:', error) + console.error("Error fetching home data:", error); } finally { - setLoading(false) + setLoading(false); } - } + }; - fetchData() - }, [router]) + void fetchData(); + }, [router]); const handleVerifyClick = async () => { - const success = await handleVerify() + const success = await handleVerify(); if (success) { - setShowVerifyModal(false) + setShowVerifyModal(false); // Refresh user data to get updated verification status - const response = await fetch('/api/home') + const response = await fetch("/api/home"); if (response.ok) { - const data = await response.json() - setUserData(data.user) + const data = await response.json(); + setUserData(data.user); } } - } + }; if (loading) { - return + return ; } return ( @@ -99,66 +108,66 @@ export default function Home() { onClose={() => setShowVerifyModal(false)} onVerify={handleVerifyClick} /> - +
-
+
- - -
- -

+
+ +

Welcome Back!

- -

+ +

Track your progress and continue your journey of self-discovery

-
-
- + - - - - - ([]) - const [isModalOpen, setIsModalOpen] = useState(true); // State for modal visibility - const router = useRouter(); // Initialize the router + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [level, setLevel] = useState({ current: 0, max: 100, title: "" }); + const [achievements, setAchievements] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(true); useEffect(() => { const fetchData = async () => { try { - const response = await fetch('/api/achievements') - const data = await response.json() - setLevel(data.level) - setAchievements(data.achievements) + const response = await fetch("/api/achievements"); + const data = await response.json(); + setLevel(data.level); + setAchievements(data.achievements); } catch (error) { - console.error('Error fetching achievements:', error) + console.error("Error fetching achievements:", error); } finally { - setLoading(false) + setLoading(false); } - } + }; - fetchData() - }, []) + void fetchData(); + }, []); const handleCloseModal = () => { - setIsModalOpen(false); // Close the modal - router.back(); // Redirect to the previous page + setIsModalOpen(false); + router.back(); }; if (loading) { - return + return ; } return (
- {/* Header Card */} -
- - {/* Content */} -
-

+
+
+

Achievements

-

- Celebrate your progress and discover what's next on your journey +

+ Celebrate your progress and discover what's next on your + journey

- - {/* Level Progress */} -
-
+ +
+
- + {level.title} - {level.current}/{level.max} points + + {level.current}/{level.max} points +
-
-
+
-

+

Reach the next level to unlock new badges and exclusive content!

- {/* Achievement Cards */} -
- {achievements.map((achievement, index) => ( -
+ {achievements.map((achievement) => ( +
- {/* Coming Soon Overlay */} {isModalOpen && ( -
-
-

Coming Soon

-

- The achievements feature is currently under development. - Check back soon to celebrate your progress! +

+
+

Coming Soon

+

+ The achievements feature is currently under development. Check + back soon to celebrate your progress!

-
); -} \ No newline at end of file +} diff --git a/frontend/src/app/api-docs/page.tsx b/frontend/src/app/api-docs/page.tsx index 92d0712..004fc13 100644 --- a/frontend/src/app/api-docs/page.tsx +++ b/frontend/src/app/api-docs/page.tsx @@ -1,25 +1,65 @@ -'use client'; +"use client"; -import { useEffect, useState } from 'react'; -import SwaggerUI from 'swagger-ui-react'; -import 'swagger-ui-react/swagger-ui.css'; +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { useEffect, useState } from "react"; +import SwaggerUI from "swagger-ui-react"; +import "swagger-ui-react/swagger-ui.css"; + +interface SwaggerSpec { + openapi: string; + info: { + title: string; + version: string; + description?: string; + }; + paths: Record; + components?: Record; + tags?: Array<{ + name: string; + description?: string; + }>; +} export default function ApiDocs() { - const [spec, setSpec] = useState(null); - - useEffect(() => { - fetch('/api/docs') - .then(response => response.json()) - .then(data => setSpec(data)); - }, []); - - if (!spec) { - return
Loading...
; - } - - return ( -
- -
- ); -} \ No newline at end of file + const [spec, setSpec] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchDocs() { + try { + const response = await fetch("/api/docs"); + if (!response.ok) { + throw new Error("Failed to fetch API documentation"); + } + const data = await response.json(); + setSpec(data); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + } + } + + void fetchDocs(); + }, []); + + if (error) { + return ( +
+

{error}

+
+ ); + } + + if (!spec) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/frontend/src/app/api/auth/[...nextauth]/route.ts b/frontend/src/app/api/auth/[...nextauth]/route.ts index 3cb333b..0fd70d3 100644 --- a/frontend/src/app/api/auth/[...nextauth]/route.ts +++ b/frontend/src/app/api/auth/[...nextauth]/route.ts @@ -1,10 +1,19 @@ -import { NextRequest, NextResponse } from 'next/server'; import { getXataClient } from "@/lib/utils"; +import { type NextRequest, NextResponse } from "next/server"; + +interface AuthUser { + id: string; + name: string; + email: string; + walletAddress: string; + subscription: boolean; + verified: boolean; +} // Helper function to get user from headers async function getUserFromHeaders(req: NextRequest) { - const userId = req.headers.get('x-user-id'); - const walletAddress = req.headers.get('x-wallet-address'); + const userId = req.headers.get("x-user-id"); + const walletAddress = req.headers.get("x-wallet-address"); if (!userId || !walletAddress) { return null; @@ -13,40 +22,33 @@ async function getUserFromHeaders(req: NextRequest) { const xata = getXataClient(); return await xata.db.Users.filter({ wallet_address: walletAddress, - xata_id: userId + xata_id: userId, }).getFirst(); } export async function GET(req: NextRequest) { try { const user = await getUserFromHeaders(req); - + if (!user) { - return new NextResponse( - JSON.stringify({ error: 'Unauthorized' }), - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - return new NextResponse( - JSON.stringify({ - user: { - id: user.xata_id, - name: user.name, - email: user.email, - walletAddress: user.wallet_address, - subscription: user.subscription, - verified: user.verified - } - }), - { status: 200 } - ); + const authUser: AuthUser = { + id: user.xata_id, + name: user.name, + email: user.email, + walletAddress: user.wallet_address, + subscription: user.subscription, + verified: user.verified, + }; + return NextResponse.json({ user: authUser }, { status: 200 }); } catch (error) { - console.error('Auth error:', error); - return new NextResponse( - JSON.stringify({ error: 'Internal server error' }), - { status: 500 } + console.error("Auth error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, ); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/auth/logout/route.ts b/frontend/src/app/api/auth/logout/route.ts index df083e6..29ee998 100644 --- a/frontend/src/app/api/auth/logout/route.ts +++ b/frontend/src/app/api/auth/logout/route.ts @@ -1,34 +1,43 @@ -import { NextResponse } from "next/server"; import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +const COOKIES_TO_CLEAR = [ + "session", + "next-auth.session-token", + "next-auth.callback-url", + "next-auth.csrf-token", +] as const; + +const COOKIE_EXPIRY = "Thu, 01 Jan 1970 00:00:00 GMT"; export async function POST() { try { - // Clear all session-related cookies const cookieStore = cookies(); - cookieStore.delete('session'); - cookieStore.delete('next-auth.session-token'); - cookieStore.delete('next-auth.callback-url'); - cookieStore.delete('next-auth.csrf-token'); - // Create response with all cookies cleared - const response = NextResponse.json({ - success: true, - message: 'Logged out successfully' - }, { status: 200 }); + // Clear all session-related cookies + for (const cookie of COOKIES_TO_CLEAR) { + cookieStore.delete(cookie); + } + + const response = NextResponse.json( + { + success: true, + message: "Logged out successfully", + }, + { status: 200 }, + ); // Set all cookies to expire - response.headers.set('Set-Cookie', 'session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'); - response.headers.append('Set-Cookie', 'next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'); - response.headers.append('Set-Cookie', 'next-auth.callback-url=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'); - response.headers.append('Set-Cookie', 'next-auth.csrf-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'); + for (const cookie of COOKIES_TO_CLEAR) { + response.headers.append( + "Set-Cookie", + `${cookie}=; Path=/; Expires=${COOKIE_EXPIRY}`, + ); + } return response; } catch (error) { - console.error('Logout error:', error); - return NextResponse.json({ - error: 'Failed to logout' - }, { - status: 500 - }); + console.error("Logout error:", error); + return NextResponse.json({ error: "Failed to logout" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/auth/session/route.ts b/frontend/src/app/api/auth/session/route.ts index 6e660df..6211796 100644 --- a/frontend/src/app/api/auth/session/route.ts +++ b/frontend/src/app/api/auth/session/route.ts @@ -1,14 +1,13 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { SignJWT, jwtVerify } from 'jose'; -import { getXataClient } from '@/lib/utils'; -import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'; +import { getXataClient } from "@/lib/utils"; +import { SignJWT, jwtVerify } from "jose"; +import { type NextRequest, NextResponse } from "next/server"; -export const dynamic = 'force-dynamic'; +export const dynamic = "force-dynamic"; // Get the secret from environment variables const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } // Create secret for JWT tokens @@ -17,41 +16,34 @@ const secret = new TextEncoder().encode(JWT_SECRET); // Verify session token async function verifyToken(token: string) { try { - console.log('Verifying token...'); - if (!token || typeof token !== 'string') { - console.error('Invalid token format'); + if (!token || typeof token !== "string") { + console.error("Invalid token format"); return null; } const verified = await jwtVerify(token, secret, { - algorithms: ['HS256'] + algorithms: ["HS256"], }); // Validate payload structure const payload = verified.payload; - if (!payload || typeof payload !== 'object') { - console.error('Invalid payload structure'); + if (!payload || typeof payload !== "object") { + console.error("Invalid payload structure"); return null; } // Ensure required fields exist and are of correct type - if (!payload.address || typeof payload.address !== 'string') { - console.error('Missing or invalid address in payload'); + if (!payload.address || typeof payload.address !== "string") { + console.error("Missing or invalid address in payload"); return null; } - console.log('Token verified successfully:', { - address: payload.address, - sub: payload.sub, - exp: payload.exp - }); - return payload; } catch (error) { - console.error('Token verification failed:', { + console.error("Token verification failed:", { error, - message: error instanceof Error ? error.message : 'Unknown error', - stack: error instanceof Error ? error.stack : undefined + message: error instanceof Error ? error.message : "Unknown error", + stack: error instanceof Error ? error.stack : undefined, }); return null; } @@ -60,122 +52,120 @@ async function verifyToken(token: string) { // GET handler for session verification export async function GET(req: NextRequest) { try { - // Safely get cookie values with fallbacks - const sessionToken = req.cookies.get('session')?.value || ''; - const worldIdVerified = req.cookies.get('worldcoin_verified')?.value || 'false'; - const siweVerified = req.cookies.get('siwe_verified')?.value || 'false'; - const registrationStatus = req.cookies.get('registration_status')?.value || ''; + // Get session token + const sessionToken = req.cookies.get("session")?.value || ""; + const siweVerified = req.cookies.get("siwe_verified")?.value || "false"; // Early return if no session token if (!sessionToken) { - console.log('No session token found'); - return new NextResponse( - JSON.stringify({ + return NextResponse.json( + { isAuthenticated: false, isRegistered: false, isVerified: false, - error: 'No session found' - }), { + error: "No session found", + }, + { status: 401, headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - } + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + }, ); } // Verify token const decoded = await verifyToken(sessionToken); if (!decoded || !decoded.address) { - console.error('Token verification failed or missing address'); - return new NextResponse( - JSON.stringify({ + console.error("Token verification failed or missing address"); + return NextResponse.json( + { isAuthenticated: false, isRegistered: false, isVerified: false, - error: 'Invalid session' - }), { + error: "Invalid session", + }, + { status: 401, headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - } + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + }, ); } // Check if user exists in database const xata = getXataClient(); const user = await xata.db.Users.filter({ - wallet_address: (decoded.address as string).toLowerCase() + wallet_address: (decoded.address as string).toLowerCase(), }).getFirst(); if (!user) { - console.error('User not found in database'); - return new NextResponse( - JSON.stringify({ + console.error("User not found in database"); + return NextResponse.json( + { isAuthenticated: false, isRegistered: false, isVerified: false, - error: 'User not found', - address: decoded.address - }), { + error: "User not found", + address: decoded.address, + }, + { status: 404, headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - } + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + }, ); } // Determine registration status - const isRegistered = user.name !== 'Temporary'; - + const isRegistered = user.name !== "Temporary"; + // Ensure all fields are serializable const responseData = { - address: decoded.address?.toString() || '', + address: decoded.address?.toString() || "", isAuthenticated: true, isRegistered: Boolean(isRegistered), isVerified: user.verified, - isSiweVerified: siweVerified === 'true', + isSiweVerified: siweVerified === "true", needsRegistration: !isRegistered, - userId: user.user_id?.toString() || '', - userUuid: user.user_uuid?.toString() || '', + userId: user.user_id?.toString() || "", + userUuid: user.user_uuid?.toString() || "", user: { - id: user.xata_id?.toString() || '', - name: user.name?.toString() || '', - email: user.email?.toString() || '', - walletAddress: decoded.address?.toString() || '' - } + id: user.xata_id?.toString() || "", + name: user.name?.toString() || "", + email: user.email?.toString() || "", + walletAddress: decoded.address?.toString() || "", + }, }; - return new NextResponse( - JSON.stringify(responseData), { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - } - ); - + return NextResponse.json(responseData, { + status: 200, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + }); } catch (error) { - console.error('Session verification error:', error); - return new NextResponse( - JSON.stringify({ + console.error("Session verification error:", error); + return NextResponse.json( + { isAuthenticated: false, isRegistered: false, isVerified: false, - error: 'Session verification failed' - }), { + error: "Session verification failed", + }, + { status: 500, headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } - } + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + }, ); } } @@ -187,13 +177,16 @@ export async function POST(req: NextRequest) { const xata = getXataClient(); // Find user by wallet address - const user = await xata.db.Users.filter('wallet_address', walletAddress).getFirst(); + const user = await xata.db.Users.filter( + "wallet_address", + walletAddress, + ).getFirst(); if (!user) { - throw new Error('User not found'); + throw new Error("User not found"); } // Check if user is registered (not temporary) - const isRegistered = user.name !== 'Temporary'; + const isRegistered = user.name !== "Temporary"; // Create session token const token = await new SignJWT({ @@ -204,11 +197,11 @@ export async function POST(req: NextRequest) { address: user.wallet_address, isRegistered, isSiweVerified, - isVerified: user.verified + isVerified: user.verified, }) - .setProtectedHeader({ alg: 'HS256' }) - .setExpirationTime('24h') - .sign(secret); + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("24h") + .sign(secret); // Create response with session data const response = NextResponse.json({ @@ -225,38 +218,35 @@ export async function POST(req: NextRequest) { needsRegistration: !isRegistered, user, userId: user.xata_id, - userUuid: user.user_uuid - }); - - // Set cookies - response.cookies.set('session', token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/' + userUuid: user.user_uuid, }); - response.cookies.set('siwe_verified', isSiweVerified ? 'true' : 'false', { + const cookieOptions = { httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/' - }); + secure: process.env.NODE_ENV === "production", + sameSite: "lax" as const, + path: "/", + }; - response.cookies.set('registration_status', isRegistered ? 'complete' : 'pending', { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/' - }); + // Set cookies + response.cookies.set("session", token, cookieOptions); + response.cookies.set( + "siwe_verified", + isSiweVerified ? "true" : "false", + cookieOptions, + ); + response.cookies.set( + "registration_status", + isRegistered ? "complete" : "pending", + cookieOptions, + ); return response; - } catch (error) { - console.error('Session creation error:', error); + console.error("Session creation error:", error); return NextResponse.json( - { error: 'Failed to create session' }, - { status: 500 } + { error: "Failed to create session" }, + { status: 500 }, ); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/complete-siwe/route.ts b/frontend/src/app/api/complete-siwe/route.ts index 5426fe1..cf663af 100644 --- a/frontend/src/app/api/complete-siwe/route.ts +++ b/frontend/src/app/api/complete-siwe/route.ts @@ -1,69 +1,69 @@ -import { cookies } from 'next/headers'; -import { NextRequest, NextResponse } from 'next/server'; -import { MiniAppWalletAuthSuccessPayload, verifySiweMessage } from '@worldcoin/minikit-js'; +import { + type MiniAppWalletAuthSuccessPayload, + verifySiweMessage, +} from "@worldcoin/minikit-js"; +import { cookies } from "next/headers"; +import { type NextRequest, NextResponse } from "next/server"; interface IRequestPayload { payload: MiniAppWalletAuthSuccessPayload; nonce: string; } +interface SiweResponse { + status: "success" | "error"; + isValid: boolean; + address?: string; + message?: string; +} + export async function POST(req: NextRequest) { try { const { payload, nonce } = (await req.json()) as IRequestPayload; - const storedNonce = cookies().get('siwe')?.value; + const storedNonce = cookies().get("siwe")?.value; - console.log('SIWE verification request:', { - payload, - nonce, - storedNonce - }); - - // Strict nonce comparison if (!storedNonce || storedNonce.trim() !== nonce.trim()) { - console.error('Nonce mismatch:', { - received: nonce, + console.error("Nonce mismatch:", { + received: nonce, stored: storedNonce, receivedLength: nonce?.length, - storedLength: storedNonce?.length + storedLength: storedNonce?.length, }); - return NextResponse.json({ - status: 'error', + + const response: SiweResponse = { + status: "error", isValid: false, - message: 'Invalid nonce', - }); + message: "Invalid nonce", + }; + + return NextResponse.json(response); } + const validMessage = await verifySiweMessage(payload, storedNonce); - try { - console.log('Verifying SIWE message...'); - const validMessage = await verifySiweMessage(payload, storedNonce); - console.log('SIWE verification result:', validMessage); + if (!validMessage.isValid || !validMessage.siweMessageData?.address) { + throw new Error("Invalid SIWE message"); + } - if (!validMessage.isValid || !validMessage.siweMessageData?.address) { - throw new Error('Invalid SIWE message'); - } + // Clear the nonce cookie after successful verification + cookies().delete("siwe"); - // Clear the nonce cookie after successful verification - cookies().delete('siwe'); + const response: SiweResponse = { + status: "success", + isValid: true, + address: validMessage.siweMessageData.address, + }; - return NextResponse.json({ - status: 'success', - isValid: true, - address: validMessage.siweMessageData.address - }); - } catch (error) { - console.error('SIWE verification error:', error); - return NextResponse.json({ - status: 'error', - isValid: false, - message: error instanceof Error ? error.message : 'SIWE verification failed', - }); - } + return NextResponse.json(response); } catch (error) { - console.error('Request processing error:', error); - return NextResponse.json({ - status: 'error', + console.error("SIWE verification error:", error); + + const response: SiweResponse = { + status: "error", isValid: false, - message: error instanceof Error ? error.message : 'Request processing failed', - }); + message: + error instanceof Error ? error.message : "SIWE verification failed", + }; + + return NextResponse.json(response); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/confirm-payment/route.ts b/frontend/src/app/api/confirm-payment/route.ts index c381354..4482dd4 100644 --- a/frontend/src/app/api/confirm-payment/route.ts +++ b/frontend/src/app/api/confirm-payment/route.ts @@ -1,128 +1,135 @@ -import { MiniAppPaymentSuccessPayload } from "@worldcoin/minikit-js"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; import { getXataClient } from "@/lib/utils"; +import type { MiniAppPaymentSuccessPayload } from "@worldcoin/minikit-js"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; +import { cookies } from "next/headers"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; interface IRequestPayload { payload: MiniAppPaymentSuccessPayload; } -const JWT_SECRET = process.env.JWT_SECRET; -if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); +interface PaymentResponse { + success?: boolean; + error?: string; + message?: string; + next_payment_date?: string; + details?: string; } -export const secret = new TextEncoder().encode(JWT_SECRET); +interface TokenPayload extends JWTPayload { + address?: string; +} + +function getSecret() { + const JWT_SECRET = process.env.JWT_SECRET; + if (!JWT_SECRET) { + throw new Error("JWT_SECRET environment variable is required"); + } + return new TextEncoder().encode(JWT_SECRET); +} export async function POST(req: NextRequest) { try { const { payload } = (await req.json()) as IRequestPayload; - console.log('Received payment confirmation payload:', payload); - + const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - console.log('No session token found'); - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { - const { payload: tokenPayload } = await jwtVerify(token, secret); - console.log('Token payload:', tokenPayload); - - if (tokenPayload.address) { - user = await xata.db.Users.filter({ - wallet_address: tokenPayload.address - }).getFirst(); - console.log('Found user:', user?.xata_id); + const { payload: tokenPayload } = await jwtVerify(token, getSecret()); + const typedPayload = tokenPayload as TokenPayload; + + if (!typedPayload.address) { + console.error("No address in token payload"); + return NextResponse.json({ error: "Invalid session" }, { status: 401 }); } - } catch (error) { - console.error('Token verification failed:', error); - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - console.log('User not found'); - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } - try { // Get the latest payment_id - const latestPayment = await xata.db.Payments.sort('payment_id', 'desc').getFirst(); + const latestPayment = await xata.db.Payments.sort( + "payment_id", + "desc", + ).getFirst(); const nextPaymentId = (latestPayment?.payment_id || 0) + 1; - // Create payment record first - const paymentRecord = await xata.db.Payments.create({ + // Create payment record + await xata.db.Payments.create({ payment_id: nextPaymentId, user: user.xata_id, - uuid: payload.transaction_id + uuid: payload.transaction_id, }); - console.log('Created payment record:', paymentRecord); - // Check if user already has an active subscription - if (user.subscription && user.subscription_expires && new Date(user.subscription_expires) > new Date()) { + if ( + user.subscription && + user.subscription_expires && + new Date(user.subscription_expires) > new Date() + ) { // Extend the existing subscription const newExpiryDate = new Date(user.subscription_expires); newExpiryDate.setDate(newExpiryDate.getDate() + 30); - + await xata.db.Users.update(user.xata_id, { - subscription_expires: newExpiryDate + subscription_expires: newExpiryDate, }); - - console.log('Extended subscription to:', newExpiryDate); - - return NextResponse.json({ + + const response: PaymentResponse = { success: true, message: "Subscription extended", - next_payment_date: newExpiryDate.toISOString().split('T')[0] - }); + next_payment_date: newExpiryDate.toISOString().split("T")[0], + }; + + return NextResponse.json(response); } // Update user's subscription status for new subscription const subscriptionExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); - + await xata.db.Users.update(user.xata_id, { subscription: true, - subscription_expires: subscriptionExpiry + subscription_expires: subscriptionExpiry, }); - - console.log('Activated new subscription until:', subscriptionExpiry); - return NextResponse.json({ + const response: PaymentResponse = { success: true, message: "Subscription activated", - next_payment_date: subscriptionExpiry.toISOString().split('T')[0] - }); - + next_payment_date: subscriptionExpiry.toISOString().split("T")[0], + }; + + return NextResponse.json(response); } catch (error) { - console.error('Database operation failed:', error); - return NextResponse.json( - { error: "Failed to process payment" }, - { status: 500 } - ); - } + console.error("Error confirming payment:", error); + const response: PaymentResponse = { + success: false, + error: "Failed to confirm payment", + details: error instanceof Error ? error.message : "Unknown error", + }; + + return NextResponse.json(response, { status: 500 }); + } } catch (error) { console.error("Error confirming payment:", error); - return NextResponse.json( - { error: "Failed to confirm payment", details: error instanceof Error ? error.message : 'Unknown error' }, - { status: 500 } - ); + + const response: PaymentResponse = { + success: false, + error: "Failed to confirm payment", + details: error instanceof Error ? error.message : "Unknown error", + }; + + return NextResponse.json(response, { status: 500 }); } } - diff --git a/frontend/src/app/api/deepseek/route.ts b/frontend/src/app/api/deepseek/route.ts index ca5c561..95bcd7f 100644 --- a/frontend/src/app/api/deepseek/route.ts +++ b/frontend/src/app/api/deepseek/route.ts @@ -1,31 +1,50 @@ +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -export async function POST(request: Request) { +interface IdeologyScores { + econ: number; + dipl: number; + govt: number; + scty: number; +} + +interface DeepSeekResponse { + analysis: string; +} + +interface ApiResponse { + analysis?: string; + error?: string; +} + +export async function POST(request: NextRequest) { try { const body = await request.json(); - const { econ, dipl, govt, scty } = body; - + const scores = body as IdeologyScores; + const { econ, dipl, govt, scty } = scores; + // Validate required fields if (!econ || !dipl || !govt || !scty) { return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 } + { error: "Missing required fields" }, + { status: 400 }, ); } - + // Validate score ranges - const scores = { econ, dipl, govt, scty }; for (const [key, value] of Object.entries(scores)) { const score = Number(value); if (Number.isNaN(score) || score < 0 || score > 100) { - return NextResponse.json({ - error: `Invalid ${key} score. Must be a number between 0 and 100` - }, { status: 400 }); + return NextResponse.json( + { + error: `Invalid ${key} score. Must be a number between 0 and 100`, + }, + { status: 400 }, + ); } } - const prompt = - `[ROLE] Act as a senior political scientist specializing in ideological analysis. Address the user directly using "you/your" to personalize insights. + const prompt = `[ROLE] Act as a senior political scientist specializing in ideological analysis. Address the user directly using "you/your" to personalize insights. [INPUT] Economic: ${econ} | Diplomatic: ${dipl} | Government: ${govt} | Social: ${scty} (All 0-100) @@ -63,31 +82,32 @@ export async function POST(request: Request) { - Explain technical terms parenthetically (e.g., "multilateralism (global cooperation)") - End with 2 open-ended reflection questions for the user -Begin immediately with "1. Your Ideological Breakdown" `; +Begin immediately with "1. Your Ideological Breakdown"`; - const deepSeekResponse = await fetch('https://api.deepseek.com/v1/analyze', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`, + const deepSeekResponse = await fetch( + "https://api.deepseek.com/v1/analyze", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`, + }, + body: JSON.stringify({ prompt }), }, - body: JSON.stringify({ prompt }), - }); + ); if (!deepSeekResponse.ok) { const error = await deepSeekResponse.text(); throw new Error(`DeepSeek API error: ${error}`); } - const data = await deepSeekResponse.json(); - return NextResponse.json({ analysis: data.analysis }); - + const data = (await deepSeekResponse.json()) as DeepSeekResponse; + const response: ApiResponse = { analysis: data.analysis }; + return NextResponse.json(response); } catch (error) { - console.error('DeepSeek API error:', error); - const message = error instanceof Error ? error.message : 'Unknown error'; - return NextResponse.json( - { error: message }, - { status: 500 } - ); + console.error("DeepSeek API error:", error); + const message = error instanceof Error ? error.message : "Unknown error"; + const response: ApiResponse = { error: message }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/docs/route.ts b/frontend/src/app/api/docs/route.ts index 33e7995..5572353 100644 --- a/frontend/src/app/api/docs/route.ts +++ b/frontend/src/app/api/docs/route.ts @@ -1,6 +1,13 @@ -import { getApiDocs } from '@/lib/swagger'; -import { NextResponse } from 'next/server'; +import { getApiDocs } from "@/lib/swagger"; +import { NextResponse } from "next/server"; export async function GET() { - return NextResponse.json(getApiDocs()); -} \ No newline at end of file + try { + const docs = getApiDocs(); + return NextResponse.json(docs); + } catch (error) { + console.error("Failed to get API docs:", error); + const message = error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/frontend/src/app/api/fetch-pay-amount/route.ts b/frontend/src/app/api/fetch-pay-amount/route.ts index 4ded4f5..7ddd327 100644 --- a/frontend/src/app/api/fetch-pay-amount/route.ts +++ b/frontend/src/app/api/fetch-pay-amount/route.ts @@ -1,6 +1,11 @@ import { getXataClient } from "@/lib/utils"; import { NextResponse } from "next/server"; +interface PriceResponse { + amount?: number; + error?: string; +} + /** * @swagger * /api/fetch-pay-amount: @@ -29,26 +34,25 @@ import { NextResponse } from "next/server"; export async function GET() { try { const xata = getXataClient(); - + // Get the subscription price const priceRecord = await xata.db.SubscriptionPrice.getFirst(); if (!priceRecord) { - return NextResponse.json( - { error: "Price not found" }, - { status: 404 } - ); + const response: PriceResponse = { error: "Price not found" }; + return NextResponse.json(response, { status: 404 }); } - return NextResponse.json({ - amount: priceRecord.world_amount - }); + const response: PriceResponse = { + amount: priceRecord.world_amount, + }; + return NextResponse.json(response); } catch (error) { console.error("Error fetching subscription price:", error); - return NextResponse.json( - { error: "Failed to fetch subscription price" }, - { status: 500 } - ); + const response: PriceResponse = { + error: "Failed to fetch subscription price", + }; + return NextResponse.json(response, { status: 500 }); } } diff --git a/frontend/src/app/api/home/route.ts b/frontend/src/app/api/home/route.ts index 7528ed7..c585103 100644 --- a/frontend/src/app/api/home/route.ts +++ b/frontend/src/app/api/home/route.ts @@ -1,11 +1,28 @@ import { getXataClient } from "@/lib/utils"; +import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; +import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -import { cookies } from 'next/headers'; -import { jwtVerify } from 'jose'; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface UserResponse { + user?: { + name: string; + last_name: string; + verified: boolean; + level: string; + points: number; + maxPoints: number; + }; + error?: string; +} const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } const secret = new TextEncoder().encode(JWT_SECRET); @@ -13,51 +30,50 @@ const secret = new TextEncoder().encode(JWT_SECRET); export async function GET() { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: UserResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: UserResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - return NextResponse.json({ - user: { - name: user.name, - last_name: user.last_name, - verified: user.verified, - level: user.level + " - Coming Soon", - points: user.level_points, - maxPoints: 100 + if (!user) { + const response: UserResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); } - }); + + const response: UserResponse = { + user: { + name: user.name, + last_name: user.last_name, + verified: user.verified, + level: `${user.level} - Coming Soon`, + points: user.level_points, + maxPoints: 100, + }, + }; + + return NextResponse.json(response); + } catch { + const response: UserResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { - console.error('Error in home API route:', error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + console.error("Error in home API route:", error); + const response: UserResponse = { error: "Internal server error" }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/ideology/route.ts b/frontend/src/app/api/ideology/route.ts index 27179db..15694a3 100644 --- a/frontend/src/app/api/ideology/route.ts +++ b/frontend/src/app/api/ideology/route.ts @@ -1,7 +1,13 @@ import { getXataClient } from "@/lib/utils"; +import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; +import { cookies } from "next/headers"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -import { cookies } from 'next/headers'; -import { jwtVerify } from 'jose'; + +interface TokenPayload extends JWTPayload { + address?: string; +} interface UserScores { dipl: number; @@ -10,9 +16,14 @@ interface UserScores { scty: number; } +interface IdeologyResponse { + ideology?: string; + error?: string; +} + const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } const secret = new TextEncoder().encode(JWT_SECRET); @@ -21,12 +32,15 @@ const secret = new TextEncoder().encode(JWT_SECRET); * Calculate the similarity between user scores and ideology scores * Lower score means more similar */ -function calculateSimilarity(userScores: UserScores, ideologyScores: UserScores): number { +function calculateSimilarity( + userScores: UserScores, + ideologyScores: UserScores, +): number { const diff = { dipl: Math.abs(userScores.dipl - ideologyScores.dipl), econ: Math.abs(userScores.econ - ideologyScores.econ), govt: Math.abs(userScores.govt - ideologyScores.govt), - scty: Math.abs(userScores.scty - ideologyScores.scty) + scty: Math.abs(userScores.scty - ideologyScores.scty), }; // Return average difference (lower is better) @@ -94,99 +108,109 @@ function calculateSimilarity(userScores: UserScores, ideologyScores: UserScores) * 500: * description: Internal server error */ -export async function POST(request: Request) { +export async function POST(request: NextRequest) { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: IdeologyResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: IdeologyResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - // Get user scores from request body - const userScores = await request.json() as UserScores; + if (!user) { + const response: IdeologyResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); + } - // Validate scores - const scores = [userScores.dipl, userScores.econ, userScores.govt, userScores.scty]; - if (scores.some(score => score < 0 || score > 100 || !Number.isFinite(score))) { - return NextResponse.json( - { error: "Invalid scores. All scores must be between 0 and 100" }, - { status: 400 } - ); - } + // Get user scores from request body + const userScores = (await request.json()) as UserScores; - // Get all ideologies - const ideologies = await xata.db.Ideologies.getAll(); - - if (!ideologies.length) { - return NextResponse.json( - { error: "No ideologies found in database" }, - { status: 404 } - ); - } + // Validate scores + const scores = [ + userScores.dipl, + userScores.econ, + userScores.govt, + userScores.scty, + ]; + if ( + scores.some( + (score) => score < 0 || score > 100 || !Number.isFinite(score), + ) + ) { + const response: IdeologyResponse = { + error: "Invalid scores. All scores must be between 0 and 100", + }; + return NextResponse.json(response, { status: 400 }); + } - // Find best matching ideology - let bestMatch = ideologies[0]; - let bestSimilarity = calculateSimilarity(userScores, ideologies[0].scores as UserScores); + // Get all ideologies + const ideologies = await xata.db.Ideologies.getAll(); - for (const ideology of ideologies) { - const similarity = calculateSimilarity(userScores, ideology.scores as UserScores); - if (similarity < bestSimilarity) { - bestSimilarity = similarity; - bestMatch = ideology; + if (!ideologies.length) { + const response: IdeologyResponse = { + error: "No ideologies found in database", + }; + return NextResponse.json(response, { status: 404 }); } - } - // Get latest ideology_user_id - const latestIdeology = await xata.db.IdeologyPerUser - .sort("ideology_user_id", "desc") - .getFirst(); - const nextIdeologyId = (latestIdeology?.ideology_user_id || 0) + 1; + // Find best matching ideology + let bestMatch = ideologies[0]; + let bestSimilarity = calculateSimilarity( + userScores, + ideologies[0].scores as UserScores, + ); + + for (const ideology of ideologies) { + const similarity = calculateSimilarity( + userScores, + ideology.scores as UserScores, + ); + if (similarity < bestSimilarity) { + bestSimilarity = similarity; + bestMatch = ideology; + } + } - // Update or create IdeologyPerUser record - await xata.db.IdeologyPerUser.create({ - user: user.xata_id, - ideology: bestMatch.xata_id, - ideology_user_id: nextIdeologyId - }); + // Get latest ideology_user_id + const latestIdeology = await xata.db.IdeologyPerUser.sort( + "ideology_user_id", + "desc", + ).getFirst(); + const nextIdeologyId = (latestIdeology?.ideology_user_id || 0) + 1; - return NextResponse.json({ - ideology: bestMatch.name - }); + // Update or create IdeologyPerUser record + await xata.db.IdeologyPerUser.create({ + user: user.xata_id, + ideology: bestMatch.xata_id, + ideology_user_id: nextIdeologyId, + }); + const response: IdeologyResponse = { ideology: bestMatch.name }; + return NextResponse.json(response); + } catch { + const response: IdeologyResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error calculating ideology:", error); - return NextResponse.json( - { error: "Failed to calculate ideology" }, - { status: 500 } - ); + const response: IdeologyResponse = { + error: "Failed to calculate ideology", + }; + return NextResponse.json(response, { status: 500 }); } } @@ -221,64 +245,57 @@ export async function POST(request: Request) { export async function GET() { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: IdeologyResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: IdeologyResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - // Get user's latest ideology from IdeologyPerUser - const userIdeology = await xata.db.IdeologyPerUser - .filter({ - "user.xata_id": user.xata_id - }) - .sort("ideology_user_id", "desc") - .select(["ideology.name"]) - .getFirst(); + if (!user) { + const response: IdeologyResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); + } - if (!userIdeology || !userIdeology.ideology?.name) { - return NextResponse.json( - { error: "No ideology found for user" }, - { status: 404 } - ); - } + // Get user's latest ideology from IdeologyPerUser + const userIdeology = await xata.db.IdeologyPerUser.filter({ + "user.xata_id": user.xata_id, + }) + .sort("ideology_user_id", "desc") + .select(["ideology.name"]) + .getFirst(); - return NextResponse.json({ - ideology: userIdeology.ideology.name - }); + if (!userIdeology || !userIdeology.ideology?.name) { + const response: IdeologyResponse = { + error: "No ideology found for user", + }; + return NextResponse.json(response, { status: 404 }); + } + const response: IdeologyResponse = { + ideology: userIdeology.ideology.name, + }; + return NextResponse.json(response); + } catch { + const response: IdeologyResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error fetching ideology:", error); - return NextResponse.json( - { error: "Failed to fetch ideology" }, - { status: 500 } - ); + const response: IdeologyResponse = { error: "Failed to fetch ideology" }; + return NextResponse.json(response, { status: 500 }); } } diff --git a/frontend/src/app/api/initiate-payment/route.ts b/frontend/src/app/api/initiate-payment/route.ts index a6803ef..b77cffe 100644 --- a/frontend/src/app/api/initiate-payment/route.ts +++ b/frontend/src/app/api/initiate-payment/route.ts @@ -1,7 +1,17 @@ -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; import { getXataClient } from "@/lib/utils"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface PaymentResponse { + id?: string; + error?: string; +} /** * @swagger @@ -35,83 +45,79 @@ import { jwtVerify } from "jose"; const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } const secret = new TextEncoder().encode(JWT_SECRET); -export async function POST(req: NextRequest) { +export async function POST() { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: PaymentResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: PaymentResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - // Generate payment UUID - const uuid = crypto.randomUUID().replace(/-/g, ""); - - // Get the latest payment_id - const latestPayment = await xata.db.Payments.sort('payment_id', 'desc').getFirst(); - const nextPaymentId = (latestPayment?.payment_id || 0) + 1; - - // Create payment record - await xata.db.Payments.create({ - payment_id: nextPaymentId, - uuid: uuid, - user: user.xata_id - }); - - // Set cookie for frontend - cookies().set({ - name: "payment-nonce", - value: uuid, - httpOnly: true, - secure: true, - sameSite: "strict", - path: "/", - maxAge: 3600 // 1 hour expiry - }); - - if (process.env.NODE_ENV === 'development') { - console.log('Payment nonce generated:', uuid); - } + if (!user) { + const response: PaymentResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); + } + + // Generate payment UUID + const uuid = crypto.randomUUID().replace(/-/g, ""); + + // Get the latest payment_id + const latestPayment = await xata.db.Payments.sort( + "payment_id", + "desc", + ).getFirst(); + const nextPaymentId = (latestPayment?.payment_id || 0) + 1; - return NextResponse.json({ id: uuid }); + // Create payment record + await xata.db.Payments.create({ + payment_id: nextPaymentId, + uuid: uuid, + user: user.xata_id, + }); + // Set cookie for frontend + cookies().set({ + name: "payment-nonce", + value: uuid, + httpOnly: true, + secure: true, + sameSite: "strict", + path: "/", + maxAge: 3600, // 1 hour expiry + }); + + if (process.env.NODE_ENV === "development") { + } + + const response: PaymentResponse = { id: uuid }; + return NextResponse.json(response); + } catch { + const response: PaymentResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error initiating payment:", error); - return NextResponse.json( - { error: "Failed to initiate payment" }, - { status: 500 } - ); + const response: PaymentResponse = { error: "Failed to initiate payment" }; + return NextResponse.json(response, { status: 500 }); } } diff --git a/frontend/src/app/api/insights/[testId]/route.ts b/frontend/src/app/api/insights/[testId]/route.ts index b4d1bec..e1d1514 100644 --- a/frontend/src/app/api/insights/[testId]/route.ts +++ b/frontend/src/app/api/insights/[testId]/route.ts @@ -1,8 +1,27 @@ -import { NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; import { getXataClient } from "@/lib/utils"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; import { cookies } from "next/headers"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface Insight { + category?: string; + percentage?: number; + description?: string; + insight?: string; + left_label?: string; + right_label?: string; +} + +interface InsightResponse { + insights?: Insight[]; + error?: string; +} /** * @swagger @@ -51,110 +70,99 @@ import { cookies } from "next/headers"; const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } -export const secret = new TextEncoder().encode(JWT_SECRET); +const secret = new TextEncoder().encode(JWT_SECRET); export async function GET( - request: Request, - { params }: { params: { testId: string } } + _request: NextRequest, + { params }: { params: { testId: string } }, ) { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: InsightResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: InsightResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } - - // Validate test ID - const testId = parseInt(params.testId); - if (Number.isNaN(testId) || testId <= 0) { - return NextResponse.json( - { error: "Invalid test ID" }, - { status: 400 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - // Get test - const test = await xata.db.Tests.filter({ test_id: testId }).getFirst(); - if (!test) { - return NextResponse.json( - { error: "Test not found" }, - { status: 404 } - ); - } + if (!user) { + const response: InsightResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); + } - // Get insights for this test - const userInsights = await xata.db.InsightsPerUserCategory - .filter({ + // Validate test ID + const testId = Number.parseInt(params.testId, 10); + if (Number.isNaN(testId) || testId <= 0) { + const response: InsightResponse = { error: "Invalid test ID" }; + return NextResponse.json(response, { status: 400 }); + } + + // Get test + const test = await xata.db.Tests.filter({ test_id: testId }).getFirst(); + if (!test) { + const response: InsightResponse = { error: "Test not found" }; + return NextResponse.json(response, { status: 404 }); + } + + // Get insights for this test + const userInsights = await xata.db.InsightsPerUserCategory.filter({ "user.xata_id": user.xata_id, - "test.test_id": testId + "test.test_id": testId, }) - .select([ - "category.category_name", - "insight.insight", - "percentage", - "description", - "category.right_label", - "category.left_label" - ]) - .getMany(); - - if (!userInsights.length) { - return NextResponse.json( - { error: "No insights found for this test" }, - { status: 404 } - ); - } + .select([ + "category.category_name", + "insight.insight", + "percentage", + "description", + "category.right_label", + "category.left_label", + ]) + .getMany(); - // Transform and organize insights - const insights = userInsights.map(record => ({ - category: record.category?.category_name, - percentage: record.percentage, - description: record.description, - insight: record.insight?.insight, - left_label: record.category?.left_label, - right_label: record.category?.right_label - })).filter(insight => insight.category && insight.insight); // Filter out any incomplete records + if (!userInsights.length) { + const response: InsightResponse = { + error: "No insights found for this test", + }; + return NextResponse.json(response, { status: 404 }); + } - return NextResponse.json({ - insights - }); + // Transform and organize insights + const insights = userInsights + .map((record) => ({ + category: record.category?.category_name, + percentage: record.percentage, + description: record.description, + insight: record.insight?.insight, + left_label: record.category?.left_label, + right_label: record.category?.right_label, + })) + .filter((insight) => insight.category && insight.insight); // Filter out any incomplete records + const response: InsightResponse = { insights }; + return NextResponse.json(response); + } catch { + const response: InsightResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error fetching test insights:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); + const response: InsightResponse = { error: "Internal server error" }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/insights/route.ts b/frontend/src/app/api/insights/route.ts index ff554d3..a206706 100644 --- a/frontend/src/app/api/insights/route.ts +++ b/frontend/src/app/api/insights/route.ts @@ -1,8 +1,22 @@ -import { NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; import { getXataClient } from "@/lib/utils"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface Test { + test_id: number; + test_name?: string; +} + +interface InsightResponse { + tests?: Test[]; + error?: string; +} /** * @swagger @@ -37,79 +51,69 @@ import { cookies } from "next/headers"; const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } -export const secret = new TextEncoder().encode(JWT_SECRET); +const secret = new TextEncoder().encode(JWT_SECRET); -export async function GET(request: Request) { +export async function GET() { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: InsightResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: InsightResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - // Get distinct tests from InsightsPerUserCategory - const testsWithInsights = await xata.db.InsightsPerUserCategory - .filter({ - "user.xata_id": user.xata_id - }) - .select([ - "test.test_id", - "test.test_name" - ]) - .getMany(); - - // Create a map to store unique tests - const uniqueTests = new Map(); - - testsWithInsights.forEach(insight => { - if (insight.test?.test_id) { - uniqueTests.set(insight.test.test_id, { - test_id: insight.test.test_id, - test_name: insight.test.test_name - }); + if (!user) { + const response: InsightResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); } - }); - return NextResponse.json({ - tests: Array.from(uniqueTests.values()) - }); + // Get distinct tests from InsightsPerUserCategory + const testsWithInsights = await xata.db.InsightsPerUserCategory.filter({ + "user.xata_id": user.xata_id, + }) + .select(["test.test_id", "test.test_name"]) + .getMany(); + + // Create a map to store unique tests + const uniqueTests = new Map(); + for (const insight of testsWithInsights) { + if (insight.test?.test_id) { + uniqueTests.set(insight.test.test_id, { + test_id: insight.test.test_id, + test_name: insight.test.test_name, + }); + } + } + + const response: InsightResponse = { + tests: Array.from(uniqueTests.values()), + }; + return NextResponse.json(response); + } catch { + const response: InsightResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error fetching insights:", error); - return NextResponse.json( - { error: "Failed to fetch insights" }, - { status: 500 } - ); + const response: InsightResponse = { error: "Failed to fetch insights" }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/nonce/route.ts b/frontend/src/app/api/nonce/route.ts index 29890e3..fbbb657 100644 --- a/frontend/src/app/api/nonce/route.ts +++ b/frontend/src/app/api/nonce/route.ts @@ -1,27 +1,31 @@ +import crypto from "node:crypto"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -import crypto from 'crypto'; + +interface NonceResponse { + nonce?: string; + error?: string; +} export function GET() { try { // Generate a simple alphanumeric nonce - const nonce = crypto.randomBytes(32).toString('base64url'); + const nonce = crypto.randomBytes(32).toString("base64url"); // Store nonce in cookie with proper settings - cookies().set("siwe", nonce, { + cookies().set("siwe", nonce, { secure: true, httpOnly: true, - path: '/', + path: "/", maxAge: 300, // 5 minutes expiry - sameSite: 'lax' // Changed to lax to work with redirects + sameSite: "lax", // Changed to lax to work with redirects }); - return NextResponse.json({ nonce }); + const response: NonceResponse = { nonce }; + return NextResponse.json(response); } catch (error) { - console.error('Error generating nonce:', error); - return NextResponse.json( - { error: 'Failed to generate nonce' }, - { status: 500 } - ); + console.error("Error generating nonce:", error); + const response: NonceResponse = { error: "Failed to generate nonce" }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/tests/[testId]/instructions/route.ts b/frontend/src/app/api/tests/[testId]/instructions/route.ts index ae48161..16124b7 100644 --- a/frontend/src/app/api/tests/[testId]/instructions/route.ts +++ b/frontend/src/app/api/tests/[testId]/instructions/route.ts @@ -1,6 +1,13 @@ import { getXataClient } from "@/lib/utils"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +interface InstructionResponse { + description?: string; + total_questions?: number; + error?: string; +} + /** * @swagger * /api/tests/{testId}/instructions: @@ -36,41 +43,38 @@ import { NextResponse } from "next/server"; * description: Internal server error */ export async function GET( - request: Request, - { params }: { params: { testId: string } } + _request: NextRequest, + { params }: { params: { testId: string } }, ) { try { const xata = getXataClient(); // Validate testId - const testId = parseInt(params.testId); + const testId = Number.parseInt(params.testId, 10); if (Number.isNaN(testId) || testId <= 0) { - return NextResponse.json( - { error: "Invalid test ID" }, - { status: 400 } - ); + const response: InstructionResponse = { error: "Invalid test ID" }; + return NextResponse.json(response, { status: 400 }); } + // Get test details and total questions count const test = await xata.db.Tests.filter({ - test_id: testId + test_id: testId, }).getFirst(); if (!test) { - return NextResponse.json( - { error: "Test not found" }, - { status: 404 } - ); + const response: InstructionResponse = { error: "Test not found" }; + return NextResponse.json(response, { status: 404 }); } - return NextResponse.json({ + const response: InstructionResponse = { description: test.test_description, - total_questions: test.total_questions - }); - + total_questions: test.total_questions, + }; + return NextResponse.json(response); } catch (error) { console.error("Error fetching test instructions:", error); - return NextResponse.json( - { error: "Failed to fetch test instructions" }, - { status: 500 } - ); + const response: InstructionResponse = { + error: "Failed to fetch test instructions", + }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/tests/[testId]/progress/route.ts b/frontend/src/app/api/tests/[testId]/progress/route.ts index 09a274a..985641a 100644 --- a/frontend/src/app/api/tests/[testId]/progress/route.ts +++ b/frontend/src/app/api/tests/[testId]/progress/route.ts @@ -1,113 +1,129 @@ import { getXataClient } from "@/lib/utils"; +import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; +import { cookies } from "next/headers"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -import { cookies } from 'next/headers'; -import { jwtVerify } from 'jose'; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface Score { + econ: number; + dipl: number; + govt: number; + scty: number; +} + +interface ProgressResponse { + currentQuestion?: number; + answers?: Record; + scores?: Score; + message?: string; + error?: string; +} const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } -export const secret = new TextEncoder().encode(JWT_SECRET); +const secret = new TextEncoder().encode(JWT_SECRET); export async function GET( - request: Request, - { params }: { params: { testId: string } } + _request: NextRequest, + { params }: { params: { testId: string } }, ) { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: ProgressResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); - } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } + const typedPayload = payload as TokenPayload; - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + if (!typedPayload.address) { + const response: ProgressResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } - // Get test progress - const progress = await xata.db.UserTestProgress.filter({ - "user.xata_id": user.xata_id, - "test.test_id": parseInt(params.testId) - }) - .select(["*", "current_question.question_id"]) - .getFirst(); + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - if (!progress) { - return NextResponse.json({ - currentQuestion: 0, - answers: {}, - scores: { econ: 0, dipl: 0, govt: 0, scty: 0 } - }); - } + if (!user) { + const response: ProgressResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); + } - return NextResponse.json({ - currentQuestion: progress.current_question?.question_id || 0, - answers: progress.answers || {}, - scores: progress.score || { econ: 0, dipl: 0, govt: 0, scty: 0 } - }); + // Get test progress + const progress = await xata.db.UserTestProgress.filter({ + "user.xata_id": user.xata_id, + "test.test_id": Number.parseInt(params.testId, 10), + }) + .select(["*", "current_question.question_id"]) + .getFirst(); + + if (!progress) { + const response: ProgressResponse = { + currentQuestion: 0, + answers: {}, + scores: { econ: 0, dipl: 0, govt: 0, scty: 0 }, + }; + return NextResponse.json(response); + } + const response: ProgressResponse = { + currentQuestion: progress.current_question?.question_id || 0, + answers: progress.answers || {}, + scores: progress.score || { econ: 0, dipl: 0, govt: 0, scty: 0 }, + }; + return NextResponse.json(response); + } catch { + const response: ProgressResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error fetching progress:", error); - return NextResponse.json( - { error: "Failed to fetch progress" }, - { status: 500 } - ); + const response: ProgressResponse = { error: "Failed to fetch progress" }; + return NextResponse.json(response, { status: 500 }); } } export async function POST( - request: Request, - { params }: { params: { testId: string } } + request: NextRequest, + { params }: { params: { testId: string } }, ) { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - const token = cookies().get('session')?.value; if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: ProgressResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: ProgressResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); + if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); + const response: ProgressResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); } const body = await request.json(); @@ -116,31 +132,27 @@ export async function POST( // Get or create progress record let progress = await xata.db.UserTestProgress.filter({ "user.xata_id": user.xata_id, - "test.test_id": parseInt(params.testId) + "test.test_id": Number.parseInt(params.testId, 10), }).getFirst(); // Get the current question record const questionRecord = await xata.db.Questions.filter({ - question_id: currentQuestion + question_id: currentQuestion, }).getFirst(); if (!questionRecord) { - return NextResponse.json( - { error: "Question not found" }, - { status: 404 } - ); + const response: ProgressResponse = { error: "Question not found" }; + return NextResponse.json(response, { status: 404 }); } if (!progress) { - const test = await xata.db.Tests.filter({ - test_id: parseInt(params.testId) + const test = await xata.db.Tests.filter({ + test_id: Number.parseInt(params.testId, 10), }).getFirst(); if (!test) { - return NextResponse.json( - { error: "Test not found" }, - { status: 404 } - ); + const response: ProgressResponse = { error: "Test not found" }; + return NextResponse.json(response, { status: 404 }); } progress = await xata.db.UserTestProgress.create({ @@ -150,28 +162,26 @@ export async function POST( score: scores, status: "in_progress", started_at: new Date(), - current_question: { xata_id: questionRecord.xata_id } + current_question: { xata_id: questionRecord.xata_id }, }); } else { await progress.update({ - answers: { - ...progress.answers as object, - [questionId]: answer + answers: { + ...(progress.answers as Record), + [questionId]: answer, }, score: scores, - current_question: { xata_id: questionRecord.xata_id } + current_question: { xata_id: questionRecord.xata_id }, }); } - return NextResponse.json({ - message: "Progress saved successfully" - }); - + const response: ProgressResponse = { + message: "Progress saved successfully", + }; + return NextResponse.json(response); } catch (error) { console.error("Error saving progress:", error); - return NextResponse.json( - { error: "Failed to save progress" }, - { status: 500 } - ); + const response: ProgressResponse = { error: "Failed to save progress" }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/tests/[testId]/questions/route.ts b/frontend/src/app/api/tests/[testId]/questions/route.ts index b24325f..4688f97 100644 --- a/frontend/src/app/api/tests/[testId]/questions/route.ts +++ b/frontend/src/app/api/tests/[testId]/questions/route.ts @@ -1,36 +1,45 @@ import { getXataClient } from "@/lib/utils"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +interface Question { + id: number; + question: string; + effect: unknown; +} + +interface QuestionResponse { + questions?: Question[]; + error?: string; +} + export async function GET( - request: Request, - { params }: { params: { testId: string } } + _request: NextRequest, + { params }: { params: { testId: string } }, ) { try { - const testId = parseInt(params.testId); + const testId = Number.parseInt(params.testId, 10); // Validate the test ID - if (isNaN(testId) || testId <= 0) { - return NextResponse.json( - { error: "Invalid test ID" }, - { status: 400 } - ); + if (Number.isNaN(testId) || testId <= 0) { + const response: QuestionResponse = { error: "Invalid test ID" }; + return NextResponse.json(response, { status: 400 }); } const xata = getXataClient(); // Fetch all questions for the specified test - const questions = await xata.db.Questions - .filter({ "test.test_id": testId }) // Filter by the test ID + const questions = await xata.db.Questions.filter({ "test.test_id": testId }) // Filter by the test ID .select(["question_id", "question", "effect", "sort_order"]) // Select necessary fields .sort("sort_order", "asc") // Sort by sort_order .getAll(); // Check if questions were found if (!questions || questions.length === 0) { - return NextResponse.json( - { error: "No questions found for this test" }, - { status: 404 } - ); + const response: QuestionResponse = { + error: "No questions found for this test", + }; + return NextResponse.json(response, { status: 404 }); } // Transform the questions to match the expected format @@ -40,17 +49,11 @@ export async function GET( effect: q.effect, // Use the effect values from the database })); - // Return the formatted questions - return NextResponse.json( - { questions: formattedQuestions }, - { status: 200 } - ); - + const response: QuestionResponse = { questions: formattedQuestions }; + return NextResponse.json(response); } catch (error) { console.error("Error fetching questions:", error); - return NextResponse.json( - { error: "Failed to fetch questions" }, - { status: 500 } - ); + const response: QuestionResponse = { error: "Failed to fetch questions" }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/tests/[testId]/results/route.ts b/frontend/src/app/api/tests/[testId]/results/route.ts index 363e91f..ce5f167 100644 --- a/frontend/src/app/api/tests/[testId]/results/route.ts +++ b/frontend/src/app/api/tests/[testId]/results/route.ts @@ -1,20 +1,31 @@ import { getXataClient } from "@/lib/utils"; +import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; +import { cookies } from "next/headers"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -import { cookies } from 'next/headers'; -import { jwtVerify } from 'jose'; -const JWT_SECRET = process.env.JWT_SECRET; -if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); +interface TokenPayload extends JWTPayload { + address?: string; } -const secret = new TextEncoder().encode(JWT_SECRET); - interface CategoryScore { category_xata_id: string; score: number; } +interface TestResult { + category: string; + insight: string; + description: string; + percentage: number; +} + +interface ResultResponse { + results?: TestResult[]; + error?: string; +} + /** * @swagger * /api/tests/{testId}/results: @@ -56,154 +67,177 @@ interface CategoryScore { * 500: * description: Internal server error */ + +const JWT_SECRET = process.env.JWT_SECRET; +if (!JWT_SECRET) { + throw new Error("JWT_SECRET environment variable is required"); +} + +const secret = new TextEncoder().encode(JWT_SECRET); + export async function GET( - request: Request, - { params }: { params: { testId: string } } + _request: NextRequest, + { params }: { params: { testId: string } }, ) { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Try JWT session from wallet auth - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: ResultResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: ResultResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - console.error('JWT verification failed:', error); - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - // Get test progress - const progress = await xata.db.UserTestProgress.filter({ - "user.xata_id": user.xata_id, - "test.test_id": parseInt(params.testId) - }).getFirst(); - - if (!progress) { - return NextResponse.json( - { error: "Test progress not found" }, - { status: 404 } - ); - } + if (!user) { + const response: ResultResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); + } - if (!progress.score) { - return NextResponse.json( - { error: "Test not completed" }, - { status: 400 } - ); - } + // Get test progress + const progress = await xata.db.UserTestProgress.filter({ + "user.xata_id": user.xata_id, + "test.test_id": Number.parseInt(params.testId, 10), + }).getFirst(); - // Get all categories with their names - const categories = await xata.db.Categories.getAll(); - - // Map scores to categories - const categoryScores: CategoryScore[] = [ - { category_xata_id: categories.find(c => c.category_name === "Economic")?.xata_id || "", score: progress.score.econ }, - { category_xata_id: categories.find(c => c.category_name === "Civil")?.xata_id || "", score: progress.score.govt }, - { category_xata_id: categories.find(c => c.category_name === "Diplomatic")?.xata_id || "", score: progress.score.dipl }, - { category_xata_id: categories.find(c => c.category_name === "Societal")?.xata_id || "", score: progress.score.scty } - ].filter(cs => cs.category_xata_id !== ""); - - // Process each category score - const results = []; - const test = await xata.db.Tests.filter({ test_id: parseInt(params.testId) }).getFirst(); - - if (!test) { - return NextResponse.json( - { error: "Test not found" }, - { status: 404 } - ); - } + if (!progress) { + const response: ResultResponse = { error: "Test progress not found" }; + return NextResponse.json(response, { status: 404 }); + } - // Round all scores to integers - categoryScores.forEach(cs => cs.score = Math.round(cs.score)); + if (!progress.score) { + const response: ResultResponse = { error: "Test not completed" }; + return NextResponse.json(response, { status: 400 }); + } - for (const categoryScore of categoryScores) { - // Find matching insight based on score - const insight = await xata.db.Insights.filter({ - "category.xata_id": categoryScore.category_xata_id, - lower_limit: { $le: categoryScore.score }, - upper_limit: { $gt: categoryScore.score } + // Get all categories with their names + const categories = await xata.db.Categories.getAll(); + + // Map scores to categories + const categoryScores: CategoryScore[] = [ + { + category_xata_id: + categories.find((c) => c.category_name === "Economic")?.xata_id || + "", + score: progress.score.econ, + }, + { + category_xata_id: + categories.find((c) => c.category_name === "Civil")?.xata_id || "", + score: progress.score.govt, + }, + { + category_xata_id: + categories.find((c) => c.category_name === "Diplomatic")?.xata_id || + "", + score: progress.score.dipl, + }, + { + category_xata_id: + categories.find((c) => c.category_name === "Societal")?.xata_id || + "", + score: progress.score.scty, + }, + ].filter((cs) => cs.category_xata_id !== ""); + + // Process each category score + const results: TestResult[] = []; + const test = await xata.db.Tests.filter({ + test_id: Number.parseInt(params.testId, 10), }).getFirst(); - if (insight) { - // Get category details - const category = categories.find(c => c.xata_id === categoryScore.category_xata_id); - - if (category) { - // Save to InsightsPerUserCategory - const latestInsight = await xata.db.InsightsPerUserCategory - .sort("insight_user_id", "desc") - .getFirst(); - const nextInsightId = (latestInsight?.insight_user_id || 0) + 1; - - // Get range description based on score - let range = 'neutral' - if (categoryScore.score >= 45 && categoryScore.score <= 55) { - range = 'centrist' - } else if (categoryScore.score >= 35 && categoryScore.score < 45) { - range = 'moderate' - } else if (categoryScore.score >= 25 && categoryScore.score < 35) { - range = 'balanced' - } + if (!test) { + const response: ResultResponse = { error: "Test not found" }; + return NextResponse.json(response, { status: 404 }); + } - await xata.db.InsightsPerUserCategory.create({ - category: category.xata_id, - insight: insight.xata_id, - test: test.xata_id, - user: user.xata_id, - description: range, - percentage: categoryScore.score, - insight_user_id: nextInsightId - }); - - // Add to results - results.push({ - category: category.category_name, - insight: insight.insight, - description: range, - percentage: categoryScore.score - }); - } + // Round all scores to integers + for (const cs of categoryScores) { + cs.score = Math.round(cs.score); } - } - // Update progress status to completed - await progress.update({ - status: "completed", - completed_at: new Date() - }); + for (const categoryScore of categoryScores) { + // Find matching insight based on score + const insight = await xata.db.Insights.filter({ + "category.xata_id": categoryScore.category_xata_id, + lower_limit: { $le: categoryScore.score }, + upper_limit: { $gt: categoryScore.score }, + }).getFirst(); - return NextResponse.json(results); + if (insight) { + // Get category details + const category = categories.find( + (c) => c.xata_id === categoryScore.category_xata_id, + ); + + if (category) { + // Save to InsightsPerUserCategory + const latestInsight = await xata.db.InsightsPerUserCategory.sort( + "insight_user_id", + "desc", + ).getFirst(); + const nextInsightId = (latestInsight?.insight_user_id || 0) + 1; + + // Get range description based on score + let range = "neutral"; + if (categoryScore.score >= 45 && categoryScore.score <= 55) { + range = "centrist"; + } else if (categoryScore.score >= 35 && categoryScore.score < 45) { + range = "moderate"; + } else if (categoryScore.score >= 25 && categoryScore.score < 35) { + range = "balanced"; + } + + await xata.db.InsightsPerUserCategory.create({ + category: category.xata_id, + insight: insight.xata_id, + test: test.xata_id, + user: user.xata_id, + description: range, + percentage: categoryScore.score, + insight_user_id: nextInsightId, + }); + + // Add to results + results.push({ + category: category.category_name, + insight: insight.insight, + description: range, + percentage: categoryScore.score, + }); + } + } + } + // Update progress status to completed + await progress.update({ + status: "completed", + completed_at: new Date(), + }); + + const response: ResultResponse = { results }; + return NextResponse.json(response); + } catch { + const response: ResultResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error processing test results:", error); - return NextResponse.json( - { error: "Failed to process test results" }, - { status: 500 } - ); + const response: ResultResponse = { + error: "Failed to process test results", + }; + return NextResponse.json(response, { status: 500 }); } } diff --git a/frontend/src/app/api/tests/route.ts b/frontend/src/app/api/tests/route.ts index 9b24fd0..e9e5496 100644 --- a/frontend/src/app/api/tests/route.ts +++ b/frontend/src/app/api/tests/route.ts @@ -1,7 +1,34 @@ import { getXataClient } from "@/lib/utils"; -import { NextResponse } from "next/server"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface Achievement { + id: string; + title: string; + description: string; +} + +interface Test { + testId: number; + testName: string; + description: string; + totalQuestions: number; + answeredQuestions: number; + progressPercentage: number; + status: "not_started" | "in_progress" | "completed"; + achievements: Achievement[]; +} + +interface TestResponse { + tests?: Test[]; + error?: string; +} /** * @swagger @@ -16,7 +43,7 @@ import { cookies } from "next/headers"; * - Progress percentage * - Test status * - Achievements (if any) - * + * * tags: * - Tests * security: @@ -113,102 +140,101 @@ import { cookies } from "next/headers"; const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); - + throw new Error("JWT_SECRET environment variable is required"); } const secret = new TextEncoder().encode(JWT_SECRET); export async function GET() { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: TestResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: TestResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } - - // Fetch all tests with total_questions - const tests = await xata.db.Tests.getAll({ - columns: ["test_id", "test_name", "test_description", "total_questions"], - sort: { "test_id": "asc" } - }); - - // Fetch user progress for all tests - const allProgress = await xata.db.UserTestProgress.filter({ - "test.test_id": { $any: tests.map(t => t.test_id) }, - "user.xata_id": user.xata_id - }).getMany(); - - type ProgressRecord = typeof allProgress[0]; - - // Create a map of test progress - const progressByTest = allProgress.reduce>((acc, p) => { - const testId = p.test?.xata_id - if (testId) { - acc[testId] = p + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); + + if (!user) { + const response: TestResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); } - return acc - }, {}); - - const testsWithProgress = tests.map(test => { - // Get user's progress for this test - const userProgress = progressByTest[test.xata_id]; - - // Count answered questions from the progress.answers JSON - const answeredQuestions = userProgress?.answers - ? Object.keys(userProgress.answers as object).length - : 0; - - return { - testId: test.test_id, - testName: test.test_name, - description: test.test_description, - totalQuestions: test.total_questions, - answeredQuestions, - progressPercentage: Math.round((answeredQuestions / test.total_questions) * 100), - status: userProgress?.status || "not_started", - // TODO: Add achievements when implemented - achievements: [] - }; - }); - - return NextResponse.json( - { tests: testsWithProgress }, - { status: 200 } - ); + // Fetch all tests with total_questions + const tests = await xata.db.Tests.getAll({ + columns: [ + "test_id", + "test_name", + "test_description", + "total_questions", + ], + sort: { test_id: "asc" }, + }); + + // Fetch user progress for all tests + const allProgress = await xata.db.UserTestProgress.filter({ + "test.test_id": { $any: tests.map((t) => t.test_id) }, + "user.xata_id": user.xata_id, + }).getMany(); + + type ProgressRecord = (typeof allProgress)[0]; + + // Create a map of test progress + const progressByTest = allProgress.reduce>( + (acc, p) => { + const testId = p.test?.xata_id; + if (testId) { + acc[testId] = p; + } + return acc; + }, + {}, + ); + + const testsWithProgress: Test[] = tests.map((test) => { + // Get user's progress for this test + const userProgress = progressByTest[test.xata_id]; + + // Count answered questions from the progress.answers JSON + const answeredQuestions = userProgress?.answers + ? Object.keys(userProgress.answers as Record).length + : 0; + + return { + testId: test.test_id, + testName: test.test_name, + description: test.test_description, + totalQuestions: test.total_questions, + answeredQuestions, + progressPercentage: Math.round( + (answeredQuestions / test.total_questions) * 100, + ), + status: (userProgress?.status as Test["status"]) || "not_started", + achievements: [], // TODO: Add achievements when implemented + }; + }); + + const response: TestResponse = { tests: testsWithProgress }; + return NextResponse.json(response); + } catch { + const response: TestResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error fetching tests:", error); - return NextResponse.json( - { error: "Failed to fetch tests" }, - { status: 500 } - ); + const response: TestResponse = { error: "Failed to fetch tests" }; + return NextResponse.json(response, { status: 500 }); } } diff --git a/frontend/src/app/api/user/check/route.ts b/frontend/src/app/api/user/check/route.ts index 8974938..b4c195e 100644 --- a/frontend/src/app/api/user/check/route.ts +++ b/frontend/src/app/api/user/check/route.ts @@ -1,37 +1,43 @@ -import { NextRequest, NextResponse } from 'next/server'; import { getXataClient } from "@/lib/utils"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +interface CheckUserResponse { + exists: boolean; + userId?: string; + error?: string; +} export async function POST(req: NextRequest) { try { const body = await req.json(); - const { walletAddress } = body; + const { walletAddress } = body as { walletAddress: string }; if (!walletAddress) { - return new NextResponse( - JSON.stringify({ error: 'Wallet address is required' }), - { status: 400 } - ); + const response: CheckUserResponse = { + exists: false, + error: "Wallet address is required", + }; + return NextResponse.json(response, { status: 400 }); } const xata = getXataClient(); const existingUser = await xata.db.Users.filter({ wallet_address: walletAddress.toLowerCase(), - name: { $isNot: 'Temporary' } + name: { $isNot: "Temporary" }, }).getFirst(); - return new NextResponse( - JSON.stringify({ - exists: !!existingUser, - userId: existingUser?.xata_id - }), - { status: 200 } - ); - + const response: CheckUserResponse = { + exists: !!existingUser, + userId: existingUser?.xata_id, + }; + return NextResponse.json(response); } catch (error) { - console.error('Error checking user:', error); - return new NextResponse( - JSON.stringify({ error: 'Failed to check user existence' }), - { status: 500 } - ); + console.error("Error checking user:", error); + const response: CheckUserResponse = { + exists: false, + error: "Failed to check user existence", + }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/user/me/route.ts b/frontend/src/app/api/user/me/route.ts index e6ecb6e..0063a61 100644 --- a/frontend/src/app/api/user/me/route.ts +++ b/frontend/src/app/api/user/me/route.ts @@ -1,52 +1,58 @@ -import { NextRequest, NextResponse } from 'next/server'; import { getXataClient } from "@/lib/utils"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +interface UserResponse { + name?: string; + lastName?: string; + email?: string; + age?: number; + country?: string; + walletAddress?: string; + subscription?: boolean; + verified?: boolean; + createdAt?: Date; + updatedAt?: Date; + error?: string; +} export async function GET(req: NextRequest) { try { - const userId = req.headers.get('x-user-id'); - const walletAddress = req.headers.get('x-wallet-address'); + const userId = req.headers.get("x-user-id"); + const walletAddress = req.headers.get("x-wallet-address"); if (!userId || !walletAddress) { - return new NextResponse( - JSON.stringify({ error: 'Unauthorized' }), - { status: 401 } - ); + const response: UserResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } const xata = getXataClient(); const user = await xata.db.Users.filter({ wallet_address: walletAddress, - xata_id: userId + xata_id: userId, }).getFirst(); if (!user) { - return new NextResponse( - JSON.stringify({ error: 'User not found' }), - { status: 404 } - ); + const response: UserResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); } - return new NextResponse( - JSON.stringify({ - name: user.name, - lastName: user.last_name, - email: user.email, - age: user.age, - country: user.country, - walletAddress: user.wallet_address, - subscription: user.subscription, - verified: user.verified, - createdAt: user.created_at, - updatedAt: user.updated_at - }), - { status: 200 } - ); - + const response: UserResponse = { + name: user.name?.toString(), + lastName: user.last_name?.toString(), + email: user.email?.toString(), + age: user.age, + country: user.country?.toString(), + walletAddress: user.wallet_address?.toString(), + subscription: user.subscription, + verified: user.verified, + createdAt: user.created_at, + updatedAt: user.updated_at, + }; + return NextResponse.json(response); } catch (error) { - console.error('Error fetching user:', error); - return new NextResponse( - JSON.stringify({ error: 'Failed to fetch user data' }), - { status: 500 } - ); + console.error("Error fetching user:", error); + const response: UserResponse = { error: "Failed to fetch user data" }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/user/route.ts b/frontend/src/app/api/user/route.ts index 2129a65..df43c59 100644 --- a/frontend/src/app/api/user/route.ts +++ b/frontend/src/app/api/user/route.ts @@ -1,9 +1,21 @@ +import { createHash } from "@/lib/crypto"; import { getXataClient } from "@/lib/utils"; -import { NextResponse, NextRequest } from "next/server"; -import { getServerSession } from "next-auth"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; import { cookies } from "next/headers"; -import { createHash } from "@/lib/crypto"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface UserResponse { + username?: string; + subscription?: boolean; + verified?: boolean; + error?: string; +} /** * Validation functions for user data @@ -24,7 +36,7 @@ const validateWalletAddress = (address: string): boolean => const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } const secret = new TextEncoder().encode(JWT_SECRET); @@ -66,51 +78,45 @@ const secret = new TextEncoder().encode(JWT_SECRET); export async function GET() { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: UserResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: UserResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - return NextResponse.json({ - username: user.username, - subscription: user.subscription, - verified: user.verified - }); + if (!user) { + const response: UserResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); + } + const response: UserResponse = { + username: user.username?.toString(), + subscription: user.subscription, + verified: user.verified, + }; + return NextResponse.json(response); + } catch { + const response: UserResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error fetching user:", error); - return NextResponse.json( - { error: "Failed to fetch user data" }, - { status: 500 } - ); + const response: UserResponse = { error: "Failed to fetch user data" }; + return NextResponse.json(response, { status: 500 }); } } @@ -165,86 +171,93 @@ export async function POST(req: NextRequest) { try { const xata = getXataClient(); const data = await req.json(); - + // Validate all input data if (!validateAge(data.age)) { return new NextResponse( - JSON.stringify({ error: 'Invalid age. Must be between 18 and 120.' }), - { status: 400 } + JSON.stringify({ error: "Invalid age. Must be between 18 and 120." }), + { status: 400 }, ); } if (!validateString(data.name) || !validateString(data.last_name)) { return new NextResponse( - JSON.stringify({ error: 'Invalid name or last name. Must be between 2 and 50 characters.' }), - { status: 400 } + JSON.stringify({ + error: + "Invalid name or last name. Must be between 2 and 50 characters.", + }), + { status: 400 }, ); } if (!validateEmail(data.email)) { return new NextResponse( - JSON.stringify({ error: 'Invalid email format.' }), - { status: 400 } + JSON.stringify({ error: "Invalid email format." }), + { status: 400 }, ); } if (!validateWalletAddress(data.wallet_address)) { return new NextResponse( - JSON.stringify({ error: 'Invalid wallet address format.' }), - { status: 400 } + JSON.stringify({ error: "Invalid wallet address format." }), + { status: 400 }, ); } // Check if a non-temporary user already exists with this wallet address const existingUser = await xata.db.Users.filter({ - 'wallet_address': data.wallet_address, - 'name': { $isNot: 'Temporary' } + wallet_address: data.wallet_address, + name: { $isNot: "Temporary" }, }).getFirst(); if (existingUser) { return new NextResponse( - JSON.stringify({ - error: 'A user with this wallet address already exists', + JSON.stringify({ + error: "A user with this wallet address already exists", isRegistered: true, userId: existingUser.user_id, - userUuid: existingUser.user_uuid + userUuid: existingUser.user_uuid, }), - { status: 400 } + { status: 400 }, ); } // Delete any temporary users with this wallet address const tempUsers = await xata.db.Users.filter({ - 'wallet_address': data.wallet_address, - 'name': 'Temporary' + wallet_address: data.wallet_address, + name: "Temporary", }).getMany(); - + for (const tempUser of tempUsers) { await xata.db.Users.delete(tempUser.xata_id); } // Generate user_uuid and username - const userUuid = await createHash(data.wallet_address + Date.now().toString()); + const userUuid = await createHash( + data.wallet_address + Date.now().toString(), + ); const username = `${data.name.toLowerCase()}_${userUuid.slice(0, 5)}`; // Validate username if (!validateUsername(username)) { return new NextResponse( - JSON.stringify({ error: 'Failed to generate valid username.' }), - { status: 500 } + JSON.stringify({ error: "Failed to generate valid username." }), + { status: 500 }, ); } // Get the latest user_id - const latestUser = await xata.db.Users.sort('user_id', 'desc').getFirst(); + const latestUser = await xata.db.Users.sort("user_id", "desc").getFirst(); const nextUserId = (latestUser?.user_id || 0) + 1; // Get default country - const countryRecord = await xata.db.Countries.filter({ country_name: "Costa Rica" }).getFirst(); + const countryRecord = await xata.db.Countries.filter({ + country_name: "Costa Rica", + }).getFirst(); if (!countryRecord) { return new NextResponse( - JSON.stringify({ error: 'Failed to get default country.' }), - { status: 500 } + JSON.stringify({ error: "Failed to get default country." }), + { status: 500 }, ); } @@ -262,41 +275,40 @@ export async function POST(req: NextRequest) { country: countryRecord.xata_id, created_at: new Date(), updated_at: new Date(), - verified: false + verified: false, }); if (!newUser) { return new NextResponse( - JSON.stringify({ error: 'Failed to create user profile' }), - { status: 500 } + JSON.stringify({ error: "Failed to create user profile" }), + { status: 500 }, ); } // Set registration cookie const response = new NextResponse( - JSON.stringify({ - message: 'User profile created successfully', + JSON.stringify({ + message: "User profile created successfully", userId: newUser.user_id, userUuid: newUser.user_uuid, - isRegistered: true + isRegistered: true, }), - { status: 200 } + { status: 200 }, ); - response.cookies.set('registration_status', 'complete', { + response.cookies.set("registration_status", "complete", { httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/' + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", }); return response; - } catch (error) { - console.error('Error creating user:', error); + console.error("Error creating user:", error); return new NextResponse( - JSON.stringify({ error: 'Failed to create user profile' }), - { status: 500 } + JSON.stringify({ error: "Failed to create user profile" }), + { status: 500 }, ); } -} +} diff --git a/frontend/src/app/api/user/subscription/route.ts b/frontend/src/app/api/user/subscription/route.ts index 7e5f3b2..8076615 100644 --- a/frontend/src/app/api/user/subscription/route.ts +++ b/frontend/src/app/api/user/subscription/route.ts @@ -1,8 +1,19 @@ -import { NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; import { getXataClient } from "@/lib/utils"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +interface TokenPayload extends JWTPayload { + address?: string; +} + +interface SubscriptionResponse { + next_payment_date?: string; + isPro: boolean; + message?: string; + error?: string; +} /** * @swagger @@ -32,69 +43,79 @@ import { cookies } from "next/headers"; const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } -export const secret = new TextEncoder().encode(JWT_SECRET); +const secret = new TextEncoder().encode(JWT_SECRET); -export async function GET(request: Request) { +export async function GET() { try { const xata = getXataClient(); - let user; + const token = cookies().get("session")?.value; - // Get token from cookies - const token = cookies().get('session')?.value; - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + const response: SubscriptionResponse = { + error: "Unauthorized", + isPro: false, + }; + return NextResponse.json(response, { status: 401 }); } try { const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); + const typedPayload = payload as TokenPayload; + + if (!typedPayload.address) { + const response: SubscriptionResponse = { + error: "Invalid session", + isPro: false, + }; + return NextResponse.json(response, { status: 401 }); } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); - } - if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); - } + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); - if (!user.subscription_expires) { - return NextResponse.json( - { - message: "No active subscription found", - isPro: false - } - ); - } + if (!user) { + const response: SubscriptionResponse = { + error: "User not found", + isPro: false, + }; + return NextResponse.json(response, { status: 404 }); + } - // Format the date to YYYY-MM-DD - const nextPaymentDate = user.subscription_expires.toISOString().split('T')[0]; + if (!user.subscription_expires) { + const response: SubscriptionResponse = { + message: "No active subscription found", + isPro: false, + }; + return NextResponse.json(response); + } - return NextResponse.json({ - next_payment_date: nextPaymentDate, - isPro: true - }); + // Format the date to YYYY-MM-DD + const nextPaymentDate = user.subscription_expires + .toISOString() + .split("T")[0]; + const response: SubscriptionResponse = { + next_payment_date: nextPaymentDate, + isPro: true, + }; + return NextResponse.json(response); + } catch { + const response: SubscriptionResponse = { + error: "Invalid session", + isPro: false, + }; + return NextResponse.json(response, { status: 401 }); + } } catch (error) { console.error("Error fetching subscription:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); + const response: SubscriptionResponse = { + error: "Internal server error", + isPro: false, + }; + return NextResponse.json(response, { status: 500 }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/api/verify/route.ts b/frontend/src/app/api/verify/route.ts index 189b715..37ec820 100644 --- a/frontend/src/app/api/verify/route.ts +++ b/frontend/src/app/api/verify/route.ts @@ -1,12 +1,15 @@ -import { - verifyCloudProof, - IVerifyResponse, - ISuccessResult -} from "@worldcoin/minikit-js"; -import { NextRequest, NextResponse } from "next/server"; import { getXataClient } from "@/lib/utils"; +import { verifyCloudProof } from "@worldcoin/minikit-js"; +import type { ISuccessResult, IVerifyResponse } from "@worldcoin/minikit-js"; import { jwtVerify } from "jose"; +import type { JWTPayload } from "jose"; import { cookies } from "next/headers"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +interface TokenPayload extends JWTPayload { + address?: string; +} interface IRequestPayload { payload: ISuccessResult; @@ -14,9 +17,16 @@ interface IRequestPayload { signal?: string; } +interface VerifyResponse { + success?: boolean; + message?: string; + error?: string; + details?: unknown; +} + const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is not set'); + throw new Error("JWT_SECRET environment variable is not set"); } const secret = new TextEncoder().encode(JWT_SECRET); @@ -138,66 +148,77 @@ const secret = new TextEncoder().encode(JWT_SECRET); export async function POST(req: NextRequest) { try { const { payload, action, signal } = (await req.json()) as IRequestPayload; - const app_id = process.env.NEXT_PUBLIC_WLD_APP_ID as `app_${string}`; - - const verifyRes = (await verifyCloudProof(payload, app_id, action, signal)) as IVerifyResponse; - - if (verifyRes.success) { - const xata = getXataClient(); - let user; - - // Get token from cookies - const token = cookies().get('session')?.value; - - if (!token) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); - } - - try { - const { payload } = await jwtVerify(token, secret); - if (payload.address) { - user = await xata.db.Users.filter({ - wallet_address: payload.address - }).getFirst(); - } - } catch (error) { - return NextResponse.json( - { error: "Invalid session" }, - { status: 401 } - ); + const rawAppId = process.env.NEXT_PUBLIC_WLD_APP_ID; + + if (!rawAppId?.startsWith("app_")) { + const response: VerifyResponse = { + success: false, + error: "Invalid app_id configuration", + }; + return NextResponse.json(response, { status: 400 }); + } + + const app_id = rawAppId as `app_${string}`; + + const verifyRes = (await verifyCloudProof( + payload, + app_id, + action, + signal, + )) as IVerifyResponse; + + if (!verifyRes.success) { + const response: VerifyResponse = { + success: false, + error: "Verification failed", + details: verifyRes, + }; + return NextResponse.json(response, { status: 400 }); + } + + const xata = getXataClient(); + const token = cookies().get("session")?.value; + + if (!token) { + const response: VerifyResponse = { error: "Unauthorized" }; + return NextResponse.json(response, { status: 401 }); + } + + try { + const { payload: tokenPayload } = await jwtVerify(token, secret); + const typedPayload = tokenPayload as TokenPayload; + + if (!typedPayload.address) { + const response: VerifyResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } - + + const user = await xata.db.Users.filter({ + wallet_address: typedPayload.address, + }).getFirst(); + if (!user) { - return NextResponse.json( - { error: "User not found" }, - { status: 404 } - ); + const response: VerifyResponse = { error: "User not found" }; + return NextResponse.json(response, { status: 404 }); } - // Update user's verified status using record ID await xata.db.Users.update(user.xata_id, { verified: true, - updated_at: new Date().toISOString() + updated_at: new Date().toISOString(), }); - return NextResponse.json({ + const response: VerifyResponse = { success: true, - message: 'Verification successful' - }, { status: 200 }); - - } else { - return NextResponse.json({ - error: 'Verification failed', - details: verifyRes - }, { status: 400 }); + message: "Verification successful", + }; + return NextResponse.json(response); + } catch { + const response: VerifyResponse = { error: "Invalid session" }; + return NextResponse.json(response, { status: 401 }); } } catch (error) { - console.error('Verification error:', error); - return NextResponse.json({ - error: 'Internal server error' - }, { status: 500 }); + console.error("Verification error:", error); + const response: VerifyResponse = { error: "Internal server error" }; + return NextResponse.json(response, { status: 500 }); } } diff --git a/frontend/src/app/api/wallet/route.ts b/frontend/src/app/api/wallet/route.ts index 85c4f94..5bd1d87 100644 --- a/frontend/src/app/api/wallet/route.ts +++ b/frontend/src/app/api/wallet/route.ts @@ -1,17 +1,17 @@ -import { cookies } from 'next/headers'; -import { jwtVerify } from 'jose'; -import { NextResponse } from 'next/server'; +import { jwtVerify } from "jose"; +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; -const JWT_SECRET = process.env.JWT_SECRET +const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required') + throw new Error("JWT_SECRET environment variable is required"); } -const secret = new TextEncoder().encode(JWT_SECRET) +const secret = new TextEncoder().encode(JWT_SECRET); export async function GET() { - const token = cookies().get('session')?.value; - + const token = cookies().get("session")?.value; + if (!token) { return NextResponse.json({ address: null }); } @@ -19,9 +19,9 @@ export async function GET() { try { const { payload } = await jwtVerify(token, secret); return NextResponse.json({ address: payload.address }); - } catch (error) { + } catch { // Clear invalid session cookie - cookies().delete('session'); + cookies().delete("session"); return NextResponse.json({ address: null }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/awaken-pro/page.tsx b/frontend/src/app/awaken-pro/page.tsx index f08a528..106a607 100644 --- a/frontend/src/app/awaken-pro/page.tsx +++ b/frontend/src/app/awaken-pro/page.tsx @@ -1,115 +1,124 @@ -"use client" +"use client"; -import { FilledButton } from "@/components/ui/FilledButton" -import { Crown, CheckCircle2, Sparkles } from "lucide-react" -import { cn } from "@/lib/utils" -import { MiniKit, tokenToDecimals, Tokens, PayCommandInput } from '@worldcoin/minikit-js' -import { useRouter } from "next/navigation" -import { useState, useEffect } from "react" -import { motion } from "framer-motion" +import { FilledButton } from "@/components/ui/FilledButton"; +import { cn } from "@/lib/utils"; +import { + MiniKit, + type PayCommandInput, + type Tokens, + tokenToDecimals, +} from "@worldcoin/minikit-js"; +import { motion } from "framer-motion"; +import { CheckCircle2, Crown, Sparkles } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; export default function AwakenProPage() { - const router = useRouter() - const [isProcessing, setIsProcessing] = useState(false) - const [currentPlan, setCurrentPlan] = useState<'Basic' | 'Pro'>('Basic') - const [payAmount, setPayAmount] = useState(0) + const router = useRouter(); + const [isProcessing, setIsProcessing] = useState(false); + const [currentPlan, setCurrentPlan] = useState<"Basic" | "Pro">("Basic"); + const [payAmount, setPayAmount] = useState(0); useEffect(() => { const fetchSubscriptionStatus = async () => { try { - const response = await fetch('/api/user/subscription') + const response = await fetch("/api/user/subscription"); if (response.ok) { - const data = await response.json() - setCurrentPlan(data.isPro ? 'Pro' : 'Basic') + const data = await response.json(); + setCurrentPlan(data.isPro ? "Pro" : "Basic"); } } catch (error) { - console.error('Error fetching subscription status:', error) + console.error("Error fetching subscription status:", error); } - } + }; const fetchPayAmount = async () => { try { - const response = await fetch('/api/fetch-pay-amount') + const response = await fetch("/api/fetch-pay-amount"); if (response.ok) { - const data = await response.json() - setPayAmount(data.amount) + const data = await response.json(); + setPayAmount(data.amount); } } catch (error) { - console.error('Error fetching pay amount:', error) + console.error("Error fetching pay amount:", error); } - } + }; - fetchSubscriptionStatus() - fetchPayAmount() - }, []) + fetchSubscriptionStatus(); + fetchPayAmount(); + }, []); const handleUpgrade = async () => { - setIsProcessing(true) + setIsProcessing(true); try { if (!MiniKit.isInstalled()) { - window.open('https://worldcoin.org/download-app', '_blank') - return + window.open("https://worldcoin.org/download-app", "_blank"); + return; } // Initiate payment - const res = await fetch('/api/initiate-payment', { - method: 'POST', - }) - const { id } = await res.json() + const res = await fetch("/api/initiate-payment", { + method: "POST", + }); + const { id } = await res.json(); // Configure payment const payload: PayCommandInput = { reference: id, - to: process.env.NEXT_PUBLIC_PAYMENT_ADDRESS!, // Your whitelisted address + to: process.env.NEXT_PUBLIC_PAYMENT_ADDRESS ?? "", tokens: [ { - symbol: Tokens.WLD, - token_amount: tokenToDecimals(payAmount, Tokens.WLD).toString(), - } + symbol: "WLD" as Tokens, + token_amount: tokenToDecimals( + payAmount, + "WLD" as Tokens, + ).toString(), + }, ], - description: 'Upgrade to Awaken Pro - 1 Month Subscription' - } + description: "Upgrade to Awaken Pro - 1 Month Subscription", + }; - const { finalPayload } = await MiniKit.commandsAsync.pay(payload) + const { finalPayload } = await MiniKit.commandsAsync.pay(payload); - if (finalPayload.status === 'success') { + if (finalPayload.status === "success") { // Verify payment - const confirmRes = await fetch('/api/confirm-payment', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const confirmRes = await fetch("/api/confirm-payment", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ payload: finalPayload }), - }) + }); - const payment = await confirmRes.json() + const payment = await confirmRes.json(); if (payment.success) { // Force refresh subscription data on settings page - router.refresh() - router.push('/settings?upgrade=success') + router.refresh(); + router.push("/settings?upgrade=success"); } else { - console.error('Payment confirmation failed:', payment.error) + console.error("Payment confirmation failed:", payment.error); } } } catch (error) { - console.error('Payment error:', error) + console.error("Payment error:", error); } finally { - setIsProcessing(false) + setIsProcessing(false); } - } + }; return (
-

Step Into the Next Level

- Current plan: {' '} - + Current plan:{" "} + {currentPlan}

@@ -118,16 +127,15 @@ export default function AwakenProPage() { {/* Upgrade Card */}
- -
@@ -139,11 +147,14 @@ export default function AwakenProPage() {
-
{payAmount} WLD
-
Per month, billed monthly
+
+ {payAmount} WLD +
+
+ Per month, billed monthly +
-
{[ "Advanced Insights", @@ -152,8 +163,8 @@ export default function AwakenProPage() { "Priority support", "Soon chat with AI", "More coming soon...", - ].map((feature, index) => ( -
+ ].map((feature) => ( +
{feature}
@@ -168,40 +179,43 @@ export default function AwakenProPage() { boxShadow: [ "0 8px 16px rgba(227,108,89,0.3)", "0 12px 24px rgba(227,108,89,0.4)", - "0 8px 16px rgba(227,108,89,0.3)" - ] + "0 8px 16px rgba(227,108,89,0.3)", + ], }} transition={{ duration: 2, - repeat: Infinity, - ease: "easeInOut" + repeat: Number.POSITIVE_INFINITY, + ease: "easeInOut", }} className="relative" > {/* Pulsing background effect */}
- + {/* Floating particles effect */}
- {[...Array(3)].map((_, i) => ( + {["top", "middle", "bottom"].map((position) => ( ))} @@ -220,19 +234,19 @@ export default function AwakenProPage() { "transform transition-all duration-300", "relative overflow-hidden", "border border-white/10", - "h-16" // Increased height for better visibility + "h-16", // Increased height for better visibility )} > -
- +
- +
@@ -241,7 +255,10 @@ export default function AwakenProPage() { Processing ... @@ -249,7 +266,10 @@ export default function AwakenProPage() { ) : ( Upgrade to Pro @@ -261,5 +281,5 @@ export default function AwakenProPage() {
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/app/ideology-test/page.tsx b/frontend/src/app/ideology-test/page.tsx index db1cd50..084e6aa 100644 --- a/frontend/src/app/ideology-test/page.tsx +++ b/frontend/src/app/ideology-test/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { useState, useEffect } from "react"; +import type { Question } from "@/app/types"; import { FilledButton } from "@/components/ui/FilledButton"; -import { ProgressBar } from "@/components/ui/ProgressBar"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Question } from "@/app/types"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { ProgressBar } from "@/components/ui/ProgressBar"; import { cn } from "@/lib/utils"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; const answerOptions = [ { label: "Strongly Agree", multiplier: 1.0 }, @@ -19,7 +19,7 @@ const answerOptions = [ export default function IdeologyTest() { const router = useRouter(); const searchParams = useSearchParams(); - const testId = searchParams.get('testId') || '1'; + const testId = searchParams.get("testId") || "1"; const [currentQuestion, setCurrentQuestion] = useState(0); const [questions, setQuestions] = useState([]); @@ -40,15 +40,20 @@ export default function IdeologyTest() { const data = await response.json(); if (data.answers && Object.keys(data.answers).length > 0) { const lastAnsweredId = Object.keys(data.answers).pop(); - const lastAnsweredIndex = loadedQuestions.findIndex(q => q.id.toString() === lastAnsweredId); - const nextQuestionIndex = Math.min(lastAnsweredIndex + 1, loadedQuestions.length - 1); + const lastAnsweredIndex = loadedQuestions.findIndex( + (q) => q.id.toString() === lastAnsweredId, + ); + const nextQuestionIndex = Math.min( + lastAnsweredIndex + 1, + loadedQuestions.length - 1, + ); setCurrentQuestion(nextQuestionIndex); setScores(data.scores || { econ: 0, dipl: 0, govt: 0, scty: 0 }); setUserAnswers(data.answers); } } } catch (error) { - console.error('Error loading progress:', error); + console.error("Error loading progress:", error); } finally { setLoading(false); } @@ -77,10 +82,22 @@ export default function IdeologyTest() { setIsSubmitting(true); try { - const maxEcon = questions.reduce((sum, q) => sum + Math.abs(q.effect.econ), 0); - const maxDipl = questions.reduce((sum, q) => sum + Math.abs(q.effect.dipl), 0); - const maxGovt = questions.reduce((sum, q) => sum + Math.abs(q.effect.govt), 0); - const maxScty = questions.reduce((sum, q) => sum + Math.abs(q.effect.scty), 0); + const maxEcon = questions.reduce( + (sum, q) => sum + Math.abs(q.effect.econ), + 0, + ); + const maxDipl = questions.reduce( + (sum, q) => sum + Math.abs(q.effect.dipl), + 0, + ); + const maxGovt = questions.reduce( + (sum, q) => sum + Math.abs(q.effect.govt), + 0, + ); + const maxScty = questions.reduce( + (sum, q) => sum + Math.abs(q.effect.scty), + 0, + ); const econScore = ((scores.econ + maxEcon) / (2 * maxEcon)) * 100; const diplScore = ((scores.dipl + maxDipl) / (2 * maxDipl)) * 100; @@ -91,47 +108,47 @@ export default function IdeologyTest() { econ: Math.round(econScore), dipl: Math.round(diplScore), govt: Math.round(govtScore), - scty: Math.round(sctyScore) + scty: Math.round(sctyScore), }; const response = await fetch(`/api/tests/${testId}/progress`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ questionId: questions[currentQuestion].id, currentQuestion: questions[currentQuestion].id, scores: roundedScores, - isComplete: true - }) + isComplete: true, + }), }); if (!response.ok) { - throw new Error('Failed to save final answers'); + throw new Error("Failed to save final answers"); } - + const resultsResponse = await fetch(`/api/tests/${testId}/results`); if (!resultsResponse.ok) { - throw new Error('Failed to save final results'); + throw new Error("Failed to save final results"); } // Calculate ideology based on final scores - const ideologyResponse = await fetch('/api/ideology', { - method: 'POST', + const ideologyResponse = await fetch("/api/ideology", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify(roundedScores) + body: JSON.stringify(roundedScores), }); if (!ideologyResponse.ok) { - throw new Error('Failed to calculate ideology'); + throw new Error("Failed to calculate ideology"); } router.push(`/insights?testId=${testId}`); } catch (error) { - console.error('Error ending test:', error); + console.error("Error ending test:", error); setIsSubmitting(false); } }; @@ -150,32 +167,32 @@ export default function IdeologyTest() { try { const response = await fetch(`/api/tests/${testId}/progress`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ questionId: question.id, answer: multiplier, currentQuestion: question.id, - scores: updatedScores - }) + scores: updatedScores, + }), }); if (!response.ok) { - throw new Error('Failed to save progress'); + throw new Error("Failed to save progress"); } - setUserAnswers(prev => ({ + setUserAnswers((prev) => ({ ...prev, - [question.id]: multiplier + [question.id]: multiplier, })); if (currentQuestion < questions.length - 1) { setCurrentQuestion(currentQuestion + 1); } } catch (error) { - console.error('Error saving progress:', error); + console.error("Error saving progress:", error); } }; @@ -183,20 +200,20 @@ export default function IdeologyTest() { if (currentQuestion < totalQuestions - 1) { const nextQuestion = currentQuestion + 1; setCurrentQuestion(nextQuestion); - + try { await fetch(`/api/tests/${testId}/progress`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ currentQuestion: questions[nextQuestion].id, - scores - }) + scores, + }), }); } catch (error) { - console.error('Error saving progress:', error); + console.error("Error saving progress:", error); } } }; @@ -205,20 +222,20 @@ export default function IdeologyTest() { if (currentQuestion > 0) { const prevQuestion = currentQuestion - 1; setCurrentQuestion(prevQuestion); - + try { await fetch(`/api/tests/${testId}/progress`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ currentQuestion: questions[prevQuestion].id, - scores - }) + scores, + }), }); } catch (error) { - console.error('Error saving progress:', error); + console.error("Error saving progress:", error); } } }; @@ -226,26 +243,31 @@ export default function IdeologyTest() { const handleLeaveTest = async () => { try { await fetch(`/api/tests/${testId}/progress`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ currentQuestion: questions[currentQuestion].id, - scores - }) + scores, + }), }); - - router.push('/test-selection'); + + router.push("/test-selection"); } catch (error) { - console.error('Error saving progress:', error); - router.push('/test-selection'); + console.error("Error saving progress:", error); + router.push("/test-selection"); } }; if (loading) return ; - if (error) return
Error: {error}
; - if (!questions || questions.length === 0 || currentQuestion >= questions.length) { + if (error) + return
Error: {error}
; + if ( + !questions || + questions.length === 0 || + currentQuestion >= questions.length + ) { return
No questions found.
; } @@ -253,11 +275,7 @@ export default function IdeologyTest() {
- + Leave Test
@@ -270,10 +288,7 @@ export default function IdeologyTest() {

- +
@@ -286,17 +301,19 @@ export default function IdeologyTest() { {/* Answer Buttons Section - Fixed at bottom */}
- {answerOptions.map((answer, index) => { - const isSelected = userAnswers[questions[currentQuestion].id] === answer.multiplier; + {answerOptions.map((answer) => { + const isSelected = + userAnswers[questions[currentQuestion].id] === + answer.multiplier; return ( handleAnswer(answer.multiplier)} > @@ -326,17 +343,13 @@ export default function IdeologyTest() { disabled={isSubmitting} className={cn( "bg-green-600 hover:bg-green-700", - isSubmitting && "opacity-50 cursor-not-allowed" + isSubmitting && "opacity-50 cursor-not-allowed", )} > {isSubmitting ? "Saving..." : "End Test"} ) : ( - + Next )} @@ -346,4 +359,4 @@ export default function IdeologyTest() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/insights/page.tsx b/frontend/src/app/insights/page.tsx index 73e42c0..bddf4c9 100644 --- a/frontend/src/app/insights/page.tsx +++ b/frontend/src/app/insights/page.tsx @@ -1,13 +1,13 @@ "use client"; -import React, { useEffect, useState } from 'react'; -import { InsightResultCard } from '@/components/ui/InsightResultCard'; -import { FilledButton } from '@/components/ui/FilledButton'; -import { useSearchParams, useRouter } from 'next/navigation'; -import { motion } from 'framer-motion'; +import { FilledButton } from "@/components/ui/FilledButton"; +import { InsightResultCard } from "@/components/ui/InsightResultCard"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; -import { BookOpen } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; +import { BookOpen } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import React, { useEffect, useState } from "react"; interface Insight { category: string; @@ -29,93 +29,95 @@ export default function InsightsPage() { const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [isProUser, setIsProUser] = useState(false); - const [fullAnalysis, setFullAnalysis] = useState(''); - const [ideology, setIdeology] = useState(''); + const [fullAnalysis, setFullAnalysis] = useState(""); + const [ideology, setIdeology] = useState(""); const searchParams = useSearchParams(); - const testId = searchParams.get('testId') + const testId = searchParams.get("testId"); - const fetchInsights = async () => { - setLoading(true); - - try { - // Check user's pro status - const userResponse = await fetch('/api/user/subscription'); - if (!userResponse.ok) { - throw new Error('Failed to fetch subscription status'); - } - const userData = await userResponse.json(); - setIsProUser(userData.isPro); + useEffect(() => { + async function fetchInsights() { + try { + // Check user's pro status + const userResponse = await fetch("/api/user/subscription"); + if (!userResponse.ok) { + throw new Error("Failed to fetch subscription status"); + } + const userData = await userResponse.json(); + setIsProUser(userData.isPro); - // Fetch ideology - const ideologyResponse = await fetch('/api/ideology'); - if (ideologyResponse.ok) { - const ideologyData = await ideologyResponse.json(); - setIdeology(ideologyData.ideology); - } + // Fetch ideology + const ideologyResponse = await fetch("/api/ideology"); + if (ideologyResponse.ok) { + const ideologyData = await ideologyResponse.json(); + setIdeology(ideologyData.ideology); + } - // Fetch insights - const response = await fetch(`/api/insights/${testId}`); - if (!response.ok) { - throw new Error('Failed to fetch insights'); - } - const data = await response.json(); - setInsights(data.insights); + // Fetch insights + const response = await fetch(`/api/insights/${testId}`); + if (!response.ok) { + throw new Error("Failed to fetch insights"); + } + const data = await response.json(); + setInsights(data.insights); - // Get scores from database - const scoresResponse = await fetch(`/api/tests/${testId}/progress`); - const scoresData = await scoresResponse.json(); - const { scores } = scoresData; + // Get scores from database + const scoresResponse = await fetch(`/api/tests/${testId}/progress`); + const scoresData = await scoresResponse.json(); + const { scores } = scoresData; - // Call DeepSeek API for full analysis - if (isProUser) { - const deepSeekResponse = await fetch('/api/deepseek', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - econ: parseFloat(scores.econ || '0'), - dipl: parseFloat(scores.dipl || '0'), - govt: parseFloat(scores.govt || '0'), - scty: parseFloat(scores.scty || '0'), - }), - }); + // Call DeepSeek API for full analysis + if (isProUser) { + const deepSeekResponse = await fetch("/api/deepseek", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + econ: Number.parseFloat(scores.econ || "0"), + dipl: Number.parseFloat(scores.dipl || "0"), + govt: Number.parseFloat(scores.govt || "0"), + scty: Number.parseFloat(scores.scty || "0"), + }), + }); - if (deepSeekResponse.status === 200) { - const deepSeekData = await deepSeekResponse.json(); - setFullAnalysis(deepSeekData.analysis); - } else { - console.error('Error fetching DeepSeek analysis:', deepSeekResponse.statusText); - setFullAnalysis('Failed to generate analysis. Please try again later.'); + if (deepSeekResponse.status === 200) { + const deepSeekData = await deepSeekResponse.json(); + setFullAnalysis(deepSeekData.analysis); + } else { + console.error( + "Error fetching DeepSeek analysis:", + deepSeekResponse.statusText, + ); + setFullAnalysis( + "Failed to generate analysis. Please try again later.", + ); + } } + } catch (error) { + console.error("Error fetching insights:", error); + setFullAnalysis("Failed to generate analysis. Please try again later."); + } finally { + setLoading(false); } - - } catch (error) { - console.error('Error fetching insights:', error); - setFullAnalysis('Failed to generate analysis. Please try again later.'); - } finally { - setLoading(false); } - }; - useEffect(() => { - fetchInsights(); - }, [searchParams]); + void fetchInsights(); + }, [testId, isProUser]); const handleAdvancedInsightsClick = () => { setIsModalOpen(true); }; if (loading) { - return + return ; } return (
- Explore how your values align across key ideological dimensions.

- - - {isProUser ? 'Advanced Insights' : 'Unlock Advanced Insights'} + {isProUser ? "Advanced Insights" : "Unlock Advanced Insights"}
- {Array.isArray(insights) && insights.length > 0 ? ( - insights.map((insight, index) => ( + insights.map((insight) => ( )) ) : ( - {isModalOpen && ( - setIsModalOpen(false)} > - e.stopPropagation()} > - @@ -248,7 +257,7 @@ export default function InsightsPage() {
) : ( -

- Dive deeper into your ideological profile with Awaken Pro. Get comprehensive analysis and personalized insights. + Dive deeper into your ideological profile with Awaken Pro. Get + comprehensive analysis and personalized insights.

{ - router.push('/awaken-pro'); + router.push("/awaken-pro"); }} className="transform transition-all duration-300 hover:scale-105" > @@ -278,4 +288,4 @@ export default function InsightsPage() { )}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 51130fc..1708774 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,23 +1,29 @@ import "@/app/globals.css"; -import { Metadata } from "next"; -import { Space_Grotesk } from "next/font/google"; + +import type { Metadata } from "next"; import dynamic from "next/dynamic"; -import MiniKitProvider from "@/providers/MiniKitProvider"; -import LayoutContent from "@/components/LayoutContent"; -import { ThemeProvider } from "@/providers/ThemeProvider" -import { NotificationsProvider } from "@/providers/NotificationsProvider" +import { Space_Grotesk } from "next/font/google"; +import type * as React from "react"; + +import { LayoutContent } from "@/components/LayoutContent"; +import { MiniKitProvider } from "@/providers/MiniKitProvider"; +import { NotificationsProvider } from "@/providers/NotificationsProvider"; +import { ThemeProvider } from "@/providers/ThemeProvider"; const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - display: 'swap', - variable: '--font-space-grotesk', + subsets: ["latin"], + display: "swap", + variable: "--font-space-grotesk", }); const ErudaProvider = dynamic( - () => import("@/providers/eruda-provider").then((mod) => ({ - default: ({ children }: { children: React.ReactNode }) => {children} - })), - { ssr: false } + () => + import("@/providers/eruda-provider").then((mod) => ({ + default: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + })), + { ssr: false }, ); export const metadata: Metadata = { @@ -25,21 +31,21 @@ export const metadata: Metadata = { description: "Your journey toward understanding your true self begins here.", }; -export default function RootLayout({ - children, -}: { +interface RootLayoutProps { children: React.ReactNode; -}) { +} + +export default function RootLayout({ children }: RootLayoutProps) { return ( - + - - {children} - + {children} @@ -47,4 +53,4 @@ export default function RootLayout({ ); -} \ No newline at end of file +} diff --git a/frontend/src/app/leaderboard/page.tsx b/frontend/src/app/leaderboard/page.tsx index 34596fe..44e4eb1 100644 --- a/frontend/src/app/leaderboard/page.tsx +++ b/frontend/src/app/leaderboard/page.tsx @@ -1,22 +1,43 @@ -"use client" +"use client"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { cn } from '@/lib/utils' -import { useState } from 'react' -import { useRouter } from 'next/navigation' +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; interface LeaderboardEntry { - rank: number - name: string - points: number - initials: string + rank: number; + name: string; + points: number; + initials: string; } const topThree = [ - { rank: 1, name: "Jennifer", points: 760, color: "#4ECCA3", height: "186px", top: "149px" }, - { rank: 2, name: "Alice", points: 749, color: "#387478", height: "175px", top: "189px" }, - { rank: 3, name: "William", points: 689, color: "#E36C59", height: "186px", top: "216px" }, -] + { + rank: 1, + name: "Jennifer", + points: 760, + color: "#4ECCA3", + height: "186px", + top: "149px", + }, + { + rank: 2, + name: "Alice", + points: 749, + color: "#387478", + height: "175px", + top: "189px", + }, + { + rank: 3, + name: "William", + points: 689, + color: "#E36C59", + height: "186px", + top: "216px", + }, +]; const leaderboardEntries: LeaderboardEntry[] = [ { rank: 1, name: "Jennifer", points: 760, initials: "JE" }, @@ -25,7 +46,7 @@ const leaderboardEntries: LeaderboardEntry[] = [ { rank: 4, name: "Lydia", points: 652, initials: "LY" }, { rank: 5, name: "Erick", points: 620, initials: "ER" }, { rank: 6, name: "Ryan", points: 577, initials: "RY" }, -] +]; export default function LeaderboardPage() { const [isModalOpen, setIsModalOpen] = useState(true); // State for modal visibility @@ -40,20 +61,22 @@ export default function LeaderboardPage() {
{/* Main Content */}
-
+

Leaderboard

- +
- {topThree.map((entry, index) => ( + {topThree.map((entry) => (
- {leaderboardEntries[index].initials} + {leaderboardEntries[entry.rank - 1].initials}
@@ -82,13 +110,13 @@ export default function LeaderboardPage() {
- {leaderboardEntries.map((entry, index) => ( + {leaderboardEntries.map((entry) => (
- {String(entry.rank).padStart(2, '0')} + {String(entry.rank).padStart(2, "0")} @@ -96,8 +124,12 @@ export default function LeaderboardPage() {
- {entry.name} - {entry.points} pts + + {entry.name} + + + {entry.points} pts +
))} @@ -107,17 +139,20 @@ export default function LeaderboardPage() { {/* Coming Soon Overlay */} {isModalOpen && (
-
+

Coming Soon

- The leaderboard feature is currently under development. - Check back soon to compete with others! + The leaderboard feature is currently under development. Check back + soon to compete with others!

-
)}
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/app/not-found.tsx b/frontend/src/app/not-found.tsx index d0156b5..26a2a34 100644 --- a/frontend/src/app/not-found.tsx +++ b/frontend/src/app/not-found.tsx @@ -1,18 +1,22 @@ "use client"; -import { useRouter } from "next/navigation"; - export default function NotFound() { - return ( -
-
- {/* Error Badge */} -
-
+
+
+
+
- - {/* Main Content Card */} -
-
-
- -
- -

Page Not Found

- -

- Oops! This page has embarked on an unexpected adventure. Let's help you find your way back! -

- -
+ +
+
+
+
+ +

+ Page Not Found +

+ +

+ Oops! This page has embarked on an unexpected adventure. + Let's help you find your way back! +

+ +
- - {/* Visual Elements */} -
-
-
-
+
+ +
+
+
- ); +
+ ); } - - \ No newline at end of file diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx index 0d4b78d..89c09c3 100644 --- a/frontend/src/app/register/page.tsx +++ b/frontend/src/app/register/page.tsx @@ -1,24 +1,24 @@ "use client"; -import { Input } from "@/components/ui/input"; import { FilledButton } from "@/components/ui/FilledButton"; -import { useState, useEffect } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { NotificationDialog } from "@/components/ui/NotificationError"; +import { Input } from "@/components/ui/input"; import { COUNTRIES, type CountryCode } from "@/constants/countries"; -import { MiniKit, RequestPermissionPayload, Permission } from '@worldcoin/minikit-js' -import NotificationError from "@/components/ui/NotificationError"; - -type NotificationErrorCode = - | "user_rejected" - | "generic_error" - | "already_requested" - | "permission_disabled" - | "already_granted" +import { MiniKit } from "@worldcoin/minikit-js"; +import type { RequestPermissionPayload } from "@worldcoin/minikit-js"; +import { Permission } from "@worldcoin/minikit-js"; +import { useRouter, useSearchParams } from "next/navigation"; +import type * as React from "react"; +import { useEffect, useState } from "react"; + +type NotificationErrorCode = + | "user_rejected" + | "generic_error" + | "already_requested" + | "permission_disabled" + | "already_granted" | "unsupported_permission"; -const API_KEY = process.env.DEV_PORTAL_API_KEY; -const APP_ID = process.env.NEXT_PUBLIC_WLD_APP_ID; - export default function Register() { const router = useRouter(); const searchParams = useSearchParams(); @@ -30,41 +30,42 @@ export default function Register() { email: "", age: "", country: "CR" as CountryCode, - wallet_address: "" + wallet_address: "", }); const [error, setError] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); - const [errorCode, setErrorCode] = useState(undefined); - + const [errorCode, setErrorCode] = useState( + undefined, + ); useEffect(() => { if (!userId) { - router.push('/sign-in'); + router.push("/sign-in"); return; } // Set the wallet address from URL parameter - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - wallet_address: userId + wallet_address: userId, })); }, [userId, router]); - // TODO update database on notifications: true - + // TODO update database on notifications: true + // Function to request notification permission const requestPermission = async () => { const requestPermissionPayload: RequestPermissionPayload = { permission: Permission.Notifications, }; try { - const payload = await MiniKit.commandsAsync.requestPermission(requestPermissionPayload); - console.log('Permission granted:', payload); + const payload = await MiniKit.commandsAsync.requestPermission( + requestPermissionPayload, + ); return payload; } catch (error) { - console.error('Permission request failed:', error); - // Set error code instead of message + console.error("Permission request failed:", error); if (error instanceof Error) { setErrorCode(error.message as NotificationErrorCode); } @@ -76,7 +77,6 @@ export default function Register() { e.preventDefault(); setIsSubmitting(true); try { - // Get username from MiniKit if available const username = MiniKit.user?.username; @@ -84,7 +84,7 @@ export default function Register() { const walletAddress = formData.wallet_address; if (!walletAddress) { - throw new Error('No wallet address provided'); + throw new Error("No wallet address provided"); } // Get form data @@ -92,79 +92,90 @@ export default function Register() { name: formData.name, last_name: formData.lastName, email: formData.email, - age: parseInt(formData.age), + age: Number.parseInt(formData.age), subscription: false, wallet_address: walletAddress, - username: username, - country: COUNTRIES.find(c => c.countryCode === formData.country)?.country || "Costa Rica" + username, + country: + COUNTRIES.find((c) => c.countryCode === formData.country)?.country || + "Costa Rica", }; // Client-side validation - if (!userData.name || userData.name.length < 2 || userData.name.length > 50) { - throw new Error('Name must be between 2 and 50 characters'); + if ( + !userData.name || + userData.name.length < 2 || + userData.name.length > 50 + ) { + throw new Error("Name must be between 2 and 50 characters"); } - if (!userData.last_name || userData.last_name.length < 2 || userData.last_name.length > 50) { - throw new Error('Last name must be between 2 and 50 characters'); + if ( + !userData.last_name || + userData.last_name.length < 2 || + userData.last_name.length > 50 + ) { + throw new Error("Last name must be between 2 and 50 characters"); } - if (!userData.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) { - throw new Error('Please enter a valid email address'); + if ( + !userData.email || + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email) + ) { + throw new Error("Please enter a valid email address"); } if (!userData.age || userData.age < 18 || userData.age > 120) { - throw new Error('Age must be between 18 and 120'); + throw new Error("Age must be between 18 and 120"); } if (!/^0x[a-fA-F0-9]{40}$/.test(userData.wallet_address)) { - throw new Error('Invalid wallet address format'); + throw new Error("Invalid wallet address format"); } - console.log('Submitting user data:', userData); - // Create user - const response = await fetch('/api/user', { - method: 'POST', + const response = await fetch("/api/user", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify(userData) + body: JSON.stringify(userData), }); const result = await response.json(); - console.log('User creation response:', result); if (!response.ok) { - throw new Error(result.error || 'Failed to create user profile'); + throw new Error(result.error || "Failed to create user profile"); } // Create session - console.log('Creating session for user:', walletAddress); - const sessionResponse = await fetch('/api/auth/session', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + const sessionResponse = await fetch("/api/auth/session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ walletAddress, - isSiweVerified: true - }) + isSiweVerified: true, + }), }); if (!sessionResponse.ok) { const sessionError = await sessionResponse.json(); - throw new Error(sessionError.error || 'Failed to create session'); + throw new Error(sessionError.error || "Failed to create session"); } // Request notification permission after successful registration await requestPermission(); // Set registration completion flag and redirect to welcome page - sessionStorage.setItem('registration_complete', 'true'); - router.push('/welcome'); - + sessionStorage.setItem("registration_complete", "true"); + router.push("/welcome"); } catch (error) { - console.error('Registration error:', error); - setError(error instanceof Error ? error.message : 'Failed to complete registration'); + setError( + error instanceof Error + ? error.message + : "Failed to complete registration", + ); if (error instanceof Error) { setErrorCode(error.message as NotificationErrorCode); } @@ -181,7 +192,7 @@ export default function Register() {
{/* Render NotificationError if errorCode is set */} {errorCode && ( - setErrorCode(undefined)} /> @@ -204,12 +215,12 @@ export default function Register() {
{error && ( -
- {error} -
+
{error}
)}
- + setFormData({ ...formData, name: e.target.value })} + onChange={(e) => + setFormData({ ...formData, name: e.target.value }) + } className="h-[30px] bg-[#d9d9d9] rounded-[20px] border-0 text-black placeholder:text-gray-500" />
- + setFormData({ ...formData, lastName: e.target.value })} + onChange={(e) => + setFormData({ ...formData, lastName: e.target.value }) + } className="h-[30px] bg-[#d9d9d9] rounded-[20px] border-0 text-black placeholder:text-gray-500" />
- + setFormData({ ...formData, email: e.target.value })} + onChange={(e) => + setFormData({ ...formData, email: e.target.value }) + } className="h-[30px] bg-[#d9d9d9] rounded-[20px] border-0 text-black placeholder:text-gray-500" />
- + setFormData({ ...formData, country: e.target.value as CountryCode })} + onChange={(e) => + setFormData({ + ...formData, + country: e.target.value as CountryCode, + }) + } className="h-[30px] bg-[#d9d9d9] rounded-[20px] border-0 px-3 w-full text-black" > {COUNTRIES.map(({ countryCode, country, flag }) => ( - ))} @@ -300,7 +338,7 @@ export default function Register() { Registering...
) : ( - 'Complete Registration' + "Complete Registration" )}
@@ -308,4 +346,4 @@ export default function Register() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/results/page.tsx b/frontend/src/app/results/page.tsx index 95c7391..7c3d2e8 100644 --- a/frontend/src/app/results/page.tsx +++ b/frontend/src/app/results/page.tsx @@ -1,95 +1,96 @@ -"use client" +"use client"; -import { useState, useEffect } from 'react' -import { LoadingSpinner } from "@/components/ui/LoadingSpinner" -import { ActionCard } from "@/components/ui/ActionCard" -import { LucideIcon, FileChartColumn, Heart, Star, Trophy, Globe} from "lucide-react" -import { useRouter } from 'next/navigation' -import { cn } from '@/lib/utils' -import { motion } from 'framer-motion' +import { ActionCard } from "@/components/ui/ActionCard"; +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; +import { FileChartColumn, Globe, Heart, Star, Trophy } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; interface Test { - testId: string - testName: string + testId: string; + testName: string; } interface TestResult { - title: string - backgroundColor: string - iconBgColor: string - Icon: LucideIcon - isEnabled: boolean - testId?: string + title: string; + backgroundColor: string; + iconBgColor: string; + Icon: LucideIcon; + isEnabled: boolean; + testId?: string; } export default function ResultsPage() { - const router = useRouter() - const [loading, setLoading] = useState(true) - const [testResults, setTestResults] = useState([]) + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [testResults, setTestResults] = useState([]); useEffect(() => { const fetchResults = async () => { try { - const response = await fetch('/api/tests') - const data = await response.json() - + const response = await fetch("/api/tests"); + const data = await response.json(); + const transformedResults = data.tests.map((test: Test) => ({ title: test.testName || "Political Values Test", backgroundColor: "#387478", iconBgColor: "#2C5154", Icon: Globe, isEnabled: true, - testId: test.testId - })) - + testId: test.testId, + })); + const comingSoonCards = [ { title: "Personality Test (Coming Soon)", backgroundColor: "#778BAD", iconBgColor: "#4A5A7A", Icon: Heart, - isEnabled: false + isEnabled: false, }, { title: "Coming Soon", backgroundColor: "#DA9540", iconBgColor: "#A66B1E", Icon: Star, - isEnabled: false + isEnabled: false, }, { title: "Coming Soon", backgroundColor: "#D87566", iconBgColor: "#A44C3D", Icon: Trophy, - isEnabled: false - } - ] - - setTestResults([...transformedResults, ...comingSoonCards]) + isEnabled: false, + }, + ]; + + setTestResults([...transformedResults, ...comingSoonCards]); } catch (error) { - console.error('Error fetching results:', error) + console.error("Error fetching results:", error); } finally { - setLoading(false) + setLoading(false); } - } + }; - fetchResults() - }, []) + fetchResults(); + }, []); const handleCardClick = (testId: string) => { - router.push(`/insights?testId=${testId}`) - } + router.push(`/insights?testId=${testId}`); + }; if (loading) { - return + return ; } return (
-
- +

Insights based on your results

-
- {testResults.map((test, index) => ( + {testResults.map((test) => ( @@ -132,11 +133,13 @@ export default function ResultsPage() { iconBgColor={test.iconBgColor} Icon={test.Icon} isClickable={test.isEnabled} - onClick={() => test.testId && test.isEnabled && handleCardClick(test.testId)} + onClick={() => + test.testId && test.isEnabled && handleCardClick(test.testId) + } className={cn( "transform transition-all duration-300", "hover:scale-105 hover:-translate-y-1", - !test.isEnabled && "opacity-30 cursor-not-allowed" + !test.isEnabled && "opacity-30 cursor-not-allowed", )} /> @@ -144,5 +147,5 @@ export default function ResultsPage() {
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index b975159..a7f9f25 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -1,191 +1,195 @@ -"use client" - -import { useState, useEffect } from 'react' -import { FilledButton } from "@/components/ui/FilledButton" -import { Moon, Bell, HelpCircle, Flag, FileText, Crown } from "lucide-react" -import { SettingsCard } from "@/components/ui/SettingsCard" -import { ToggleSwitch } from "@/components/ui/ToggleSwitch" -import { MembershipCard } from "@/components/ui/MembershipCard" -import { NotificationsToggle } from "@/components/ui/NotificationsToggle" -import { useRouter } from "next/navigation" -import { cn } from "@/lib/utils" -import { LoadingSpinner } from "@/components/ui/LoadingSpinner" -import { clearVerificationSession } from "@/hooks/useVerification" -import { motion } from 'framer-motion' -import { LucideIcon, Settings } from 'lucide-react' +"use client"; + +import { FilledButton } from "@/components/ui/FilledButton"; +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { MembershipCard } from "@/components/ui/MembershipCard"; +import { NotificationsToggle } from "@/components/ui/NotificationsToggle"; +import { SettingsCard } from "@/components/ui/SettingsCard"; +import { ToggleSwitch } from "@/components/ui/ToggleSwitch"; +import { clearVerificationSession } from "@/hooks/useVerification"; +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; +import { + Bell, + Crown, + FileText, + Flag, + HelpCircle, + type LucideIcon, + Moon, + Settings, +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import type * as React from "react"; +import { useEffect, useState } from "react"; + +interface SubscriptionData { + next_payment_date: string | null; + isPro: boolean; + subscription_start: string | null; + subscription_expires: string | null; +} interface SettingItem { - Icon: LucideIcon - label: string - element?: React.ReactNode - onClick?: () => void + Icon: LucideIcon; + label: string; + element?: React.ReactNode; + onClick?: () => void; } export default function SettingsPage() { - const router = useRouter() - const [loading, setLoading] = useState(true) - const [subscriptionData, setSubscriptionData] = useState<{ - next_payment_date: string | null; - isPro: boolean; - subscription_start: string | null; - subscription_expires: string | null; - }>({ + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [subscriptionData, setSubscriptionData] = useState({ next_payment_date: null, isPro: false, subscription_start: null, - subscription_expires: null + subscription_expires: null, }); useEffect(() => { const fetchSettings = async () => { try { - const subscriptionResponse = await fetch('/api/user/subscription', { - method: 'GET', + const subscriptionResponse = await fetch("/api/user/subscription", { + method: "GET", headers: { - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0' + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", }, - credentials: 'include' + credentials: "include", }); - + if (subscriptionResponse.ok) { const data = await subscriptionResponse.json(); setSubscriptionData({ next_payment_date: data.next_payment_date || null, isPro: data.isPro || false, subscription_start: data.subscription_start || null, - subscription_expires: data.subscription_expires || null + subscription_expires: data.subscription_expires || null, }); } - } catch (error) { - console.error('Error fetching settings:', error); + } catch { + // Error handling is done via the UI loading state } finally { setLoading(false); } - } + }; const handleVisibilityChange = () => { - if (document.visibilityState === 'visible') { - fetchSettings(); + if (document.visibilityState === "visible") { + void fetchSettings(); } }; - // Fetch immediately when component mounts - fetchSettings(); + void fetchSettings(); - // Add event listener for focus and visibility change - window.addEventListener('focus', fetchSettings); - document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener("focus", fetchSettings); + document.addEventListener("visibilitychange", handleVisibilityChange); - // Cleanup return () => { - window.removeEventListener('focus', fetchSettings); - document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener("focus", fetchSettings); + document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, []); const handleUpgradeClick = () => { - router.push('/awaken-pro'); + router.push("/awaken-pro"); }; const handleLogout = async () => { try { - // Clear verification session data - clearVerificationSession() - - // Clear session cookie - const response = await fetch('/api/auth/logout', { - method: 'POST' - }) + clearVerificationSession(); + + const response = await fetch("/api/auth/logout", { + method: "POST", + }); if (response.ok) { - // Redirect to sign-in page - window.location.href = '/sign-in' + window.location.href = "/sign-in"; } - } catch (error) { - console.error('Logout error:', error) + } catch { + // Error handling is done via the UI state } - } + }; if (loading) { - return + return ; } return (
-
+
- -
- -

+
+ +

Settings

- -

+ +

- {subscriptionData.isPro && } - - {subscriptionData.isPro ? 'Premium Member' : 'Basic Member'} + {subscriptionData.isPro && ( + + )} + + {subscriptionData.isPro ? "Premium Member" : "Basic Member"}

- - {/* Membership Section */} - - - + {!subscriptionData.isPro && ( -
-
+
+
- + Upgrade to Awaken Pro
- -
+ +

Unlock advanced features @@ -197,25 +201,34 @@ export default function SettingsPage() { )} - {/* Settings Items */} - - {([ - { Icon: Bell, label: "Notifications", element: }, - { Icon: Moon, label: "Dark Theme", element: }, - { Icon: FileText, label: "View Privacy Policy", onClick: () => {} }, - { Icon: HelpCircle, label: "Help Center", onClick: () => {} }, - { Icon: Flag, label: "Report an Issue", onClick: () => {} } - ] as SettingItem[]).map((setting, index) => ( + {( + [ + { + Icon: Bell, + label: "Notifications", + element: , + }, + { Icon: Moon, label: "Dark Theme", element: }, + { + Icon: FileText, + label: "View Privacy Policy", + onClick: () => {}, + }, + { Icon: HelpCircle, label: "Help Center", onClick: () => {} }, + { Icon: Flag, label: "Report an Issue", onClick: () => {} }, + ] as SettingItem[] + ).map((setting, index) => ( -

- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/app/sign-in/page.tsx b/frontend/src/app/sign-in/page.tsx index 8e34cb4..041c61e 100644 --- a/frontend/src/app/sign-in/page.tsx +++ b/frontend/src/app/sign-in/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { useEffect, useState } from 'react'; import { FilledButton } from "@/components/ui/FilledButton"; +import { MiniKit } from "@worldcoin/minikit-js"; +import { AnimatePresence, motion } from "framer-motion"; import { Wallet } from "lucide-react"; +import Image from "next/image"; import { useRouter } from "next/navigation"; -import { MiniKit } from '@worldcoin/minikit-js'; -import { motion, AnimatePresence } from 'framer-motion'; -import Image from 'next/image'; +import { useEffect, useState } from "react"; const headingConfig = { firstLine: { @@ -16,7 +16,7 @@ const headingConfig = { secondLine: { prefix: "Transform Your", words: ["View", "Lens", "Vision", "Mind", "Path", "Light", "World"], - } + }, }; export default function SignIn() { @@ -28,18 +28,22 @@ export default function SignIn() { useEffect(() => { // Clear any old session data - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { sessionStorage.clear(); - document.cookie.split(";").forEach(function(c) { - document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); - }); + for (const c of document.cookie.split(";")) { + document.cookie = `${c.replace(/^ +/, "").replace(/=.*/, "=;expires=")}${new Date().toUTCString()};path=/`; + } } }, []); useEffect(() => { const wordInterval = setInterval(() => { - setCurrentTrueWord(prev => (prev + 1) % headingConfig.firstLine.words.length); - setCurrentPerspectiveWord(prev => (prev + 1) % headingConfig.secondLine.words.length); + setCurrentTrueWord( + (prev) => (prev + 1) % headingConfig.firstLine.words.length, + ); + setCurrentPerspectiveWord( + (prev) => (prev + 1) % headingConfig.secondLine.words.length, + ); }, 3000); return () => clearInterval(wordInterval); @@ -49,190 +53,191 @@ export default function SignIn() { setIsConnecting(true); try { setError(null); - + if (!MiniKit.isInstalled()) { router.push("https://worldcoin.org/download-app"); return; } - console.log('Requesting nonce...'); - const nonceResponse = await fetch(`/api/nonce`, { - credentials: 'include' + const nonceResponse = await fetch("/api/nonce", { + credentials: "include", }); if (!nonceResponse.ok) { const errorText = await nonceResponse.text(); - console.error('Nonce fetch failed:', errorText); + console.error("Nonce fetch failed:", errorText); throw new Error(`Failed to fetch nonce: ${errorText}`); } - + const { nonce } = await nonceResponse.json(); - console.log('Nonce received:', nonce); if (!nonce) { - throw new Error('Invalid nonce received'); + throw new Error("Invalid nonce received"); } - console.log('Initiating wallet auth...'); - const { finalPayload } = await MiniKit.commandsAsync.walletAuth({ - nonce, - statement: 'Sign in with your Ethereum wallet' - }).catch(error => { - console.error('Wallet auth command failed:', error); - if (error instanceof DOMException) { - if (error.name === 'SyntaxError') { - throw new Error('Invalid SIWE message format'); + const { finalPayload } = await MiniKit.commandsAsync + .walletAuth({ + nonce, + statement: "Sign in with your Ethereum wallet", + }) + .catch((error) => { + console.error("Wallet auth command failed:", error); + if (error instanceof DOMException) { + if (error.name === "SyntaxError") { + throw new Error("Invalid SIWE message format"); + } + throw new Error("Authentication cancelled"); } - console.log('World ID auth cancelled by user'); - throw new Error('Authentication cancelled'); - } - throw error; - }); + throw error; + }); - if (!finalPayload || finalPayload.status !== 'success') { - console.error('Wallet auth failed:', finalPayload); - throw new Error('Authentication failed'); + if (!finalPayload || finalPayload.status !== "success") { + console.error("Wallet auth failed:", finalPayload); + throw new Error("Authentication failed"); } - console.log('SIWE payload:', finalPayload); - // Get the wallet address from MiniKit after successful auth const walletAddress = MiniKit.user?.walletAddress; - console.log('Wallet address from MiniKit:', walletAddress); - console.log('Completing SIWE verification...'); - const response = await fetch('/api/complete-siwe', { - method: 'POST', + const response = await fetch("/api/complete-siwe", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - credentials: 'include', + credentials: "include", body: JSON.stringify({ payload: finalPayload, - nonce + nonce, }), }); if (!response.ok) { const errorData = await response.json(); - throw new Error(errorData.message || errorData.error || 'Failed to complete SIWE verification'); + throw new Error( + errorData.message || + errorData.error || + "Failed to complete SIWE verification", + ); } const data = await response.json(); - console.log('SIWE completion response:', data); - - if (data.status === 'error' || !data.isValid) { - throw new Error(data.message || 'Failed to verify SIWE message'); + + if (data.status === "error" || !data.isValid) { + throw new Error(data.message || "Failed to verify SIWE message"); } // Get the normalized wallet address const userWalletAddress = (walletAddress || data.address)?.toLowerCase(); - + if (!userWalletAddress) { - throw new Error('No wallet address available'); + throw new Error("No wallet address available"); } // Check if user exists using the API endpoint - const userCheckResponse = await fetch('/api/user/check', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ walletAddress: userWalletAddress }) + const userCheckResponse = await fetch("/api/user/check", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ walletAddress: userWalletAddress }), }); if (!userCheckResponse.ok) { const errorData = await userCheckResponse.json(); - throw new Error(errorData.error || 'Failed to check user existence'); + throw new Error(errorData.error || "Failed to check user existence"); } const userCheckData = await userCheckResponse.json(); if (userCheckData.exists) { // User exists, create session and redirect to home - console.log('User exists, creating session...'); try { - const sessionResponse = await fetch('/api/auth/session', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + const sessionResponse = await fetch("/api/auth/session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ walletAddress: userWalletAddress, - isSiweVerified: data.isValid - }) + isSiweVerified: data.isValid, + }), }); - if (!sessionResponse.ok) { - const sessionError = await sessionResponse.json(); - throw new Error(sessionError.error || 'Failed to create session'); - } + if (!sessionResponse.ok) { + const sessionError = await sessionResponse.json(); + throw new Error(sessionError.error || "Failed to create session"); + } // Add a small delay to ensure session is properly set - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); // Check if this is the user's first login - const userResponse = await fetch('/api/user/me', { - method: 'GET', - credentials: 'include', + const userResponse = await fetch("/api/user/me", { + method: "GET", + credentials: "include", headers: { - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache' - } + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, }); - + if (!userResponse.ok) { - console.error('Failed to fetch user data:', await userResponse.text()); + console.error( + "Failed to fetch user data:", + await userResponse.text(), + ); // If we can't fetch user data, just redirect to home - router.push('/'); + router.push("/"); return; } const userData = await userResponse.json(); - console.log('User data fetched:', userData); - + // If this is their first login (checking created_at vs updated_at) if (userData.createdAt === userData.updatedAt) { - router.push('/welcome'); + router.push("/welcome"); } else { - router.push('/'); + router.push("/"); } } catch (error) { - console.error('Session/User data error:', error); + console.error("Session/User data error:", error); // If something goes wrong after session creation, redirect to home - router.push('/'); + router.push("/"); } } else { // User doesn't exist, redirect to registration - console.log('User not found, redirecting to registration...'); - router.push(`/register?userId=${encodeURIComponent(userWalletAddress)}`); + router.push( + `/register?userId=${encodeURIComponent(userWalletAddress)}`, + ); } - } catch (error) { - if (error instanceof Error && error.message === 'Authentication cancelled') { - console.log('Authentication cancelled by user'); - setError('Authentication was cancelled'); + if ( + error instanceof Error && + error.message === "Authentication cancelled" + ) { + setError("Authentication was cancelled"); return; } - - console.error('WorldID auth failed:', { + + console.error("WorldID auth failed:", { error, - message: error instanceof Error ? error.message : 'Unknown error', + message: error instanceof Error ? error.message : "Unknown error", stack: error instanceof Error ? error.stack : undefined, type: error?.constructor?.name, code: error instanceof DOMException ? error.code : undefined, - name: error instanceof DOMException ? error.name : undefined + name: error instanceof DOMException ? error.name : undefined, }); - - let errorMessage = 'Authentication failed'; + + let errorMessage = "Authentication failed"; if (error instanceof Error) { - if (error.message === 'Invalid SIWE message format') { - errorMessage = 'Failed to create authentication message. Please try again.'; + if (error.message === "Invalid SIWE message format") { + errorMessage = + "Failed to create authentication message. Please try again."; } else { errorMessage = error.message; } - } else if (typeof error === 'string') { + } else if (typeof error === "string") { errorMessage = error; - } else if (error && typeof error === 'object' && 'message' in error) { + } else if (error && typeof error === "object" && "message" in error) { errorMessage = String(error.message); } - + setError(errorMessage); } finally { setIsConnecting(false); @@ -240,13 +245,13 @@ export default function SignIn() { }; return ( - - - MindVault Logo - +

{headingConfig.firstLine.prefix}{" "} @@ -305,17 +310,13 @@ export default function SignIn() {

- - {error && ( -
- {error} -
- )} + {error &&
{error}
} )}
- {isConnecting ? 'Connecting...' : 'World ID'} + {isConnecting ? "Connecting..." : "World ID"}
@@ -342,4 +343,4 @@ export default function SignIn() { ); -} \ No newline at end of file +} diff --git a/frontend/src/app/test-selection/page.tsx b/frontend/src/app/test-selection/page.tsx index 61ba6ec..a08fc46 100644 --- a/frontend/src/app/test-selection/page.tsx +++ b/frontend/src/app/test-selection/page.tsx @@ -1,111 +1,116 @@ -"use client" +"use client"; -import SearchBar from "@/components/ui/SearchBar" -import { TestCard } from "@/components/ui/TestCard" -import { useRouter } from 'next/navigation' -import { motion } from 'framer-motion' -import { Brain } from 'lucide-react' -import { LoadingSpinner } from "@/components/ui/LoadingSpinner" -import { useState, useEffect } from 'react' +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { SearchBar } from "@/components/ui/SearchBar"; +import { TestCard } from "@/components/ui/TestCard"; +import { motion } from "framer-motion"; +import { Brain } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; interface Achievement { - id: string - title: string - description: string + id: string; + title: string; + description: string; } interface TestData { - testId: string - title: string - totalQuestions: number - answeredQuestions: number - achievements: Achievement[] + testId: string; + title: string; + totalQuestions: number; + answeredQuestions: number; + achievements: Achievement[]; } export default function TestsPage() { - const router = useRouter() - const [loading, setLoading] = useState(true) + const router = useRouter(); + const [loading, setLoading] = useState(true); const [testData, setTestData] = useState({ testId: "", title: "", totalQuestions: 0, answeredQuestions: 0, - achievements: [] - }) - + achievements: [], + }); + useEffect(() => { const fetchData = async () => { try { // Fetch test data - const response = await fetch('/api/tests') - const data = await response.json() - + const response = await fetch("/api/tests"); + const data = await response.json(); + if (data.tests && data.tests.length > 0) { - const firstTest = data.tests[0] - + const firstTest = data.tests[0]; + // Fetch progress for this test - const progressResponse = await fetch(`/api/tests/${firstTest.testId}/progress`) - const progressData = await progressResponse.json() - - const answeredCount = progressData.answers ? Object.keys(progressData.answers).length : 0 - + const progressResponse = await fetch( + `/api/tests/${firstTest.testId}/progress`, + ); + const progressData = await progressResponse.json(); + + const answeredCount = progressData.answers + ? Object.keys(progressData.answers).length + : 0; + setTestData({ testId: firstTest.testId, title: firstTest.testName, totalQuestions: firstTest.totalQuestions || 0, answeredQuestions: answeredCount, - achievements: firstTest.achievements || [] - }) + achievements: firstTest.achievements || [], + }); } else { - console.log('No tests found in response') // Debug log } } catch (error) { - console.error('Error fetching test data:', error) + console.error("Error fetching test data:", error); } finally { - setLoading(false) + setLoading(false); } - } + }; - fetchData() - }, []) + void fetchData(); + }, []); const handleSearch = (query: string) => { - console.log('Searching for:', query) - } + if (query.toLowerCase().includes(testData.title.toLowerCase())) { + setTestData((prev) => ({ ...prev })); + } + }; if (loading) { - return + return ; } return (
-
+
- - -
- -

+
+ +

Available Tests

- -

+ +

Explore our collection of tests to understand yourself better

- - - -
+ +

Achievements coming soon! 🏆

@@ -113,22 +118,24 @@ export default function TestsPage() {
-
-
+
router.push(`/tests/instructions?testId=${testData.testId}`)} + onCardClick={() => + router.push(`/tests/instructions?testId=${testData.testId}`) + } /> -
+
- -

+ +

More tests coming soon! Stay tuned 🎉

- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/app/tests/instructions/page.tsx b/frontend/src/app/tests/instructions/page.tsx index 0b97e93..c801a1b 100644 --- a/frontend/src/app/tests/instructions/page.tsx +++ b/frontend/src/app/tests/instructions/page.tsx @@ -1,82 +1,81 @@ -'use client' +"use client"; -import { useRouter, useSearchParams } from 'next/navigation' -import { motion } from 'framer-motion' -import { Card } from '@/components/ui/card' -import { Brain, ArrowLeft, FileQuestion } from 'lucide-react' -import { useState, useEffect } from 'react' -import { LoadingSpinner } from "@/components/ui/LoadingSpinner" -import { FilledButton } from "@/components/ui/FilledButton" +import { FilledButton } from "@/components/ui/FilledButton"; +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { Card } from "@/components/ui/card"; +import { motion } from "framer-motion"; +import { ArrowLeft, Brain, FileQuestion } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; interface TestInstructions { - description: string - total_questions: number + description: string; + total_questions: number; } export default function TestInstructions() { - const router = useRouter() - const searchParams = useSearchParams() - const testId = searchParams.get('testId') || '1' // Fallback to 1 for now - - const [loading, setLoading] = useState(true) + const router = useRouter(); + const searchParams = useSearchParams(); + const testId = searchParams.get("testId") || "1"; // Fallback to 1 for now + + const [loading, setLoading] = useState(true); const [instructions, setInstructions] = useState({ - description: '', - total_questions: 0 - }) - const [currentQuestion, setCurrentQuestion] = useState(0) - const estimatedTime = Math.ceil(instructions.total_questions * 0.15) // Roughly 9 seconds per question + description: "", + total_questions: 0, + }); + const [currentQuestion, setCurrentQuestion] = useState(0); + const estimatedTime = Math.ceil(instructions.total_questions * 0.15); // Roughly 9 seconds per question useEffect(() => { const fetchData = async () => { try { - // Fetch instructions - const instructionsResponse = await fetch(`/api/tests/${testId}/instructions`); + const instructionsResponse = await fetch( + `/api/tests/${testId}/instructions`, + ); const instructionsData = await instructionsResponse.json(); - - // Fetch progress + const progressResponse = await fetch(`/api/tests/${testId}/progress`); const progressData = await progressResponse.json(); - + setInstructions({ description: instructionsData.description, - total_questions: instructionsData.total_questions + total_questions: instructionsData.total_questions, }); if (progressData.answers) { const answeredCount = Object.keys(progressData.answers).length; setCurrentQuestion(answeredCount); } - } catch (error) { - console.error('Error fetching data:', error); + console.error("Error fetching data:", error); } finally { setLoading(false); } }; - fetchData(); + void fetchData(); }, [testId]); if (loading) { - return + return ; } - const progress = (currentQuestion / instructions.total_questions) * 100 + const progress = (currentQuestion / instructions.total_questions) * 100; return ( -
+
- +
-
-
+
+
- - -
- -

+
+ +

Uncover Your Political Values

- -
-

- Before you start -

- -
-
- -

- This test consists of {instructions.total_questions} thought-provoking statements designed to explore your political beliefs. Your answers will reflect your position across eight core values. -

-
- -
-

- Please respond honestly, based on your true opinions. -

-
- -
-

- Estimated Time: {estimatedTime} min -

-

- Progress: {currentQuestion}/{instructions.total_questions} -

-
+ +

+ Before you start +

+ +
+
+ +

+ This test consists of {instructions.total_questions}{" "} + thought-provoking statements designed to explore your + political beliefs. Your answers will reflect your position + across eight core values. +

- {currentQuestion > 0 && ( -
- -
- )} +
+

+ Please respond honestly, based on your true opinions. +

+
+ +
+

+ Estimated Time:{" "} + + {estimatedTime} min + +

+

+ Progress:{" "} + + {currentQuestion}/{instructions.total_questions} + +

+
+ + {currentQuestion > 0 && ( +
+ +
+ )}
- { - try { - // Skip saving progress initially and just navigate - router.push(`/ideology-test?testId=${testId}`); - } catch (error) { - console.error('Error starting test:', error); - } + className="relative w-full overflow-hidden bg-gradient-to-r from-[#E36C59] to-[#E36C59]/90 px-8 text-white hover:from-[#E36C59]/90 hover:to-[#E36C59] sm:w-auto" + onClick={() => { + void router.push(`/ideology-test?testId=${testId}`); }} > - {currentQuestion > 0 ? 'Continue test' : 'Start test'} + {currentQuestion > 0 ? "Continue test" : "Start test"} @@ -171,9 +172,9 @@ export default function TestInstructions() {
-
-
-
+
+
+
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/app/types.ts b/frontend/src/app/types.ts index 004b582..b9d0ef2 100644 --- a/frontend/src/app/types.ts +++ b/frontend/src/app/types.ts @@ -1,33 +1,33 @@ export interface Question { - id: number; - question: string; - effect: { - econ: number; - dipl: number; - govt: number; - scty: number; - }; - } - - export interface TestResult { + id: number; + question: string; + effect: { econ: number; dipl: number; govt: number; scty: number; - } - - export interface TestProgress { - testId: number; - testName: string; - description: string; - totalQuestions: number; - answeredQuestions: number; - status: "not_started" | "in_progress" | "completed"; - achievements: Achievement[]; - } - - export interface Achievement { - id: string; - title: string; - description: string; - } \ No newline at end of file + }; +} + +export interface TestResult { + econ: number; + dipl: number; + govt: number; + scty: number; +} + +export interface TestProgress { + testId: number; + testName: string; + description: string; + totalQuestions: number; + answeredQuestions: number; + status: "not_started" | "in_progress" | "completed"; + achievements: Achievement[]; +} + +export interface Achievement { + id: string; + title: string; + description: string; +} diff --git a/frontend/src/app/welcome/page.tsx b/frontend/src/app/welcome/page.tsx index 3157559..debbf22 100644 --- a/frontend/src/app/welcome/page.tsx +++ b/frontend/src/app/welcome/page.tsx @@ -1,27 +1,28 @@ "use client"; -import { useRouter } from "next/navigation"; -import { FilledButton } from "@/components/ui/FilledButton"; import { motion } from "framer-motion"; import { Sparkles } from "lucide-react"; -import Image from 'next/image' -import { useAuth } from "@/hooks/useAuth"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { FilledButton } from "@/components/ui/FilledButton"; +import { useAuth } from "@/hooks/useAuth"; + export default function Welcome() { const router = useRouter(); const { isAuthenticated, isRegistered } = useAuth(); - const [userName, setUserName] = useState("User"); + const [userName, setUserName] = useState("User"); useEffect(() => { async function fetchUserData() { try { - const response = await fetch('/api/user/me', { - credentials: 'include', + const response = await fetch("/api/user/me", { + credentials: "include", headers: { - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache' - } + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, }); if (response.ok) { @@ -29,123 +30,112 @@ export default function Welcome() { setUserName(userData.name || "User"); } } catch (error) { - console.error('Error fetching user data:', error); + console.error("Error fetching user data:", error); } } - fetchUserData(); + void fetchUserData(); }, []); const handleGetStarted = async () => { try { - // Verify session is still valid before navigating - const sessionResponse = await fetch('/api/auth/session', { - method: 'GET', + const sessionResponse = await fetch("/api/auth/session", { + method: "GET", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - credentials: 'include' + credentials: "include", }); if (!sessionResponse.ok) { - throw new Error('Session verification failed'); + throw new Error("Session verification failed"); } - // Clear registration completion flag - sessionStorage.removeItem('registration_complete'); - - // Navigate to home page - router.replace('/'); - + sessionStorage.removeItem("registration_complete"); + router.replace("/"); } catch (error) { - console.error('Error during navigation:', error); - // If session is invalid, redirect to sign-in - router.replace('/sign-in'); + console.error("Error during navigation:", error); + router.replace("/sign-in"); } }; - // Only redirect if user manually navigates to welcome page useEffect(() => { - const registrationComplete = sessionStorage.getItem('registration_complete'); - - // Allow viewing welcome page if registration was just completed or has valid session + const registrationComplete = sessionStorage.getItem( + "registration_complete", + ); + if (registrationComplete || (isAuthenticated && isRegistered)) { - return; // Exit early without redirecting + return; } - - // Otherwise, redirect unauthorized users - router.replace('/sign-in'); + + router.replace("/sign-in"); }, [isAuthenticated, isRegistered, router]); return ( -
- {/* Background Pattern */} +
- - {/* Content Container */} -
+ +
- {/* Logo */} - Vault Logo - {/* Welcome Message */}
- - Welcome to your journey + + + Welcome to your journey + - -

+ +

Welcome, {userName}!

- {/* Main Message */} -

+

Your journey toward understanding your true self begins here. Let's unlock your potential together!

- {/* Get Started Button */} Get Started @@ -155,10 +145,9 @@ export default function Welcome() {
- {/* Decorative Elements */} -
-
-
+
+
+
); -} \ No newline at end of file +} diff --git a/frontend/src/components/BottomNav.tsx b/frontend/src/components/BottomNav.tsx index f21e1cc..cdd9263 100644 --- a/frontend/src/components/BottomNav.tsx +++ b/frontend/src/components/BottomNav.tsx @@ -1,51 +1,61 @@ -'use client' +"use client"; -import { useState, useEffect } from 'react' -import { Home, BookCheck, Trophy, Settings, Lightbulb } from 'lucide-react' -import Link from 'next/link' -import { usePathname } from 'next/navigation' +import { BookCheck, Home, Lightbulb, Settings, Trophy } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import type * as React from "react"; +import { useEffect, useState } from "react"; const navItems = [ - { icon: Home, href: '/' }, - { icon: BookCheck, href: '/test-selection' }, - { icon: Lightbulb, href: '/results' }, - { icon: Trophy, href: '/achievements' }, - { icon: Settings, href: '/settings' }, -] + { icon: Home, href: "/" }, + { icon: BookCheck, href: "/test-selection" }, + { icon: Lightbulb, href: "/results" }, + { icon: Trophy, href: "/achievements" }, + { icon: Settings, href: "/settings" }, +] as const; -export default function MobileBottomNav() { - const pathname = usePathname() - const [active, setActive] = useState(0) +export function BottomNav() { + const pathname = usePathname(); + const [active, setActive] = useState(0); - useEffect(() => { - const currentIndex = navItems.findIndex(item => item.href === pathname) - if (currentIndex !== -1) { - setActive(currentIndex) - } - }, [pathname]) + useEffect(() => { + const currentIndex = navItems.findIndex((item) => item.href === pathname); + if (currentIndex !== -1) { + setActive(currentIndex); + } + }, [pathname]); - // Hide bottom nav on ideology test page - if (pathname.includes('/ideology-test')) { - return null - } + if (pathname.includes("/ideology-test")) { + return null; + } - return ( - - ) + return ( + + ); } diff --git a/frontend/src/components/LayoutContent.tsx b/frontend/src/components/LayoutContent.tsx index 608b806..4cefd6d 100644 --- a/frontend/src/components/LayoutContent.tsx +++ b/frontend/src/components/LayoutContent.tsx @@ -1,85 +1,95 @@ "use client"; +import { BottomNav } from "@/components/BottomNav"; +import { BackgroundEffect } from "@/components/ui/BackgroundEffect"; +import { BannerTop } from "@/components/ui/BannerTop"; +import { LoadingOverlay } from "@/components/ui/LoadingOverlay"; import { useAuth } from "@/hooks/useAuth"; import { useVerification } from "@/hooks/useVerification"; import { usePathname } from "next/navigation"; -import { BannerTop } from "@/components/ui/BannerTop"; -import MobileBottomNav from "@/components/BottomNav"; -import { LoadingOverlay } from "@/components/ui/LoadingOverlay"; -import { BackgroundEffect } from "@/components/ui/BackgroundEffect"; -import { useEffect } from "react"; +import type * as React from "react"; +import { useEffect, useMemo } from "react"; + +type BackgroundVariant = "signin" | "home" | "settings" | "results" | "default"; -export default function LayoutContent({ - children, -}: { +interface LayoutContentProps { children: React.ReactNode; -}) { - const { isAuthenticated, isRegistered, loading: authLoading, refreshAuth } = useAuth(); - const { - isVerified, - isLoading: verificationLoading, - refreshVerification, - hasCheckedInitial - } = useVerification(); +} + +export function LayoutContent({ children }: LayoutContentProps) { + const { + isAuthenticated, + isRegistered, + loading: authLoading, + refreshAuth, + } = useAuth(); + + const { isVerified, refreshVerification, hasCheckedInitial } = + useVerification(); const pathname = usePathname(); - + // Page checks - const isSignInPage = pathname === "/sign-in"; - const isRegisterPage = pathname === "/register"; - const isWelcomePage = pathname === "/welcome"; - const isTestInstructions = pathname === "/tests/instructions"; - const isIdeologyTest = pathname.includes("/ideology-test"); - const isHomePage = pathname === "/"; - const isSettingsPage = pathname === "/settings"; - const isResultsPage = pathname === "/results"; - - // Debug logging for state changes - useEffect(() => { - console.log('Layout state:', { - isAuthenticated, - isRegistered, - isVerified, - authLoading, - verificationLoading, - hasCheckedInitial, - pathname - }); - }, [isAuthenticated, isRegistered, isVerified, authLoading, verificationLoading, hasCheckedInitial, pathname]); - - // Refresh both auth and verification when auth state changes + const pageStates = useMemo( + () => ({ + isSignInPage: pathname === "/sign-in", + isRegisterPage: pathname === "/register", + isWelcomePage: pathname === "/welcome", + isTestInstructions: pathname === "/tests/instructions", + isIdeologyTest: pathname.includes("/ideology-test"), + isHomePage: pathname === "/", + isSettingsPage: pathname === "/settings", + isResultsPage: pathname === "/results", + }), + [pathname], + ); + + // Auth refresh effect useEffect(() => { - // Skip auth check on auth-related pages - if (isSignInPage || isRegisterPage || isWelcomePage) { - return; - } + const { isSignInPage, isRegisterPage, isWelcomePage } = pageStates; + + if (isSignInPage || isRegisterPage || isWelcomePage) return; - console.log('Auth state changed:', { isAuthenticated, isRegistered, authLoading }); if (!authLoading) { if (isAuthenticated && isRegistered) { - console.log('Refreshing verification status...'); refreshVerification(); } else if (!isAuthenticated) { - console.log('Refreshing auth status...'); refreshAuth(); } } - }, [isAuthenticated, isRegistered, authLoading, refreshVerification, refreshAuth, isSignInPage, isRegisterPage, isWelcomePage]); - - // Determine background effect variant based on current page - const getBackgroundVariant = () => { - if (isSignInPage) return 'signin'; - if (isHomePage) return 'home'; - if (isSettingsPage) return 'settings'; - if (isResultsPage) return 'results'; - return 'default'; + }, [ + isAuthenticated, + isRegistered, + authLoading, + refreshVerification, + refreshAuth, + pageStates, + ]); + + const getBackgroundVariant = (): BackgroundVariant => { + const { isSignInPage, isHomePage, isSettingsPage, isResultsPage } = + pageStates; + + if (isSignInPage) return "signin"; + if (isHomePage) return "home"; + if (isSettingsPage) return "settings"; + if (isResultsPage) return "results"; + return "default"; }; - // Don't show loading overlay on auth-related pages + const { + isSignInPage, + isRegisterPage, + isWelcomePage, + isTestInstructions, + isIdeologyTest, + } = pageStates; + const showLoadingOverlay = !isSignInPage && !isRegisterPage && !isWelcomePage; - - // Show loading state while checking auth or initial verification - if ((authLoading || (isAuthenticated && isRegistered && !hasCheckedInitial)) && showLoadingOverlay) { - console.log('Showing loading overlay'); + + if ( + (authLoading || (isAuthenticated && isRegistered && !hasCheckedInitial)) && + showLoadingOverlay + ) { return ( <> @@ -87,34 +97,32 @@ export default function LayoutContent({ ); } - - const showBanner = isAuthenticated && + + const showBanner = + isAuthenticated && isRegistered && !isVerified && - !isSignInPage && - !isRegisterPage && + !isSignInPage && + !isRegisterPage && !isWelcomePage && !isTestInstructions && !isIdeologyTest; - - const showNav = isAuthenticated && + + const showNav = + isAuthenticated && isRegistered && - !isSignInPage && - !isRegisterPage && + !isSignInPage && + !isRegisterPage && !isWelcomePage; - console.log('Render state:', { showBanner, showNav }); - return ( -
+
{showBanner && }
-
- {children} -
+
{children}
- {showNav && } + {showNav && }
); } diff --git a/frontend/src/components/ui/AchievementButton.tsx b/frontend/src/components/ui/AchievementButton.tsx index 23e2bec..28b2cd7 100644 --- a/frontend/src/components/ui/AchievementButton.tsx +++ b/frontend/src/components/ui/AchievementButton.tsx @@ -1,24 +1,33 @@ -import { cn } from "@/lib/utils" +"use client"; -export function AchievementButton({ hasNewAchievement = true }) { - return ( - - ) -} \ No newline at end of file +import { cn } from "@/lib/utils"; +import type * as React from "react"; + +interface AchievementButtonProps { + hasNewAchievement?: boolean; +} + +export function AchievementButton({ + hasNewAchievement = true, +}: AchievementButtonProps) { + return ( + + ); +} diff --git a/frontend/src/components/ui/AchievementCard.tsx b/frontend/src/components/ui/AchievementCard.tsx index e5e1735..3f5dc7c 100644 --- a/frontend/src/components/ui/AchievementCard.tsx +++ b/frontend/src/components/ui/AchievementCard.tsx @@ -1,41 +1,41 @@ -import React from 'react' +"use client"; + +import type * as React from "react"; interface AchievementCardProps { - title?: string - description?: string - date?: string + title?: string; + description?: string; + date?: string; } -const AchievementCard: React.FC = ({ - title = "Ideology Explorer", - description = "Completed the Ideology Test", - date = "[date]" -}) => { - return ( -
-
-
-
-
-
-
-
-
-

{title}

-

{description}

-

Obtained on {date}

-
-
- ) +export function AchievementCard({ + title = "Ideology Explorer", + description = "Completed the Ideology Test", + date = "[date]", +}: AchievementCardProps) { + return ( +
+
+ ); } - -export default AchievementCard \ No newline at end of file diff --git a/frontend/src/components/ui/ActionCard.tsx b/frontend/src/components/ui/ActionCard.tsx index ff9c4d5..11decac 100644 --- a/frontend/src/components/ui/ActionCard.tsx +++ b/frontend/src/components/ui/ActionCard.tsx @@ -1,42 +1,59 @@ -import { LucideIcon } from 'lucide-react'; +"use client"; + +import type { LucideIcon } from "lucide-react"; +import type * as React from "react"; interface ActionCardProps { - title: string; - backgroundColor: string; - iconBgColor: string; - Icon: LucideIcon; - className?: string; - onClick?: () => void; - isClickable?: boolean; + title: string; + backgroundColor: string; + iconBgColor: string; + Icon: LucideIcon; + className?: string; + onClick?: () => void; + isClickable?: boolean; } export function ActionCard({ - title, - backgroundColor, - iconBgColor, - Icon, - className = '', - onClick, - isClickable = false, + title, + backgroundColor, + iconBgColor, + Icon, + className = "", + onClick, + isClickable = false, }: ActionCardProps) { - return ( -
-
-
- {title} -
-
- -
-
- ); -} \ No newline at end of file + const handleKeyDown = (event: React.KeyboardEvent) => { + if ( + isClickable && + onClick && + (event.key === "Enter" || event.key === " ") + ) { + event.preventDefault(); + onClick(); + } + }; + + return ( + + ); +} diff --git a/frontend/src/components/ui/FilledButton.tsx b/frontend/src/components/ui/FilledButton.tsx index aaa66b4..6a036be 100644 --- a/frontend/src/components/ui/FilledButton.tsx +++ b/frontend/src/components/ui/FilledButton.tsx @@ -1,50 +1,58 @@ -import * as React from "react" -import { cn } from "@/lib/utils" -import { LucideIcon } from "lucide-react" -import { buttonSizes, buttonVariants, ButtonSize, ButtonVariant } from "./styles/buttonStyles" +import type { LucideIcon } from "lucide-react"; +import type * as React from "react"; -interface FilledButtonProps extends React.ButtonHTMLAttributes { - size?: ButtonSize - variant?: ButtonVariant - fullWidth?: boolean - icon?: LucideIcon - iconClassName?: string - className?: string - children: React.ReactNode +import { cn } from "@/lib/utils"; +import { + type ButtonSize, + type ButtonVariant, + buttonSizes, + buttonVariants, +} from "./styles/buttonStyles"; + +interface FilledButtonProps + extends React.ButtonHTMLAttributes { + size?: ButtonSize; + variant?: ButtonVariant; + fullWidth?: boolean; + icon?: LucideIcon; + iconClassName?: string; + className?: string; + children: React.ReactNode; } export function FilledButton({ - size = 'md', - variant = 'default', - fullWidth = false, - icon: Icon, - iconClassName, - className, - children, - ...props + size = "md", + variant = "default", + fullWidth = false, + icon: Icon, + iconClassName, + className, + children, + ...props }: FilledButtonProps) { - return ( - - ) + return ( + + ); } diff --git a/frontend/src/components/ui/InsightResultCard.tsx b/frontend/src/components/ui/InsightResultCard.tsx index e9ec77d..3f8c7fc 100644 --- a/frontend/src/components/ui/InsightResultCard.tsx +++ b/frontend/src/components/ui/InsightResultCard.tsx @@ -1,52 +1,74 @@ -import React from 'react'; -import InsightResultTag from '@/components/ui/InsightResultTag'; +"use client"; + +import type * as React from "react"; interface InsightResultCardProps { - title: string; - insight: string; - description: string; - percentage: number; // Equals to previous values.left - left_label: string; // Equals to previous values.label.split(' / ')[0] - right_label: string; - values: { - left: number; - right: number; - label: string; - }; + title: string; + insight: string; + description: string; + percentage: number; + left_label: string; + right_label: string; + values: { + left: number; + right: number; + label: string; + }; } -export function InsightResultCard({ title, insight, description, percentage, left_label, right_label, values }: InsightResultCardProps) { - return ( -
-

{title}

-

"{insight}"

- -
-
- - - {left_label} - - - {right_label} - - -
-
-
-
-
- {/* */} -
- {description.charAt(0).toUpperCase() + description.slice(1)} -
-
-
-
- ); -} \ No newline at end of file +export function InsightResultCard({ + title, + insight, + description, + percentage, + left_label, + right_label, +}: InsightResultCardProps) { + return ( +
+

+ {title} +

+

+ “{insight}” +

+ +
+
+ + + + {right_label} + +
+
+
+
+
+
+ {description.charAt(0).toUpperCase() + description.slice(1)} +
+
+
+
+ ); +} diff --git a/frontend/src/components/ui/InsightResultTag.tsx b/frontend/src/components/ui/InsightResultTag.tsx index d28e51b..befdf80 100644 --- a/frontend/src/components/ui/InsightResultTag.tsx +++ b/frontend/src/components/ui/InsightResultTag.tsx @@ -1,19 +1,19 @@ -'use client' +"use client"; -import { useState, useEffect } from 'react' +import { useEffect, useState } from "react"; interface IdeologyTagProps { - scale: number; // Expecting a number between 0-100 + scale: number; className?: string; } function validateScale(scale: number): void { if (scale < 0 || scale > 100) { - throw new RangeError('scale must be between 0 and 100'); + throw new RangeError("scale must be between 0 and 100"); } } -export default function IdeologyTag({ scale, className = '' }: IdeologyTagProps) { +export function IdeologyTag({ scale, className = "" }: IdeologyTagProps) { validateScale(scale); const [mounted, setMounted] = useState(false); @@ -23,10 +23,10 @@ export default function IdeologyTag({ scale, className = '' }: IdeologyTagProps) }, []); const getIdeology = (scale: number) => { - if (scale >= 45 && scale <= 55) return 'centrist'; - if (scale >= 35 && scale < 45) return 'moderate'; - if (scale >= 25 && scale < 35) return 'balanced'; - return 'neutral'; + if (scale >= 45 && scale <= 55) return "centrist"; + if (scale >= 35 && scale < 45) return "moderate"; + if (scale >= 25 && scale < 35) return "balanced"; + return "neutral"; }; const ideologyValue = getIdeology(scale); @@ -37,9 +37,9 @@ export default function IdeologyTag({ scale, className = '' }: IdeologyTagProps) return (
{ideologyValue.charAt(0).toUpperCase() + ideologyValue.slice(1)}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/ui/LeaderboardButton.tsx b/frontend/src/components/ui/LeaderboardButton.tsx index db974e8..f27836b 100644 --- a/frontend/src/components/ui/LeaderboardButton.tsx +++ b/frontend/src/components/ui/LeaderboardButton.tsx @@ -1,74 +1,79 @@ -"use client" +"use client"; -import { useRouter } from 'next/navigation' +import { useRouter } from "next/navigation"; export function LeaderboardButton() { - const router = useRouter() + const router = useRouter(); - return ( - - ) -} \ No newline at end of file + return ( + + ); +} diff --git a/frontend/src/components/ui/LoadingOverlay.tsx b/frontend/src/components/ui/LoadingOverlay.tsx index a0915d2..8d4e237 100644 --- a/frontend/src/components/ui/LoadingOverlay.tsx +++ b/frontend/src/components/ui/LoadingOverlay.tsx @@ -1,11 +1,13 @@ -import { LoadingSpinner } from "./LoadingSpinner" +import type * as React from "react"; + +import { LoadingSpinner } from "./LoadingSpinner"; export function LoadingOverlay() { - return ( -
-
- -
-
- ) -} \ No newline at end of file + return ( +
+
+ +
+
+ ); +} diff --git a/frontend/src/components/ui/LoadingSpinner.tsx b/frontend/src/components/ui/LoadingSpinner.tsx index d957ac3..c818b83 100644 --- a/frontend/src/components/ui/LoadingSpinner.tsx +++ b/frontend/src/components/ui/LoadingSpinner.tsx @@ -1,17 +1,19 @@ -"use client" +"use client"; -import { cn } from "@/lib/utils" +import type * as React from "react"; + +import { cn } from "@/lib/utils"; interface LoadingSpinnerProps { - className?: string + className?: string; } export function LoadingSpinner({ className }: LoadingSpinnerProps) { - return ( -
-
- -
-
- ) -} \ No newline at end of file + return ( +
+
+ +
+
+ ); +} diff --git a/frontend/src/components/ui/MembershipCard.tsx b/frontend/src/components/ui/MembershipCard.tsx index e3d7a7d..b55e74f 100644 --- a/frontend/src/components/ui/MembershipCard.tsx +++ b/frontend/src/components/ui/MembershipCard.tsx @@ -1,37 +1,48 @@ +"use client"; + interface MembershipCardProps { - expiryDate: string - isActive: boolean - cost: number + expiryDate: string; + isActive: boolean; + cost: number; } -export function MembershipCard({ expiryDate, isActive, cost }: MembershipCardProps) { +export function MembershipCard({ + expiryDate, + isActive, + cost, +}: MembershipCardProps) { return ( -
-
-

+
+
+

Awaken Pro

-
- - {isActive ? 'Active' : 'Inactive'} +
+ + {isActive ? "Active" : "Inactive"}
- -

+ +

Your next membership payment is scheduled for {expiryDate}

- +
-
- - {cost} - - - +
+ {cost} +
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/components/ui/NotificationError.tsx b/frontend/src/components/ui/NotificationError.tsx index 34acfa0..39991a1 100644 --- a/frontend/src/components/ui/NotificationError.tsx +++ b/frontend/src/components/ui/NotificationError.tsx @@ -1,113 +1,126 @@ -"use client" +"use client"; -import { useState } from "react" +import type * as React from "react"; +import { useState } from "react"; interface NotificationDialogProps { - errorCode?: - | "user_rejected" - | "generic_error" - | "already_requested" - | "permission_disabled" - | "already_granted" - | "unsupported_permission" - onProceed?: () => void - onDecline?: () => void + errorCode?: + | "user_rejected" + | "generic_error" + | "already_requested" + | "permission_disabled" + | "already_granted" + | "unsupported_permission"; + onProceed?: () => void; + onDecline?: () => void; } -export default function NotificationDialog({ errorCode, onProceed, onDecline }: NotificationDialogProps) { - const [isClosing, setIsClosing] = useState(false) +export function NotificationDialog({ + errorCode, + onProceed, + onDecline, +}: NotificationDialogProps) { + const [isClosing, setIsClosing] = useState(false); - const getErrorMessage = () => { - switch (errorCode) { - case "user_rejected": - return "You have declined the permission request. You can change this in your settings." - case "generic_error": - return "Something went wrong. Please try again later." - case "already_requested": - return "You have already declined notifications once. You can enable them in your settings." - case "permission_disabled": - return "Notifications are disabled for World App. Please enable them in your settings." - case "already_granted": - return "You have already granted notification permissions to this mini app." - case "unsupported_permission": - return "This permission is not supported yet. Please try again later." - default: - return null - } - } + const getErrorMessage = () => { + switch (errorCode) { + case "user_rejected": + return "You have declined the permission request. You can change this in your settings."; + case "generic_error": + return "Something went wrong. Please try again later."; + case "already_requested": + return "You have already declined notifications once. You can enable them in your settings."; + case "permission_disabled": + return "Notifications are disabled for World App. Please enable them in your settings."; + case "already_granted": + return "You have already granted notification permissions to this mini app."; + case "unsupported_permission": + return "This permission is not supported yet. Please try again later."; + default: + return null; + } + }; - const handleProceed = () => { - setIsClosing(true) - setTimeout(() => { - onProceed?.() - }, 150) - } + const handleProceed = () => { + setIsClosing(true); + void setTimeout(() => { + onProceed?.(); + }, 150); + }; - const handleDecline = () => { - setIsClosing(true) - setTimeout(() => { - onDecline?.() - }, 150) - } + const handleDecline = () => { + setIsClosing(true); + void setTimeout(() => { + onDecline?.(); + }, 150); + }; - const errorMessage = getErrorMessage() + const errorMessage = getErrorMessage(); - return ( -
-
- {errorMessage ? ( - <> -

- Notification Error -

-

{errorMessage}

- - - ) : ( - <> -

Before you continue:

-

- Would you like to receive notifications from the MiniApp? -

-

- By clicking "Yes, Proceed" you agree to receive notifications about new test releases, reminders to check - resources, and other important updates. -

-
- {["Yes, Proceed", "No, Maybe Later"].map((text, index) => ( - - ))} -
- - )} -
-
- ) + return ( +
+
+ {errorMessage ? ( + <> +

+ Notification Error +

+

+ {errorMessage} +

+ + + ) : ( + <> +

+ Before you continue: +

+

+ Would you like to receive notifications from the MiniApp? +

+

+ By clicking "Yes, Proceed" you agree to receive + notifications about new test releases, reminders to check + resources, and other important updates. +

+
+ {["Yes, Proceed", "No, Maybe Later"].map((text) => ( + + ))} +
+ + )} +
+
+ ); } - diff --git a/frontend/src/components/ui/NotificationsToggle.tsx b/frontend/src/components/ui/NotificationsToggle.tsx index 2700c84..0e8123d 100644 --- a/frontend/src/components/ui/NotificationsToggle.tsx +++ b/frontend/src/components/ui/NotificationsToggle.tsx @@ -1,27 +1,28 @@ -"use client" +"use client"; -import { useNotifications } from "@/providers/NotificationsProvider" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; +import { useNotifications } from "@/providers/NotificationsProvider"; export function NotificationsToggle() { - const { notificationsEnabled, toggleNotifications } = useNotifications() + const { notificationsEnabled, toggleNotifications } = useNotifications(); return ( - ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/components/ui/OutlinedButton.tsx b/frontend/src/components/ui/OutlinedButton.tsx index 81ffd54..9be779d 100644 --- a/frontend/src/components/ui/OutlinedButton.tsx +++ b/frontend/src/components/ui/OutlinedButton.tsx @@ -1,51 +1,58 @@ -import * as React from "react" -import { cn } from "@/lib/utils" -import { LucideIcon } from "lucide-react" -import { ButtonSize, ButtonVariant } from "./styles/buttonStyles" -import { outlinedButtonSizes, outlinedButtonVariants } from "./styles/outlinedButtonStyles" +"use client"; -interface OutlinedButtonProps extends React.ButtonHTMLAttributes { - size?: ButtonSize - variant?: ButtonVariant - fullWidth?: boolean - icon?: LucideIcon - iconClassName?: string - className?: string - children: React.ReactNode +import { cn } from "@/lib/utils"; +import type { LucideIcon } from "lucide-react"; +import type * as React from "react"; +import type { ButtonSize, ButtonVariant } from "./styles/buttonStyles"; +import { + outlinedButtonSizes, + outlinedButtonVariants, +} from "./styles/outlinedButtonStyles"; + +interface OutlinedButtonProps + extends React.ButtonHTMLAttributes { + size?: ButtonSize; + variant?: ButtonVariant; + fullWidth?: boolean; + icon?: LucideIcon; + iconClassName?: string; + className?: string; + children: React.ReactNode; } export function OutlinedButton({ - size = 'md', - variant = 'default', - fullWidth = false, - icon: Icon, - iconClassName, - className, - children, - ...props + size = "md", + variant = "default", + fullWidth = false, + icon: Icon, + iconClassName, + className, + children, + ...props }: OutlinedButtonProps) { - return ( - - ) -} \ No newline at end of file + return ( + + ); +} diff --git a/frontend/src/components/ui/ProfileCard.tsx b/frontend/src/components/ui/ProfileCard.tsx index 1b00941..b898fd3 100644 --- a/frontend/src/components/ui/ProfileCard.tsx +++ b/frontend/src/components/ui/ProfileCard.tsx @@ -1,114 +1,135 @@ -"use client" +"use client"; -import { useState, useEffect, useMemo } from "react" -import { cn } from "@/lib/utils" -import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { motivationalQuotes } from "@/data/motivationalQuotes" +import type * as React from "react"; +import { useEffect, useMemo, useState } from "react"; + +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { motivationalQuotes } from "@/data/motivationalQuotes"; +import { cn } from "@/lib/utils"; interface User { - name: string - last_name: string - level: string - level_points: number - maxPoints: number + name: string; + last_name: string; + level: string; + level_points: number; + maxPoints: number; } interface ProfileCardProps { - className?: string - user?: User + className?: string; + user?: User; +} + +interface ProfileAvatarProps { + name: string; + lastName: string; +} + +interface LevelProgressProps { + points: number; + maxPoints: number; } -const ProfileAvatar = ({ name, lastName }: { name: string; lastName: string }) => { - const initials = useMemo(() => { - return `${name[0]}${lastName[0]}`.toUpperCase() - }, [name, lastName]) +function ProfileAvatar({ name, lastName }: ProfileAvatarProps) { + const initials = useMemo(() => { + return `${name[0]}${lastName[0]}`.toUpperCase(); + }, [name, lastName]); - return ( - - {initials} - - ) + return ( + + + {initials} + + + ); } -const LevelProgress = ({ points, maxPoints }: { points: number; maxPoints: number }) => { - const progress = (points / maxPoints) * 100 - - return ( -
-
-
-
-

- {points}/{maxPoints} points to level up -

-
- ) +function LevelProgress({ points, maxPoints }: LevelProgressProps) { + const progress = (points / maxPoints) * 100; + + return ( +
+
+
+
+

+ {points}/{maxPoints} points to level up +

+
+ ); } export function ProfileCard({ - className, - user = { - name: "John", - last_name: "Doe", - level: "Conscious Explorer", - level_points: 45, - maxPoints: 100, - }, + className, + user = { + name: "John", + last_name: "Doe", + level: "Conscious Explorer", + level_points: 45, + maxPoints: 100, + }, }: ProfileCardProps) { - const [quote, setQuote] = useState(motivationalQuotes[0]) - - useEffect(() => { - setQuote(motivationalQuotes[Math.floor(Math.random() * motivationalQuotes.length)]) - - const intervalId = setInterval(() => { - setQuote((prevQuote) => { - let newQuote - do { - newQuote = motivationalQuotes[Math.floor(Math.random() * motivationalQuotes.length)] - } while (newQuote === prevQuote) - return newQuote - }) - }, 10000) - - return () => clearInterval(intervalId) - }, []) - - return ( -
-
-
- - -
-

- {user.name} {user.last_name} -

-

Level: {user.level}

-
- - - -
-

Your daily motivation:

-

{quote}

-
-
-
- ) -} \ No newline at end of file + const [quote, setQuote] = useState(motivationalQuotes[0]); + + useEffect(() => { + const getRandomQuote = (): string => { + const randomIndex = Math.floor(Math.random() * motivationalQuotes.length); + return motivationalQuotes[randomIndex]; + }; + + setQuote(getRandomQuote()); + + const intervalId = setInterval(() => { + setQuote((prevQuote) => { + let newQuote: string; + do { + newQuote = getRandomQuote(); + } while (newQuote === prevQuote); + return newQuote; + }); + }, 10000); + + return () => clearInterval(intervalId); + }, []); + + return ( +
+
+
+ + +
+

+ {user.name} {user.last_name} +

+

+ Level: {user.level} +

+
+ + + +
+

+ Your daily motivation: +

+

+ {quote} +

+
+
+
+ ); +} diff --git a/frontend/src/components/ui/ProgressBar.tsx b/frontend/src/components/ui/ProgressBar.tsx index 29a95c7..69c315b 100644 --- a/frontend/src/components/ui/ProgressBar.tsx +++ b/frontend/src/components/ui/ProgressBar.tsx @@ -1,112 +1,119 @@ -'use client' +"use client"; -import { useMemo } from 'react' -import type * as React from 'react' -import { cn } from "@/lib/utils" -import { motion } from 'framer-motion' +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; +import type * as React from "react"; +import { useMemo } from "react"; interface ProgressBarProps { - progress?: number - className?: string - variant?: 'default' | 'success' | 'warning' + progress?: number; + className?: string; + variant?: "default" | "success" | "warning"; } export function ProgressBar({ - progress = 0, - className = "", - variant = 'default', + progress = 0, + className = "", + variant = "default", }: ProgressBarProps) { - const progressBarColors = { - default: 'bg-accent-red', - success: 'bg-brand-primary', - warning: 'bg-accent-orange' - } + const progressBarColors = { + default: "bg-accent-red", + success: "bg-brand-primary", + warning: "bg-accent-orange", + }; - // Calculate diamond positions based on total questions - const diamondPositions = useMemo(() => { - const positions = [] + // Calculate diamond positions based on total questions + const diamondPositions = useMemo(() => { + const positions = [ + { id: "first-third", pixelPosition: 56 }, + { id: "second-third", pixelPosition: 168 }, + { id: "final-third", pixelPosition: 281 }, + ]; - // Progress bar width is 337px, so calculate exact positions - // Each third is approximately 112.33px - // Center of each third: 56.16px, 168.5px, 280.83px - positions.push({ - pixelPosition: 56 - }) - positions.push({ - pixelPosition: 168 - }) - positions.push({ - pixelPosition: 281 - }) + return positions; + }, []); - return positions - }, []) + return ( +
+
- return ( -
-
- - - - {diamondPositions.map(({ pixelPosition }, index) => { - // Calculate if the progress bar has reached this diamond's center - const progressInPixels = (progress / 100) * 337 - const isActive = progressInPixels >= pixelPosition - const shouldAnimate = isActive && !sessionStorage.getItem(`diamond-${index}-animated`) + - if (shouldAnimate) { - sessionStorage.setItem(`diamond-${index}-animated`, 'true') - } + {diamondPositions.map(({ id, pixelPosition }) => { + // Calculate if the progress bar has reached this diamond's center + const progressInPixels = (progress / 100) * 337; + const isActive = progressInPixels >= pixelPosition; + const shouldAnimate = + isActive && !sessionStorage.getItem(`diamond-${id}-animated`); - return ( - - - - - - - ) - })} -
- ) + if (shouldAnimate) { + sessionStorage.setItem(`diamond-${id}-animated`, "true"); + } + + return ( + + + + ); + })} +
+ ); } diff --git a/frontend/src/components/ui/QuizCard.tsx b/frontend/src/components/ui/QuizCard.tsx index e344221..21932ea 100644 --- a/frontend/src/components/ui/QuizCard.tsx +++ b/frontend/src/components/ui/QuizCard.tsx @@ -1,43 +1,48 @@ -import { ClockIcon } from 'lucide-react' -import { cn } from '@/lib/utils' -import { FilledButton } from '@/components/ui/FilledButton' -import { ArrowRight } from 'lucide-react' +"use client"; -export default function QuizCard() { - return ( -
-
-
- Keep your streak alive!
Complete a quiz today -
-
-
-
-
- DAILY QUESTIONNAIRE -
- -
-
- - Start Quiz - -
-
-
- ) -} +import { FilledButton } from "@/components/ui/FilledButton"; +import { cn } from "@/lib/utils"; +import { ArrowRight, ClockIcon } from "lucide-react"; +import type * as React from "react"; +export function QuizCard() { + return ( +
+
+
+ Keep your streak alive!
+ Complete a quiz today +
+
+
+
+
+ DAILY QUESTIONNAIRE +
+
+
+ + Start Quiz + +
+
+
+ ); +} diff --git a/frontend/src/components/ui/ResultCard.tsx b/frontend/src/components/ui/ResultCard.tsx index a3c4878..1356fde 100644 --- a/frontend/src/components/ui/ResultCard.tsx +++ b/frontend/src/components/ui/ResultCard.tsx @@ -1,23 +1,32 @@ -'use client' +"use client"; -import { useState } from 'react' -import IdeologyTag from './InsightResultTag' +import { cn } from "@/lib/utils"; +import type * as React from "react"; +import { useState } from "react"; +import { IdeologyTag } from "./InsightResultTag"; interface ResultCardProps { - equalityPercentage: number - className?: string + equalityPercentage: number; + className?: string; } -export default function ResultCard({ equalityPercentage, className = '' }: ResultCardProps) { - const [showDetails, setShowDetails] = useState(false) +export function ResultCard({ + equalityPercentage, + className = "", +}: ResultCardProps) { + const [showDetails, setShowDetails] = useState(false); return ( -
setShowDetails(!showDetails)} >
-

+

Your Economic Perspective

@@ -28,19 +37,22 @@ export default function ResultCard({ equalityPercentage, className = '' }: Resul
- {showDetails ? 'Click to hide details' : 'Click here to see more details'} + {showDetails + ? "Click to hide details" + : "Click here to see more details"}
- + {showDetails && ( -
+

- Your economic perspective leans towards a balance between equality and market forces. - This suggests a preference for policies that combine elements of both social welfare and free-market principles. + Your economic perspective leans towards a balance between equality + and market forces. This suggests a preference for policies that + combine elements of both social welfare and free-market principles.

)} -
- ) + + ); } diff --git a/frontend/src/components/ui/SearchBar.tsx b/frontend/src/components/ui/SearchBar.tsx index d3f17ca..300e96a 100644 --- a/frontend/src/components/ui/SearchBar.tsx +++ b/frontend/src/components/ui/SearchBar.tsx @@ -1,78 +1,84 @@ -"use client" +"use client"; -import { useState, ChangeEvent, InputHTMLAttributes, forwardRef } from 'react' -import { Search, X } from 'lucide-react' +import { Search, X } from "lucide-react"; +import type * as React from "react"; +import { forwardRef, useState } from "react"; +import type { ChangeEvent, InputHTMLAttributes } from "react"; -// Utility function for class names -const cn = (...classes: (string | boolean | undefined)[]) => classes.filter(Boolean).join(' ') +import { cn } from "@/lib/utils"; -// Input component -const Input = forwardRef>( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" +interface InputProps extends InputHTMLAttributes { + className?: string; +} + +const Input = forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; interface SearchBarProps { - onSearch: (query: string) => void - className?: string - placeholder?: string + onSearch: (query: string) => void; + className?: string; + placeholder?: string; } -export default function SearchBar({ - onSearch, - className, - placeholder = "Search for tests" +export function SearchBar({ + onSearch, + className, + placeholder = "Search for tests", }: SearchBarProps) { - const [searchQuery, setSearchQuery] = useState('') + const [searchQuery, setSearchQuery] = useState(""); - const handleInputChange = (e: ChangeEvent) => { - setSearchQuery(e.target.value) - } + const handleInputChange = (e: ChangeEvent) => { + setSearchQuery(e.target.value); + }; - const handleSearch = () => { - onSearch(searchQuery) - } + const handleSearch = () => { + onSearch(searchQuery); + }; - const handleClear = () => { - setSearchQuery('') - } + const handleClear = () => { + setSearchQuery(""); + onSearch(""); + }; - return ( -
- -
- - -
-
- ) + return ( +
+ +
+ + +
+
+ ); } - diff --git a/frontend/src/components/ui/SettingsCard.tsx b/frontend/src/components/ui/SettingsCard.tsx index c61141e..0b1f1a5 100644 --- a/frontend/src/components/ui/SettingsCard.tsx +++ b/frontend/src/components/ui/SettingsCard.tsx @@ -1,30 +1,39 @@ -import { LucideIcon } from "lucide-react" -import { cn } from "@/lib/utils" +"use client"; + +import { cn } from "@/lib/utils"; +import type { LucideIcon } from "lucide-react"; +import type * as React from "react"; interface SettingsCardProps { - icon: LucideIcon - label: string - rightElement?: React.ReactNode - onClick?: () => void + icon: LucideIcon; + label: string; + rightElement?: React.ReactNode; + onClick?: () => void; } -export function SettingsCard({ icon: Icon, label, rightElement, onClick }: SettingsCardProps) { +export function SettingsCard({ + icon: Icon, + label, + rightElement, + onClick, +}: SettingsCardProps) { return ( -
- - +
{rightElement} -
- ) -} \ No newline at end of file + + ); +} diff --git a/frontend/src/components/ui/TestCard.tsx b/frontend/src/components/ui/TestCard.tsx index 87a917a..cccdef6 100644 --- a/frontend/src/components/ui/TestCard.tsx +++ b/frontend/src/components/ui/TestCard.tsx @@ -1,84 +1,104 @@ -'use client' +"use client"; + +import type * as React from "react"; interface Achievement { - id: string - title: string - description: string + id: string; + title: string; + description: string; } interface TestCardProps { - totalQuestions: number - answeredQuestions: number - achievements: Achievement[] - onCardClick: () => void - title: string + totalQuestions: number; + answeredQuestions: number; + achievements: Achievement[]; + onCardClick: () => void; + title: string; } export function TestCard({ - totalQuestions, - answeredQuestions, - achievements, - onCardClick, - title, + totalQuestions, + answeredQuestions, + achievements, + onCardClick, + title, }: TestCardProps) { - const progress = Math.round((answeredQuestions / totalQuestions) * 100) + const progress = Math.round((answeredQuestions / totalQuestions) * 100); - return ( -
-
-
-
-

{title}

-
-
-
- Progress - {progress}% -
-
-
-
-
-
-
-
-
- - - - - - - - - Achievements -
-
- {achievements.map((achievement) => ( -
- {achievement.title.charAt(0)} - {achievement.title} -
- ))} -
-
-
-
-
-
- ) -} \ No newline at end of file + return ( + + ); +} diff --git a/frontend/src/components/ui/ToggleSwitch.tsx b/frontend/src/components/ui/ToggleSwitch.tsx index 25452f2..5bd471f 100644 --- a/frontend/src/components/ui/ToggleSwitch.tsx +++ b/frontend/src/components/ui/ToggleSwitch.tsx @@ -1,28 +1,31 @@ -"use client" +"use client"; -import { useTheme } from "@/providers/ThemeProvider" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; +import { useTheme } from "@/providers/ThemeProvider"; +import type * as React from "react"; export function ToggleSwitch() { - const { theme, toggleTheme } = useTheme() - const checked = theme === "dark" + const { theme, toggleTheme } = useTheme(); + const checked = theme === "dark"; return ( - ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/components/ui/VerifyModal.tsx b/frontend/src/components/ui/VerifyModal.tsx index b6872f3..8c1dc2c 100644 --- a/frontend/src/components/ui/VerifyModal.tsx +++ b/frontend/src/components/ui/VerifyModal.tsx @@ -1,9 +1,10 @@ -'use client' +"use client"; -import { Dialog, DialogContent } from "@/components/ui/dialog" -import { FilledButton } from "@/components/ui/FilledButton" -import Image from "next/image" -import { useVerification } from "@/hooks/useVerification" +import { FilledButton } from "@/components/ui/FilledButton"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { useVerification } from "@/hooks/useVerification"; +import Image from "next/image"; +import type * as React from "react"; interface VerifyModalProps { isOpen: boolean; @@ -12,12 +13,12 @@ interface VerifyModalProps { } export function VerifyModal({ isOpen, onClose, onVerify }: VerifyModalProps) { - const { isVerifying, error } = useVerification() + const { isVerifying, error } = useVerification(); return ( - e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} > @@ -29,28 +30,28 @@ export function VerifyModal({ isOpen, onClose, onVerify }: VerifyModalProps) { height={110} className="mb-4" /> - -

+ +

Not Verified yet?

- -

+ +

Find your closest Orb and completely verify your World ID! -

- By verifying you will have access to more features on the app and no ads. +
+
+ By verifying you will have access to more features on the app and no + ads.

{error && ( -

- {error} -

+

{error}

)} -
+
Maybe Later @@ -59,15 +60,15 @@ export function VerifyModal({ isOpen, onClose, onVerify }: VerifyModalProps) { - {isVerifying ? 'Verifying...' : 'Verify!'} + {isVerifying ? "Verifying..." : "Verify!"}
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/components/ui/WorldIDButton.tsx b/frontend/src/components/ui/WorldIDButton.tsx index a20bd08..a931759 100644 --- a/frontend/src/components/ui/WorldIDButton.tsx +++ b/frontend/src/components/ui/WorldIDButton.tsx @@ -1,53 +1,61 @@ -'use client' +"use client"; -import { FilledButton } from "./FilledButton" -import Image from "next/image" -import { useState } from "react" +import { cn } from "@/lib/utils"; +import Image from "next/image"; +import type * as React from "react"; +import { useState } from "react"; +import { FilledButton } from "./FilledButton"; interface WorldIDButtonProps { - onClick: () => Promise - className?: string + onClick: () => Promise; + className?: string; } export function WorldIDButton({ onClick, className }: WorldIDButtonProps) { - const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false); - const handleClick = async () => { - setIsLoading(true) - try { - await onClick() - } finally { - setIsLoading(false) - } - } + const handleClick = async () => { + setIsLoading(true); + try { + await onClick(); + } finally { + setIsLoading(false); + } + }; - return ( - -
-
- World ID Logo - {isLoading && ( -
-
-
- )} -
- - {isLoading ? 'Connecting...' : 'Continue with World ID'} - -
- - ) -} \ No newline at end of file + return ( + +
+
+ World ID Logo + {isLoading && ( +
+
+
+ )} +
+ + {isLoading ? "Connecting..." : "Continue with World ID"} + +
+ + ); +} diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx index 01b8b6d..59528bc 100644 --- a/frontend/src/components/ui/skeleton.tsx +++ b/frontend/src/components/ui/skeleton.tsx @@ -1,4 +1,5 @@ -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; +import type * as React from "react"; function Skeleton({ className, @@ -9,7 +10,7 @@ function Skeleton({ className={cn("animate-pulse rounded-md bg-muted", className)} {...props} /> - ) + ); } -export { Skeleton } +export { Skeleton }; diff --git a/frontend/src/components/ui/styles/outlinedButtonStyles.ts b/frontend/src/components/ui/styles/outlinedButtonStyles.ts index 3409d6d..ec149bb 100644 --- a/frontend/src/components/ui/styles/outlinedButtonStyles.ts +++ b/frontend/src/components/ui/styles/outlinedButtonStyles.ts @@ -1,16 +1,20 @@ -import { ButtonSize, ButtonVariant } from './buttonStyles' +import type { ButtonSize, ButtonVariant } from "./buttonStyles"; export const outlinedButtonSizes = { - sm: 'h-8 px-4 text-sm', - md: 'h-10 px-6 text-base', - lg: 'h-12 px-8 text-lg' -} as const + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-base", + lg: "px-5 py-2.5 text-lg", +}; export const outlinedButtonVariants = { - default: 'border-2 border-accent-red text-accent-red hover:bg-accent-red/10', - secondary: 'border-2 border-brand-secondary text-brand-secondary hover:bg-brand-secondary/10', - warning: 'border-2 border-accent-orange text-accent-orange hover:bg-accent-orange/10' -} as const + default: "border border-white text-white hover:bg-white/10", + primary: + "border border-brand-primary text-brand-primary hover:bg-brand-primary/10", + secondary: + "border border-brand-secondary text-brand-secondary hover:bg-brand-secondary/10", + accent: "border border-accent-red text-accent-red hover:bg-accent-red/10", + warning: "border border-yellow-500 text-yellow-500 hover:bg-yellow-500/10" +}; -export type OutlinedButtonSize = ButtonSize -export type OutlinedButtonVariant = ButtonVariant \ No newline at end of file +export type OutlinedButtonSize = ButtonSize; +export type OutlinedButtonVariant = ButtonVariant; diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 7f264c4..c83a0ea 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -1,5 +1,5 @@ -import { useEffect, useState, useCallback } from 'react'; -import { useRouter, usePathname } from 'next/navigation'; +import { usePathname, useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; interface AuthState { isAuthenticated: boolean; @@ -15,33 +15,33 @@ export function useAuth() { isRegistered: false, needsRegistration: false, walletAddress: null, - loading: true + loading: true, }); const router = useRouter(); const pathname = usePathname(); // Skip auth checks on auth-related pages - const shouldSkipAuthCheck = pathname === '/register' || - pathname === '/sign-in' || - pathname === '/welcome'; + const shouldSkipAuthCheck = + pathname === "/register" || + pathname === "/sign-in" || + pathname === "/welcome"; const checkAuth = useCallback(async () => { // Skip auth check on auth-related pages if (shouldSkipAuthCheck) { - setAuthState(prev => ({ ...prev, loading: false })); + setAuthState((prev) => ({ ...prev, loading: false })); return; } - console.log('Checking auth state...'); try { // Check session first - const sessionResponse = await fetch('/api/auth/session', { - method: 'GET', - credentials: 'include', + const sessionResponse = await fetch("/api/auth/session", { + method: "GET", + credentials: "include", headers: { - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache' - } + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, }); if (!sessionResponse.ok) { @@ -51,48 +51,46 @@ export function useAuth() { isRegistered: false, needsRegistration: false, walletAddress: null, - loading: false + loading: false, }); - if (pathname !== '/sign-in') { - router.replace('/sign-in'); + if (pathname !== "/sign-in") { + router.replace("/sign-in"); } return; } - throw new Error('Session check failed'); + throw new Error("Session check failed"); } const sessionData = await sessionResponse.json(); - console.log('Session check response:', sessionData); setAuthState({ isAuthenticated: sessionData.isAuthenticated, isRegistered: sessionData.isRegistered, needsRegistration: sessionData.needsRegistration, walletAddress: sessionData.address, - loading: false + loading: false, }); // Handle redirects based on auth state - if (!sessionData.isAuthenticated && pathname !== '/sign-in') { - router.replace('/sign-in'); - } else if (sessionData.needsRegistration && pathname !== '/register') { - router.replace('/register'); + if (!sessionData.isAuthenticated && pathname !== "/sign-in") { + router.replace("/sign-in"); + } else if (sessionData.needsRegistration && pathname !== "/register") { + router.replace("/register"); } } catch (error) { - console.error('Auth check failed:', error); - if (error instanceof DOMException && error.name === 'SyntaxError') { - console.log('Session error detected'); - if (pathname !== '/sign-in' && pathname !== '/welcome') { - router.replace('/sign-in'); + console.error("Auth check failed:", error); + if (error instanceof DOMException && error.name === "SyntaxError") { + if (pathname !== "/sign-in" && pathname !== "/welcome") { + router.replace("/sign-in"); } } - + setAuthState({ isAuthenticated: false, isRegistered: false, needsRegistration: false, walletAddress: null, - loading: false + loading: false, }); } }, [pathname, router, shouldSkipAuthCheck]); @@ -102,7 +100,7 @@ export function useAuth() { if (!shouldSkipAuthCheck) { checkAuth(); } else { - setAuthState(prev => ({ ...prev, loading: false })); + setAuthState((prev) => ({ ...prev, loading: false })); } }, [checkAuth, shouldSkipAuthCheck]); @@ -111,12 +109,11 @@ export function useAuth() { if (shouldSkipAuthCheck) return; const handleFocus = () => { - console.log('Window focused, rechecking auth...'); checkAuth(); }; - window.addEventListener('focus', handleFocus); - return () => window.removeEventListener('focus', handleFocus); + window.addEventListener("focus", handleFocus); + return () => window.removeEventListener("focus", handleFocus); }, [checkAuth, shouldSkipAuthCheck]); // Recheck auth periodically (only on protected pages) @@ -124,7 +121,6 @@ export function useAuth() { if (shouldSkipAuthCheck) return; const interval = setInterval(() => { - console.log('Periodic auth check...'); checkAuth(); }, 60000); // Check every minute @@ -133,6 +129,6 @@ export function useAuth() { return { ...authState, - refreshAuth: checkAuth + refreshAuth: checkAuth, }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useVerification.ts b/frontend/src/hooks/useVerification.ts index 6d98369..1138e4d 100644 --- a/frontend/src/hooks/useVerification.ts +++ b/frontend/src/hooks/useVerification.ts @@ -1,171 +1,173 @@ -'use client' +"use client"; -import { useState, useEffect, useCallback } from 'react' -import { MiniKit, VerifyCommandInput, VerificationLevel, ISuccessResult } from '@worldcoin/minikit-js' -import { useRouter } from 'next/navigation' +import type { ISuccessResult, VerifyCommandInput } from "@worldcoin/minikit-js"; +import { MiniKit, VerificationLevel } from "@worldcoin/minikit-js"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; // Utility function to clear verification session data -export const clearVerificationSession = () => { - if (typeof window !== 'undefined') { - sessionStorage.removeItem('verify-modal-shown') +export function clearVerificationSession() { + if (typeof window !== "undefined") { + sessionStorage.removeItem("verify-modal-shown"); } } export function useVerification() { - const [isVerified, setIsVerified] = useState(false) - const [isVerifying, setIsVerifying] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - const [hasCheckedInitial, setHasCheckedInitial] = useState(false) - const router = useRouter() + const [isVerified, setIsVerified] = useState(false); + const [isVerifying, setIsVerifying] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [hasCheckedInitial, setHasCheckedInitial] = useState(false); + const router = useRouter(); const clearVerificationSession = useCallback(() => { - setIsVerified(false) - setIsVerifying(false) - setError(null) + setIsVerified(false); + setIsVerifying(false); + setError(null); // Clear session cookies - document.cookie = 'session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' - document.cookie = 'worldcoin_verified=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' - document.cookie = 'siwe_verified=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' - document.cookie = 'registration_status=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' - }, []) + document.cookie = + "session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"; + document.cookie = + "worldcoin_verified=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"; + document.cookie = + "siwe_verified=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"; + document.cookie = + "registration_status=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"; + }, []); const checkVerificationStatus = useCallback(async () => { - console.log('Checking verification status...') - setError(null) + setError(null); try { - const response = await fetch('/api/auth/session', { - method: 'GET', - credentials: 'include', + const response = await fetch("/api/auth/session", { + method: "GET", + credentials: "include", headers: { - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache' - } - }) + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, + }); if (!response.ok) { if (response.status === 401) { - clearVerificationSession() - router.push('/sign-in') - return + clearVerificationSession(); + router.push("/sign-in"); + return; } - throw new Error('Failed to check verification status') + throw new Error("Failed to check verification status"); } - const data = await response.json() - console.log('Verification status:', data) + const data = await response.json(); + setIsVerified(data.isVerified); - setIsVerified(data.isVerified) - // Handle registration and authentication states if (!data.isAuthenticated) { - clearVerificationSession() - router.push('/sign-in') - return + clearVerificationSession(); + router.push("/sign-in"); + return; } if (!data.isRegistered) { - router.push('/register') - return + router.push("/register"); + return; } - } catch (error) { - console.error('Error in checkVerificationStatus:', error) + console.error("Error in checkVerificationStatus:", error); if (error instanceof DOMException) { - console.log('DOMException caught, clearing session...') - clearVerificationSession() - router.push('/sign-in') - return + clearVerificationSession(); + router.push("/sign-in"); + return; } - setError(error instanceof Error ? error.message : 'Failed to check verification status') + setError( + error instanceof Error + ? error.message + : "Failed to check verification status", + ); } finally { - setIsLoading(false) - setHasCheckedInitial(true) + setIsLoading(false); + setHasCheckedInitial(true); } - }, [router, clearVerificationSession]) + }, [router, clearVerificationSession]); // Initial check on mount useEffect(() => { - if (typeof window !== 'undefined') { - console.log('Initial verification check') - checkVerificationStatus() + if (typeof window !== "undefined") { + checkVerificationStatus(); } - }, [checkVerificationStatus]) + }, [checkVerificationStatus]); const handleVerify = async () => { - console.log('Starting verification process...') - setError(null) + setError(null); if (!MiniKit.isInstalled()) { - setError('World App is not installed') - window.open('https://worldcoin.org/download-app', '_blank') - return false + setError("World App is not installed"); + window.open("https://worldcoin.org/download-app", "_blank"); + return false; } - setIsVerifying(true) + setIsVerifying(true); try { const verifyPayload: VerifyCommandInput = { - action: 'verify-user', + action: "verify-user", verification_level: VerificationLevel.Orb, - } + }; - console.log('Requesting World ID verification...') - const { finalPayload } = await MiniKit.commandsAsync.verify(verifyPayload) - .catch(error => { + const { finalPayload } = await MiniKit.commandsAsync + .verify(verifyPayload) + .catch((error) => { if (error instanceof DOMException) { - console.log('World ID verification cancelled by user') - throw new Error('Verification cancelled') + throw new Error("Verification cancelled"); } - throw error - }) + throw error; + }); - if (finalPayload.status === 'error') { - console.error('World ID verification failed:', finalPayload) - setError('Verification failed') - return false + if (finalPayload.status === "error") { + console.error("World ID verification failed:", finalPayload); + setError("Verification failed"); + return false; } - console.log('Verifying proof with backend...') - const verifyResponse = await fetch('/api/verify', { - method: 'POST', + const verifyResponse = await fetch("/api/verify", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - credentials: 'include', + credentials: "include", body: JSON.stringify({ payload: finalPayload as ISuccessResult, - action: 'verify-user' + action: "verify-user", }), - }) + }); + + const verifyResponseJson = await verifyResponse.json(); - const verifyResponseJson = await verifyResponse.json() - console.log('Backend verification response:', verifyResponseJson) - if (verifyResponse.ok) { - setIsVerified(true) - setError(null) - await checkVerificationStatus() - return true + setIsVerified(true); + setError(null); + await checkVerificationStatus(); + return true; } if (verifyResponse.status === 401) { - clearVerificationSession() - router.push('/sign-in') + clearVerificationSession(); + router.push("/sign-in"); } - - setError(verifyResponseJson.error || 'Verification failed') - return false + + setError(verifyResponseJson.error || "Verification failed"); + return false; } catch (error) { - console.error('Verification error:', error) - if (error instanceof Error && error.message === 'Verification cancelled') { - setError('Verification was cancelled') + console.error("Verification error:", error); + if ( + error instanceof Error && + error.message === "Verification cancelled" + ) { + setError("Verification was cancelled"); } else { - setError('Verification failed') + setError("Verification failed"); } - return false + return false; } finally { - setIsVerifying(false) + setIsVerifying(false); } - } + }; return { isVerified, @@ -175,6 +177,6 @@ export function useVerification() { hasCheckedInitial, handleVerify, checkVerificationStatus, - refreshVerification: checkVerificationStatus - } -} \ No newline at end of file + refreshVerification: checkVerificationStatus, + }; +} diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 314bfab..21139fc 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -1,44 +1,46 @@ -import { NextRequest } from 'next/server'; -import { getXataClient } from './utils'; +import type { NextRequest } from "next/server"; +import { getXataClient } from "./utils"; export interface AuthUser { - id: string; - name: string; - email: string; - walletAddress: string; - subscription: boolean; - verified: boolean; + id: string; + name: string; + email: string; + walletAddress: string; + subscription: boolean; + verified: boolean; } -export async function getCurrentUser(req: NextRequest): Promise { - try { - const userId = req.headers.get('x-user-id'); - const walletAddress = req.headers.get('x-wallet-address'); +export async function getCurrentUser( + req: NextRequest, +): Promise { + try { + const userId = req.headers.get("x-user-id"); + const walletAddress = req.headers.get("x-wallet-address"); - if (!userId || !walletAddress) { - return null; - } + if (!userId || !walletAddress) { + return null; + } - const xata = getXataClient(); - const user = await xata.db.Users.filter({ - wallet_address: walletAddress, - xata_id: userId - }).getFirst(); + const xata = getXataClient(); + const user = await xata.db.Users.filter({ + wallet_address: walletAddress, + xata_id: userId, + }).getFirst(); - if (!user) { - return null; - } + if (!user) { + return null; + } - return { - id: user.xata_id, - name: user.name, - email: user.email, - walletAddress: user.wallet_address, - subscription: user.subscription, - verified: user.verified - }; - } catch (error) { - console.error('Error getting current user:', error); - return null; - } -} \ No newline at end of file + return { + id: user.xata_id, + name: user.name, + email: user.email, + walletAddress: user.wallet_address, + subscription: user.subscription, + verified: user.verified, + }; + } catch (error) { + console.error("Error getting current user:", error); + return null; + } +} diff --git a/frontend/src/lib/crypto.ts b/frontend/src/lib/crypto.ts index 7990953..f34da29 100644 --- a/frontend/src/lib/crypto.ts +++ b/frontend/src/lib/crypto.ts @@ -1,11 +1,11 @@ /** * Creates a SHA-256 hash of the provided text - * @param text The text to hash + * @param text - The text to hash * @returns A hex string representation of the hash */ export async function createHash(text: string): Promise { - const msgBuffer = new TextEncoder().encode(text); - const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); -} \ No newline at end of file + const msgBuffer = new TextEncoder().encode(text); + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); +} diff --git a/frontend/src/lib/swagger.ts b/frontend/src/lib/swagger.ts index d1bb035..c85bef6 100644 --- a/frontend/src/lib/swagger.ts +++ b/frontend/src/lib/swagger.ts @@ -1,26 +1,17 @@ -import { createSwaggerSpec } from 'next-swagger-doc'; +import { createSwaggerSpec } from "next-swagger-doc"; -export const getApiDocs = () => { - const spec = createSwaggerSpec({ - apiFolder: 'src/app/api', // Path to API folder - definition: { - openapi: '3.0.0', - info: { - title: 'MindVault API Documentation', - version: '1.0.0', - description: 'API documentation for MindVault application', - }, - // components: { - // securitySchemes: { - // BearerAuth: { - // type: 'http', - // scheme: 'bearer', - // bearerFormat: 'JWT', - // }, - // }, - // }, - security: [], - }, - }); - return spec; -}; \ No newline at end of file +export function getApiDocs() { + const spec = createSwaggerSpec({ + apiFolder: "src/app/api", + definition: { + openapi: "3.0.0", + info: { + title: "MindVault API Documentation", + version: "1.0.0", + description: "API documentation for MindVault application", + }, + security: [], + }, + }); + return spec; +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index f73fa87..a99f1af 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,25 +1,29 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" -import { XataClient } from "./xata" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { XataClient } from "./xata"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } let instance: XataClient | undefined = undefined; -export const getXataClient = () => { - if (instance) return instance; +export function getXataClient() { + if (instance) return instance; - if (!process.env.XATA_DATABASE_URL || !process.env.XATA_API_KEY || !process.env.XATA_BRANCH) { - throw new Error('Missing Xata configuration environment variables.'); - } + if ( + !process.env.XATA_DATABASE_URL || + !process.env.XATA_API_KEY || + !process.env.XATA_BRANCH + ) { + throw new Error("Missing Xata configuration environment variables."); + } - instance = new XataClient({ - databaseURL: process.env.XATA_DATABASE_URL, - apiKey: process.env.XATA_API_KEY, - fetch: fetch, - branch: process.env.XATA_BRANCH, - }); - return instance; -}; \ No newline at end of file + instance = new XataClient({ + databaseURL: process.env.XATA_DATABASE_URL, + apiKey: process.env.XATA_API_KEY, + fetch, + branch: process.env.XATA_BRANCH, + }); + return instance; +} diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 9c171a0..ccbda33 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,129 +1,122 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' -import { jwtVerify } from 'jose' +import { jwtVerify } from "jose"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; // List of public paths that don't require authentication const publicPaths = [ - '/sign-in', - '/api/auth/session', - '/api/user', - '/api/user/check', - '/api/nonce', - '/api/complete-siwe', - '/_next', - '/favicon.ico', - '/register' -] + "/sign-in", + "/api/auth/session", + "/api/user", + "/api/user/check", + "/api/nonce", + "/api/complete-siwe", + "/_next", + "/favicon.ico", + "/register", +]; // Get the secret from environment variables const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { - throw new Error('JWT_SECRET environment variable is required'); + throw new Error("JWT_SECRET environment variable is required"); } // Create secret for JWT tokens const secret = new TextEncoder().encode(JWT_SECRET); +interface JWTPayload { + walletAddress?: string; + sub?: string; + exp?: number; +} + // Function to verify JWT token async function verifyToken(token: string) { try { - console.log('Verifying token in middleware...'); - if (!token || typeof token !== 'string') { - console.error('Invalid token format'); + if (!token || typeof token !== "string") { return null; } const verified = await jwtVerify(token, secret, { - algorithms: ['HS256'] + algorithms: ["HS256"], }); // Validate payload structure - const payload = verified.payload; - if (!payload || typeof payload !== 'object') { - console.error('Invalid payload structure'); + const payload = verified.payload as JWTPayload; + if (!payload || typeof payload !== "object") { return null; } // Ensure required fields exist if (!payload.walletAddress || !payload.sub) { - console.error('Missing required fields in payload'); return null; } - console.log('Token verified successfully in middleware:', { - sub: payload.sub, - walletAddress: payload.walletAddress, - exp: payload.exp - }); return payload; - } catch (err) { - console.error('Token verification failed in middleware:', err); + } catch { return null; } } // Middleware function export async function middleware(request: NextRequest) { - const { pathname } = request.nextUrl + const { pathname } = request.nextUrl; // Allow public paths - if (publicPaths.some(path => pathname.startsWith(path))) { - return NextResponse.next() + if (publicPaths.some((path) => pathname.startsWith(path))) { + return NextResponse.next(); } // Get session token and registration status - const sessionToken = request.cookies.get('session')?.value - const registrationStatus = request.cookies.get('registration_status')?.value + const sessionToken = request.cookies.get("session")?.value; + const registrationStatus = request.cookies.get("registration_status")?.value; // For all protected routes if (!sessionToken) { - return NextResponse.redirect(new URL('/sign-in', request.url)) + return NextResponse.redirect(new URL("/sign-in", request.url)); } try { - const decoded = await verifyToken(sessionToken) + const decoded = await verifyToken(sessionToken); if (!decoded) { - const response = NextResponse.redirect(new URL('/sign-in', request.url)) - response.cookies.delete('session') - response.cookies.delete('registration_status') - return response + const response = NextResponse.redirect(new URL("/sign-in", request.url)); + response.cookies.delete("session"); + response.cookies.delete("registration_status"); + return response; } // Handle registration flow - if (pathname !== '/register' && registrationStatus !== 'complete') { - const url = new URL('/register', request.url); + if (pathname !== "/register" && registrationStatus !== "complete") { + const url = new URL("/register", request.url); if (decoded.walletAddress) { - url.searchParams.set('walletAddress', decoded.walletAddress as string); + url.searchParams.set("walletAddress", decoded.walletAddress); } return NextResponse.redirect(url); } // Add user info to request headers for API routes - if (pathname.startsWith('/api/')) { - const requestHeaders = new Headers(request.headers) - requestHeaders.set('x-user-id', decoded.sub as string) - requestHeaders.set('x-wallet-address', decoded.walletAddress as string) + if (pathname.startsWith("/api/")) { + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-user-id", decoded.sub as string); + requestHeaders.set("x-wallet-address", decoded.walletAddress as string); return NextResponse.next({ request: { headers: requestHeaders, }, - }) + }); } - return NextResponse.next() - } catch (error) { - console.error('Middleware error:', error) + return NextResponse.next(); + } catch { // Clear invalid session - const response = NextResponse.redirect(new URL('/sign-in', request.url)) - response.cookies.delete('session') - response.cookies.delete('registration_status') - return response + const response = NextResponse.redirect(new URL("/sign-in", request.url)); + response.cookies.delete("session"); + response.cookies.delete("registration_status"); + return response; } } export const config = { - matcher: [ - '/((?!_next/static|_next/image|favicon.ico).*)', - ], -} \ No newline at end of file + matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], +}; diff --git a/frontend/src/providers/MiniKitProvider.tsx b/frontend/src/providers/MiniKitProvider.tsx index 3d92d77..53ed933 100644 --- a/frontend/src/providers/MiniKitProvider.tsx +++ b/frontend/src/providers/MiniKitProvider.tsx @@ -1,23 +1,25 @@ "use client"; -import { ReactNode, useEffect } from "react"; import { MiniKit } from "@worldcoin/minikit-js"; +import type * as React from "react"; +import { useEffect } from "react"; -export default function MiniKitProvider({ children }: { children: ReactNode }) { +interface MiniKitProviderProps { + children: React.ReactNode; +} + +export function MiniKitProvider({ children }: MiniKitProviderProps) { useEffect(() => { try { if (!process.env.NEXT_PUBLIC_WLD_APP_ID) { - throw new Error('NEXT_PUBLIC_WLD_APP_ID is not defined'); + throw new Error("NEXT_PUBLIC_WLD_APP_ID is not defined"); } MiniKit.install(process.env.NEXT_PUBLIC_WLD_APP_ID); - } catch (error) { - console.error('Failed to initialize MiniKit:', error); + } catch { + // Silently fail if MiniKit initialization fails + // The app will handle missing MiniKit functionality gracefully } }, []); - return ( -
- {children} -
- ); -} \ No newline at end of file + return
{children}
; +} diff --git a/frontend/src/providers/NotificationsProvider.tsx b/frontend/src/providers/NotificationsProvider.tsx index 0bf1c53..2798b21 100644 --- a/frontend/src/providers/NotificationsProvider.tsx +++ b/frontend/src/providers/NotificationsProvider.tsx @@ -1,24 +1,40 @@ -"use client" +"use client"; -import { createContext, useContext, useState } from "react" +import type * as React from "react"; +import { createContext, useContext, useState } from "react"; -const NotificationsContext = createContext({ - notificationsEnabled: true, - toggleNotifications: () => {} -}) +interface NotificationsContextType { + notificationsEnabled: boolean; + toggleNotifications: () => void; +} + +const NotificationsContext = createContext({ + notificationsEnabled: true, + toggleNotifications: () => undefined, +}); -export function NotificationsProvider({ children }: { children: React.ReactNode }) { - const [notificationsEnabled, setNotificationsEnabled] = useState(true) +interface NotificationsProviderProps { + children: React.ReactNode; +} - const toggleNotifications = () => { - setNotificationsEnabled(prev => !prev) - } +export function NotificationsProvider({ + children, +}: NotificationsProviderProps) { + const [notificationsEnabled, setNotificationsEnabled] = useState(true); - return ( - - {children} - - ) + const toggleNotifications = () => { + setNotificationsEnabled((prev) => !prev); + }; + + return ( + + {children} + + ); } -export const useNotifications = () => useContext(NotificationsContext) \ No newline at end of file +export function useNotifications() { + return useContext(NotificationsContext); +} diff --git a/frontend/src/providers/ThemeProvider.tsx b/frontend/src/providers/ThemeProvider.tsx index 426b517..fa61db3 100644 --- a/frontend/src/providers/ThemeProvider.tsx +++ b/frontend/src/providers/ThemeProvider.tsx @@ -1,30 +1,42 @@ -"use client" +"use client"; -import { createContext, useContext, useEffect, useState } from "react" +import type * as React from "react"; +import { createContext, useContext, useEffect, useState } from "react"; -type Theme = "dark" | "light" +type Theme = "dark" | "light"; -const ThemeContext = createContext({ - theme: "dark" as Theme, - toggleTheme: () => {} -}) +interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; +} + +const ThemeContext = createContext({ + theme: "dark", + toggleTheme: () => undefined, +}); -export function ThemeProvider({ children }: { children: React.ReactNode }) { - const [theme, setTheme] = useState("dark") +interface ThemeProviderProps { + children: React.ReactNode; +} - useEffect(() => { - document.documentElement.classList.toggle("dark", theme === "dark") - }, [theme]) +export function ThemeProvider({ children }: ThemeProviderProps) { + const [theme, setTheme] = useState("dark"); - const toggleTheme = () => { - setTheme(prev => prev === "dark" ? "light" : "dark") - } + useEffect(() => { + document.documentElement.classList.toggle("dark", theme === "dark"); + }, [theme]); - return ( - - {children} - - ) + const toggleTheme = () => { + setTheme((prev) => (prev === "dark" ? "light" : "dark")); + }; + + return ( + + {children} + + ); } -export const useTheme = () => useContext(ThemeContext) \ No newline at end of file +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/frontend/src/providers/eruda-provider.tsx b/frontend/src/providers/eruda-provider.tsx index af98c9f..9b5ad7e 100644 --- a/frontend/src/providers/eruda-provider.tsx +++ b/frontend/src/providers/eruda-provider.tsx @@ -1,18 +1,23 @@ "use client"; import eruda from "eruda"; -import { ReactNode, useEffect } from "react"; +import type * as React from "react"; +import { useEffect } from "react"; -export const Eruda = (props: { children: ReactNode }) => { +interface ErudaProps { + children: React.ReactNode; +} + +export function Eruda({ children }: ErudaProps) { useEffect(() => { if (typeof window !== "undefined") { try { eruda.init(); - } catch (error) { - console.log("Eruda failed to initialize", error); + } catch { + // Silently fail if Eruda initialization fails } } }, []); - return <>{props.children}; -}; \ No newline at end of file + return <>{children}; +} diff --git a/frontend/src/providers/index.tsx b/frontend/src/providers/index.tsx index f1f418b..c4cf659 100644 --- a/frontend/src/providers/index.tsx +++ b/frontend/src/providers/index.tsx @@ -1,15 +1,19 @@ "use client"; import dynamic from "next/dynamic"; -import { ReactNode } from "react"; +import type * as React from "react"; + +interface ErudaProviderProps { + children: React.ReactNode; +} const Eruda = dynamic(() => import("./eruda-provider").then((c) => c.Eruda), { ssr: false, }); -export const ErudaProvider = (props: { children: ReactNode }) => { +export function ErudaProvider({ children }: ErudaProviderProps) { if (process.env.NEXT_PUBLIC_APP_ENV === "production") { - return props.children; + return children; } - return {props.children}; -}; \ No newline at end of file + return {children}; +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index b5ed5e7..b1623ac 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,140 +1,133 @@ import type { Config } from "tailwindcss"; -import animate from "tailwindcss-animate" +import animate from "tailwindcss-animate"; const colors = { base: { background: "var(--background)", foreground: "var(--foreground)", - white: '#FFFFFF', + white: "#FFFFFF", }, brand: { - primary: '#4ECCA3', - secondary: '#387478', - tertiary: '#2C5154', + primary: "#4ECCA3", + secondary: "#387478", + tertiary: "#2C5154", }, neutral: { - bg: '#EEEEEE', - black: '#232931', - grey: '#393E46', - white: '#232931' + bg: "#EEEEEE", + black: "#232931", + grey: "#393E46", + white: "#232931", }, accent: { - red: '#E36C59', - orange: '#DA9540', - blue: '#778BAD', - teal: '#42888D', - redSoft: '#D87566', - } + red: "#E36C59", + orange: "#DA9540", + blue: "#778BAD", + teal: "#42888D", + redSoft: "#D87566", + }, }; export default { - darkMode: ['class'], - content: [ - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/providers/**/*.{js,ts,jsx,tsx,mdx}' - ], - safelist: [ - 'h-5', - 'shadow-[0px_4px_4px_rgba(0,0,0,0.25)]', - 'rounded-[15px]' + darkMode: ["class"], + content: [ + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/providers/**/*.{js,ts,jsx,tsx,mdx}", ], + safelist: ["h-5", "shadow-[0px_4px_4px_rgba(0,0,0,0.25)]", "rounded-[15px]"], theme: { - extend: { - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - brand: colors.brand, - neutral: colors.neutral, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - ...colors.accent - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - }, - sidebar: { - DEFAULT: 'hsl(var(--sidebar-background))', - foreground: 'hsl(var(--sidebar-foreground))', - primary: 'hsl(var(--sidebar-primary))', - 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', - accent: 'hsl(var(--sidebar-accent))', - 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', - border: 'hsl(var(--sidebar-border))', - ring: 'hsl(var(--sidebar-ring))' - } - }, - fontFamily: { - spaceGrotesk: [ - 'var(--font-space-grotesk)', - 'sans-serif' - ] - }, - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - keyframes: { - 'fade-in': { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, + extend: { + colors: { + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + brand: colors.brand, + neutral: colors.neutral, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + ...colors.accent, + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + chart: { + "1": "hsl(var(--chart-1))", + "2": "hsl(var(--chart-2))", + "3": "hsl(var(--chart-3))", + "4": "hsl(var(--chart-4))", + "5": "hsl(var(--chart-5))", + }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, + }, + fontFamily: { + spaceGrotesk: ["var(--font-space-grotesk)", "sans-serif"], + }, + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", }, - 'modal-scale': { - '0%': { - opacity: '0', - transform: 'scale(0.9)' + keyframes: { + "fade-in": { + "0%": { opacity: "0" }, + "100%": { opacity: "1" }, }, - '100%': { - opacity: '1', - transform: 'scale(1)' + "modal-scale": { + "0%": { + opacity: "0", + transform: "scale(0.9)", + }, + "100%": { + opacity: "1", + transform: "scale(1)", + }, }, - } + }, + animation: { + "fade-in": "fade-in 0.3s ease-out", + "modal-scale": "modal-scale 0.3s ease-out", + }, }, - animation: { - 'fade-in': 'fade-in 0.3s ease-out', - 'modal-scale': 'modal-scale 0.3s ease-out', - } - } }, plugins: [animate], -} satisfies Config; \ No newline at end of file +} satisfies Config; diff --git a/package.json b/package.json new file mode 100644 index 0000000..402da4f --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@next/bundle-analyzer": "^15.1.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7f1c8c7 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,256 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@biomejs/biome': + specifier: 1.9.4 + version: 1.9.4 + '@next/bundle-analyzer': + specifier: ^15.1.6 + version: 15.1.6 + +packages: + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + + '@next/bundle-analyzer@15.1.6': + resolution: {integrity: sha512-hGzQyDqJzFHcHNCyTqM3o05BpVq5tGnRODccZBVJDBf5Miv/26UJPMB0wh9L9j3ylgHC+0/v8BaBnBBek1rC6Q==} + + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + webpack-bundle-analyzer@4.10.1: + resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} + engines: {node: '>= 10.13.0'} + hasBin: true + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@discoveryjs/json-ext@0.5.7': {} + + '@next/bundle-analyzer@15.1.6': + dependencies: + webpack-bundle-analyzer: 4.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@polka/url@1.0.0-next.28': {} + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + commander@7.2.0: {} + + debounce@1.2.1: {} + + duplexer@0.1.2: {} + + escape-string-regexp@4.0.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + html-escaper@2.0.2: {} + + is-plain-object@5.0.0: {} + + mrmime@2.0.0: {} + + opener@1.5.2: {} + + picocolors@1.1.1: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + + totalist@3.0.1: {} + + webpack-bundle-analyzer@4.10.1: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.14.0 + acorn-walk: 8.3.4 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + is-plain-object: 5.0.0 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@7.5.10: {}