diff --git a/.env.example b/.env.example index 0ad8d96..860db75 100644 --- a/.env.example +++ b/.env.example @@ -1,53 +1,17 @@ -# ================================ -# APPLICATION SETTINGS -# ================================ -NEXT_PUBLIC_APP_URL='http://localhost:3000' -NODE_ENV=development -APP_NAME="Flash Fathom AI" -APP_DESCRIPTION="Your AI-powered flashcard generator" -APP_VERSION="1.0.0" -APP_HOST=localhost -APP_PORT=3000 - -# ================================ -# POSTGRESQL CONFIGURATION -# ================================ -POSTGRES_DB=flashfathom -POSTGRES_USER=postgres -POSTGRES_PASSWORD=your_secure_password_here -POSTGRES_PORT=5432 - -# ================================ -# CLERK AUTHENTICATION -# ================================ -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="your nextjs publishable key" -CLERK_SECRET_KEY="your clerk secret key" +# Clerk +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= +CLERK_SECRET_KEY= -# Clerk URLs -NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in -NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up -NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/generate -NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ +# Razorpay +NEXT_PUBLIC_RAZORPAY_KEY_ID= +RAZORPAY_KEY_ID= +RAZORPAY_KEY_SECRET= -# ================================ -# AI APIS -# ================================ -GEMINI_API_KEY="" +# Database +DATABASE_URL= -# ================================ -# EMAIL CONFIGURATION -# ================================ -EMAIL_USER= -EMAIL_PASS= +# Gemini API +GEMINI_API_KEY= -# ================================ -# DATABASE URL (Updated for Docker) -# ================================ -DATABASE_URL="postgresql://postgres:your_secure_password_here@postgres:5432/flashfathom" - -# ================================ -# RAZORPAY CONFIGURATION -# ================================ -RAZORPAY_KEY_ID="your_razorpay_key_id" -RAZORPAY_KEY_SECRET="your_razorpay_key_secret" -NEXT_PUBLIC_RAZORPAY_KEY_ID="your_razorpay_key_id" +# App Port +APP_PORT=3000 diff --git a/.gitignore b/.gitignore index 011c715..176f698 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# clerk configuration (can include secrets) +/.clerk/ diff --git a/Dockerfile b/Dockerfile index ec88d44..8bf9669 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ # ================================ FROM node:18-alpine AS builder -# Install system dependencies -RUN apk add --no-cache openssl libc6-compat postgresql-client +# Install system dependencies, including the required OpenSSL 1.1 library +RUN apk add --no-cache openssl1.1-compat libc6-compat postgresql-client WORKDIR /app @@ -58,8 +58,8 @@ ENV NEXT_TELEMETRY_DISABLED=1 # Enable corepack and install pnpm RUN corepack enable && corepack prepare pnpm@latest --activate -# Install runtime dependencies INCLUDING postgresql-client for pg_isready -RUN apk add --no-cache openssl libc6-compat postgresql-client +# Install runtime dependencies, including the required OpenSSL 1.1 library +RUN apk add --no-cache openssl1.1-compat libc6-compat postgresql-client # Create non-root user RUN addgroup --system --gid 1001 flashfathom \ diff --git a/next.config.js b/next.config.js index 71e244f..3e1e7c2 100644 --- a/next.config.js +++ b/next.config.js @@ -1,9 +1,13 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone', + // Remove standalone output to avoid Windows symlink permission issues + // output: 'standalone', transpilePackages: ['react-toastify'], // ✅ FIXED: Updated for Next.js 15 stable serverExternalPackages: ['@prisma/client'], // Moved from experimental + // Disable static generation to avoid Clerk issues during build + trailingSlash: false, + generateEtags: false, turbopack: { // ✅ FIXED: Moved from experimental.turbo rules: { diff --git a/package.json b/package.json index 6efbafe..5ba5c7e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@vercel/analytics": "^1.5.0", "axios": "^1.8.2", "button": "^1.1.1", + "chart.js": "^4.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "^16.4.7", @@ -45,6 +46,7 @@ "nodemailer": "^6.10.0", "razorpay": "^2.9.6", "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", "react-toastify": "^11.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbf4b98..060970b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: button: specifier: ^1.1.1 version: 1.1.1 + chart.js: + specifier: ^4.5.0 + version: 4.5.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -108,6 +111,9 @@ importers: react: specifier: ^19.0.0 version: 19.1.1 + react-chartjs-2: + specifier: ^5.3.0 + version: 5.3.0(chart.js@4.5.0)(react@19.1.1) react-dom: specifier: ^19.0.0 version: 19.1.1(react@19.1.1) @@ -138,7 +144,7 @@ importers: devDependencies: '@types/node': specifier: ^20.17.24 - version: 20.19.12 + version: 20.19.13 '@types/react': specifier: 19.1.8 version: 19.1.8 @@ -183,44 +189,44 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-ses@3.879.0': - resolution: {integrity: sha512-6yydcKf01tXAIsya5YBOcznvGN4DN8crLEuYC0jwG+67loCeq2HZMO1rL3ouaIllCSgmO0l7KHDK62BQr3Z3Zg==} + '@aws-sdk/client-ses@3.882.0': + resolution: {integrity: sha512-Ao+AKBOg9kX1EhTefcKTawfg71NMEWkL9EB0gZlfjoLbDCWNNW5SDbSXh9wvCJ9+P2S7ubLbn/9Qe+wugBFQ1A==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.879.0': - resolution: {integrity: sha512-+Pc3OYFpRYpKLKRreovPM63FPPud1/SF9vemwIJfz6KwsBCJdvg7vYD1xLSIp5DVZLeetgf4reCyAA5ImBfZuw==} + '@aws-sdk/client-sso@3.882.0': + resolution: {integrity: sha512-JFWJB+2PZvygDuqb4iWKCro1Tl5L4tGBXMHe94jYMYnfajYGm58bW3RsPj3cKD2+TvIMUSXmNriNv+LbDKZmNw==} engines: {node: '>=18.0.0'} - '@aws-sdk/core@3.879.0': - resolution: {integrity: sha512-AhNmLCrx980LsK+SfPXGh7YqTyZxsK0Qmy18mWmkfY0TSq7WLaSDB5zdQbgbnQCACCHy8DUYXbi4KsjlIhv3PA==} + '@aws-sdk/core@3.882.0': + resolution: {integrity: sha512-m43/gEDbxqxLT/Mbn/OA21TuFpyocOUzjiSA2HBnLQ3KivA4ez0nsW91vh0Sp3TOfLgiZbRbVhmI6XfsFinwBg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-env@3.879.0': - resolution: {integrity: sha512-JgG7A8SSbr5IiCYL8kk39Y9chdSB5GPwBorDW8V8mr19G9L+qd6ohED4fAocoNFaDnYJ5wGAHhCfSJjzcsPBVQ==} + '@aws-sdk/credential-provider-env@3.882.0': + resolution: {integrity: sha512-khhE1k+4XvGm8Mk6vVUbrVvEnx3r8E6dymSKSiAKf0lwsnKWAWd1RLGwLusqVgtGR4Jfsrbg7ox9MczIjgCiTg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-http@3.879.0': - resolution: {integrity: sha512-2hM5ByLpyK+qORUexjtYyDZsgxVCCUiJQZRMGkNXFEGz6zTpbjfTIWoh3zRgWHEBiqyPIyfEy50eIF69WshcuA==} + '@aws-sdk/credential-provider-http@3.882.0': + resolution: {integrity: sha512-j3mBF+Q6RU3u8t5O1KOWbQQCi0WNSl47sNIa1RvyN6qK1WIA8BxM1hB25mI9TMPrNZMFthljVec+JcNjRNG34A==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-ini@3.879.0': - resolution: {integrity: sha512-07M8zfb73KmMBqVO5/V3Ea9kqDspMX0fO0kaI1bsjWI6ngnMye8jCE0/sIhmkVAI0aU709VA0g+Bzlopnw9EoQ==} + '@aws-sdk/credential-provider-ini@3.882.0': + resolution: {integrity: sha512-nUacsSYKyTUmv/Fqe0efihCRCabea5MZtGSZF0l2V8QBo39yJjw0wVmRK6G4bfm5lY7v2EVVIUCpiTvxRRUbHg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-node@3.879.0': - resolution: {integrity: sha512-FYaAqJbnSTrVL2iZkNDj2hj5087yMv2RN2GA8DJhe7iOJjzhzRojrtlfpWeJg6IhK0sBKDH+YXbdeexCzUJvtA==} + '@aws-sdk/credential-provider-node@3.882.0': + resolution: {integrity: sha512-sELdV+leCfY+Bw8NQo3H65oIT+9thqZU0RWyv85EfZVvKEwWDt4McA7+Co1VkH+nCY21s5jz4SOqIrYuT0cSQg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-process@3.879.0': - resolution: {integrity: sha512-7r360x1VyEt35Sm1JFOzww2WpnfJNBbvvnzoyLt7WRfK0S/AfsuWhu5ltJ80QvJ0R3AiSNbG+q/btG2IHhDYPQ==} + '@aws-sdk/credential-provider-process@3.882.0': + resolution: {integrity: sha512-S3BgGcaR+L7CQAQn3Ysy9KSnck7+hDicAGM/dYvvJ8GwZNIOc0542Y+ntpV1UYa7OuZPWzGy2v2NcJSCbYDXEA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-sso@3.879.0': - resolution: {integrity: sha512-gd27B0NsgtKlaPNARj4IX7F7US5NuU691rGm0EUSkDsM7TctvJULighKoHzPxDQlrDbVI11PW4WtKS/Zg5zPlQ==} + '@aws-sdk/credential-provider-sso@3.882.0': + resolution: {integrity: sha512-1pZRTKiDl6Oh/jP75lEoSkJrer1YEm8lMconB8dX9bsaWbp9cZeMJMK6pts5VQcveeOLr/8/U9TESboPjHBcyA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-web-identity@3.879.0': - resolution: {integrity: sha512-Jy4uPFfGzHk1Mxy+/Wr43vuw9yXsE2yiF4e4598vc3aJfO0YtA2nSfbKD3PNKRORwXbeKqWPfph9SCKQpWoxEg==} + '@aws-sdk/credential-provider-web-identity@3.882.0': + resolution: {integrity: sha512-EvpsD0Vcz5WgXjpC53KAQ2CkeUp0KwwiV6brgQTXl+9yV/M8M0aK5Qk5ep/MPbAn5gtbqXHaCkiExaN4YYOhCg==} engines: {node: '>=18.0.0'} '@aws-sdk/middleware-host-header@3.873.0': @@ -235,20 +241,20 @@ packages: resolution: {integrity: sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-user-agent@3.879.0': - resolution: {integrity: sha512-DDSV8228lQxeMAFKnigkd0fHzzn5aauZMYC3CSj6e5/qE7+9OwpkUcjHfb7HZ9KWG6L2/70aKZXHqiJ4xKhOZw==} + '@aws-sdk/middleware-user-agent@3.882.0': + resolution: {integrity: sha512-IdLVpV2b0qryxFb/gNPwZoayLUdgmb41fWpLiIf99pyNwR7TGs/9Ri2amS3PnaQHuES947xYSYZ9Ej0kBgjHKg==} engines: {node: '>=18.0.0'} - '@aws-sdk/nested-clients@3.879.0': - resolution: {integrity: sha512-7+n9NpIz9QtKYnxmw1fHi9C8o0GrX8LbBR4D50c7bH6Iq5+XdSuL5AFOWWQ5cMD0JhqYYJhK/fJsVau3nUtC4g==} + '@aws-sdk/nested-clients@3.882.0': + resolution: {integrity: sha512-IQkOtl/DhLV5+tJI7ZwjBDJO1lIoYOcmNQzcg8ly9RTdMoTcEtklevxmAwWB4DEFiIctUk2OSjHqhfWjeYredA==} engines: {node: '>=18.0.0'} '@aws-sdk/region-config-resolver@3.873.0': resolution: {integrity: sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==} engines: {node: '>=18.0.0'} - '@aws-sdk/token-providers@3.879.0': - resolution: {integrity: sha512-47J7sCwXdnw9plRZNAGVkNEOlSiLb/kR2slnDIHRK9NB/ECKsoqgz5OZQJ9E2f0yqOs8zSNJjn3T01KxpgW8Qw==} + '@aws-sdk/token-providers@3.882.0': + resolution: {integrity: sha512-/Z6F8Cc+QjBMEPh3ZXy7JM1vMZCS41+Nh9VgdUwvvdJTA7LRXSDBRDL3cQPa7bii9unZ8SqsIC+7Nlw1LKwwJA==} engines: {node: '>=18.0.0'} '@aws-sdk/types@3.862.0': @@ -266,8 +272,8 @@ packages: '@aws-sdk/util-user-agent-browser@3.873.0': resolution: {integrity: sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==} - '@aws-sdk/util-user-agent-node@3.879.0': - resolution: {integrity: sha512-A5KGc1S+CJRzYnuxJQQmH1BtGsz46AgyHkqReKfGiNQA8ET/9y9LQ5t2ABqnSBHHIh3+MiCcQSkUZ0S3rTodrQ==} + '@aws-sdk/util-user-agent-node@3.882.0': + resolution: {integrity: sha512-7zPtGXeAs6UzKjrrSbMNiFMSLZ/2DWvJ26KBOasS3zQbL534yoNos4HUA3OOXSpKFBAIEcYWu6rzR4ptlvx50w==} engines: {node: '>=18.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -540,6 +546,9 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -991,176 +1000,176 @@ packages: '@rushstack/eslint-patch@1.12.0': resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} - '@smithy/abort-controller@4.0.5': - resolution: {integrity: sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==} + '@smithy/abort-controller@4.1.0': + resolution: {integrity: sha512-wEhSYznxOmx7EdwK1tYEWJF5+/wmSFsff9BfTOn8oO/+KPl3gsmThrb6MJlWbOC391+Ya31s5JuHiC2RlT80Zg==} engines: {node: '>=18.0.0'} - '@smithy/config-resolver@4.1.5': - resolution: {integrity: sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==} + '@smithy/config-resolver@4.2.0': + resolution: {integrity: sha512-FA10YhPFLy23uxeWu7pOM2ctlw+gzbPMTZQwrZ8FRIfyJ/p8YIVz7AVTB5jjLD+QIerydyKcVMZur8qzzDILAQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.9.2': - resolution: {integrity: sha512-H7H+dnfyHa/XXmZB3+IcqB1snIvbXaeGbV7//PMY69YKMOfGtuHPg6aukxsD0TyqmIU+bcX5nitR+nf/19nTlQ==} + '@smithy/core@3.10.0': + resolution: {integrity: sha512-bXyD3Ij6b1qDymEYlEcF+QIjwb9gObwZNaRjETJsUEvSIzxFdynSQ3E4ysY7lUFSBzeWBNaFvX+5A0smbC2q6A==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.0.7': - resolution: {integrity: sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==} + '@smithy/credential-provider-imds@4.1.0': + resolution: {integrity: sha512-iVwNhxTsCQTPdp++4C/d9xvaDmuEWhXi55qJobMp9QMaEHRGH3kErU4F8gohtdsawRqnUy/ANylCjKuhcR2mPw==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.1.1': - resolution: {integrity: sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==} + '@smithy/fetch-http-handler@5.2.0': + resolution: {integrity: sha512-VZenjDdVaUGiy3hwQtxm75nhXZrhFG+3xyL93qCQAlYDyhT/jeDWM8/3r5uCFMlTmmyrIjiDyiOynVFchb0BSg==} engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.0.5': - resolution: {integrity: sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==} + '@smithy/hash-node@4.1.0': + resolution: {integrity: sha512-mXkJQ/6lAXTuoSsEH+d/fHa4ms4qV5LqYoPLYhmhCRTNcMMdg+4Ya8cMgU1W8+OR40eX0kzsExT7fAILqtTl2w==} engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.0.5': - resolution: {integrity: sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==} + '@smithy/invalid-dependency@4.1.0': + resolution: {integrity: sha512-4/FcV6aCMzgpM4YyA/GRzTtG28G0RQJcWK722MmpIgzOyfSceWcI9T9c8matpHU9qYYLaWtk8pSGNCLn5kzDRw==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/is-array-buffer@4.0.0': - resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} + '@smithy/is-array-buffer@4.1.0': + resolution: {integrity: sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.0.5': - resolution: {integrity: sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==} + '@smithy/middleware-content-length@4.1.0': + resolution: {integrity: sha512-x3dgLFubk/ClKVniJu+ELeZGk4mq7Iv0HgCRUlxNUIcerHTLVmq7Q5eGJL0tOnUltY6KFw5YOKaYxwdcMwox/w==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.1.21': - resolution: {integrity: sha512-VCFE6LGSbnXs6uxLTdtar6dbkOHa9mrj692pZJx1mQVEzk0gvckAX9WB9BzlONUpv92QBWGezROz/+yEitQjAQ==} + '@smithy/middleware-endpoint@4.2.0': + resolution: {integrity: sha512-J1eCF7pPDwgv7fGwRd2+Y+H9hlIolF3OZ2PjptonzzyOXXGh/1KGJAHpEcY1EX+WLlclKu2yC5k+9jWXdUG4YQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.1.22': - resolution: {integrity: sha512-mb6/wn4ixnSJCkKVLs51AKAyknbSTvwrHCM7cqgwGfYQ7/J6Qvv+49cBHe6Rl8Q0m3fROVYcSvM6bBiQtuhYWg==} + '@smithy/middleware-retry@4.2.0': + resolution: {integrity: sha512-raL5oWYf5ALl3jCJrajE8enKJEnV/2wZkKS6mb3ZRY2tg3nj66ssdWy5Ps8E6Yu8Wqh3Tt+Sb9LozjvwZupq+A==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.0.9': - resolution: {integrity: sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==} + '@smithy/middleware-serde@4.1.0': + resolution: {integrity: sha512-CtLFYlHt7c2VcztyVRc+25JLV4aGpmaSv9F1sPB0AGFL6S+RPythkqpGDa2XBQLJQooKkjLA1g7Xe4450knShg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.0.5': - resolution: {integrity: sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==} + '@smithy/middleware-stack@4.1.0': + resolution: {integrity: sha512-91Fuw4IKp0eK8PNhMXrHRcYA1jvbZ9BJGT91wwPy3bTQT8mHTcQNius/EhSQTlT9QUI3Ki1wjHeNXbWK0tO8YQ==} engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.1.4': - resolution: {integrity: sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==} + '@smithy/node-config-provider@4.2.0': + resolution: {integrity: sha512-8/fpilqKurQ+f8nFvoFkJ0lrymoMJ+5/CQV5IcTv/MyKhk2Q/EFYCAgTSWHD4nMi9ux9NyBBynkyE9SLg2uSLA==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.1.1': - resolution: {integrity: sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==} + '@smithy/node-http-handler@4.2.0': + resolution: {integrity: sha512-G4NV70B4hF9vBrUkkvNfWO6+QR4jYjeO4tc+4XrKCb4nPYj49V9Hu8Ftio7Mb0/0IlFyEOORudHrm+isY29nCA==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.0.5': - resolution: {integrity: sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==} + '@smithy/property-provider@4.1.0': + resolution: {integrity: sha512-eksMjMHUlG5PwOUWO3k+rfLNOPVPJ70mUzyYNKb5lvyIuAwS4zpWGsxGiuT74DFWonW0xRNy+jgzGauUzX7SyA==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.1.3': - resolution: {integrity: sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==} + '@smithy/protocol-http@5.2.0': + resolution: {integrity: sha512-bwjlh5JwdOQnA01be+5UvHK4HQz4iaRKlVG46hHSJuqi0Ribt3K06Z1oQ29i35Np4G9MCDgkOGcHVyLMreMcbg==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.0.5': - resolution: {integrity: sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==} + '@smithy/querystring-builder@4.1.0': + resolution: {integrity: sha512-JqTWmVIq4AF8R8OK/2cCCiQo5ZJ0SRPsDkDgLO5/3z8xxuUp1oMIBBjfuueEe+11hGTZ6rRebzYikpKc6yQV9Q==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.0.5': - resolution: {integrity: sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==} + '@smithy/querystring-parser@4.1.0': + resolution: {integrity: sha512-VgdHhr8YTRsjOl4hnKFm7xEMOCRTnKw3FJ1nU+dlWNhdt/7eEtxtkdrJdx7PlRTabdANTmvyjE4umUl9cK4awg==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.0.7': - resolution: {integrity: sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==} + '@smithy/service-error-classification@4.1.0': + resolution: {integrity: sha512-UBpNFzBNmS20jJomuYn++Y+soF8rOK9AvIGjS9yGP6uRXF5rP18h4FDUsoNpWTlSsmiJ87e2DpZo9ywzSMH7PQ==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.0.5': - resolution: {integrity: sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==} + '@smithy/shared-ini-file-loader@4.1.0': + resolution: {integrity: sha512-W0VMlz9yGdQ/0ZAgWICFjFHTVU0YSfGoCVpKaExRM/FDkTeP/yz8OKvjtGjs6oFokCRm0srgj/g4Cg0xuHu8Rw==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.1.3': - resolution: {integrity: sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==} + '@smithy/signature-v4@5.2.0': + resolution: {integrity: sha512-ObX1ZqG2DdZQlXx9mLD7yAR8AGb7yXurGm+iWx9x4l1fBZ8CZN2BRT09aSbcXVPZXWGdn5VtMuupjxhOTI2EjA==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.5.2': - resolution: {integrity: sha512-WRdTJ7aNSJY0WuGpxrvVgRaFKGiuvtXX1Txhnu2BdynraSlH2bcP75riQ4SiQfawU1HNEKaPI5gf/ePm+Ro/Cw==} + '@smithy/smithy-client@4.6.0': + resolution: {integrity: sha512-TvlIshqx5PIi0I0AiR+PluCpJ8olVG++xbYkAIGCUkByaMUlfOXLgjQTmYbr46k4wuDe8eHiTIlUflnjK2drPQ==} engines: {node: '>=18.0.0'} - '@smithy/types@4.3.2': - resolution: {integrity: sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==} + '@smithy/types@4.4.0': + resolution: {integrity: sha512-4jY91NgZz+ZnSFcVzWwngOW6VuK3gR/ihTwSU1R/0NENe9Jd8SfWgbhDCAGUWL3bI7DiDSW7XF6Ui6bBBjrqXw==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.0.5': - resolution: {integrity: sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==} + '@smithy/url-parser@4.1.0': + resolution: {integrity: sha512-/LYEIOuO5B2u++tKr1NxNxhZTrr3A63jW8N73YTwVeUyAlbB/YM+hkftsvtKAcMt3ADYo0FsF1GY3anehffSVQ==} engines: {node: '>=18.0.0'} - '@smithy/util-base64@4.0.0': - resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} + '@smithy/util-base64@4.1.0': + resolution: {integrity: sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==} engines: {node: '>=18.0.0'} - '@smithy/util-body-length-browser@4.0.0': - resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} + '@smithy/util-body-length-browser@4.1.0': + resolution: {integrity: sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==} engines: {node: '>=18.0.0'} - '@smithy/util-body-length-node@4.0.0': - resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} + '@smithy/util-body-length-node@4.1.0': + resolution: {integrity: sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==} engines: {node: '>=18.0.0'} '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} - '@smithy/util-buffer-from@4.0.0': - resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} + '@smithy/util-buffer-from@4.1.0': + resolution: {integrity: sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==} engines: {node: '>=18.0.0'} - '@smithy/util-config-provider@4.0.0': - resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} + '@smithy/util-config-provider@4.1.0': + resolution: {integrity: sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.0.29': - resolution: {integrity: sha512-awrIb21sWml3OMRhqf8e5GPLuZAcH3PRAHXVOPof/rBOKLxc6N01ZRs25154Ww6Ygm9oNP6G0tVvhcy8ktYXtw==} + '@smithy/util-defaults-mode-browser@4.1.0': + resolution: {integrity: sha512-D27cLtJtC4EEeERJXS+JPoogz2tE5zeE3zhWSSu6ER5/wJ5gihUxIzoarDX6K1U27IFTHit5YfHqU4Y9RSGE0w==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.0.29': - resolution: {integrity: sha512-DxBWCC059GwOQXc5nxVudhdGQLZHTDhU4rkK4rvaBQn8IWBw8G+3H2hWk897LaNv6zwwhh7kpfqF0rJ77DvlSg==} + '@smithy/util-defaults-mode-node@4.1.0': + resolution: {integrity: sha512-gnZo3u5dP1o87plKupg39alsbeIY1oFFnCyV2nI/++pL19vTtBLgOyftLEjPjuXmoKn2B2rskX8b7wtC/+3Okg==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.0.7': - resolution: {integrity: sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==} + '@smithy/util-endpoints@3.1.0': + resolution: {integrity: sha512-5LFg48KkunBVGrNs3dnQgLlMXJLVo7k9sdZV5su3rjO3c3DmQ2LwUZI0Zr49p89JWK6sB7KmzyI2fVcDsZkwuw==} engines: {node: '>=18.0.0'} - '@smithy/util-hex-encoding@4.0.0': - resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} + '@smithy/util-hex-encoding@4.1.0': + resolution: {integrity: sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==} engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.0.5': - resolution: {integrity: sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==} + '@smithy/util-middleware@4.1.0': + resolution: {integrity: sha512-612onNcKyxhP7/YOTKFTb2F6sPYtMRddlT5mZvYf1zduzaGzkYhpYIPxIeeEwBZFjnvEqe53Ijl2cYEfJ9d6/Q==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.0.7': - resolution: {integrity: sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==} + '@smithy/util-retry@4.1.0': + resolution: {integrity: sha512-5AGoBHb207xAKSVwaUnaER+L55WFY8o2RhlafELZR3mB0J91fpL+Qn+zgRkPzns3kccGaF2vy0HmNVBMWmN6dA==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.2.4': - resolution: {integrity: sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==} + '@smithy/util-stream@4.3.0': + resolution: {integrity: sha512-ZOYS94jksDwvsCJtppHprUhsIscRnCKGr6FXCo3SxgQ31ECbza3wqDBqSy6IsAak+h/oAXb1+UYEBmDdseAjUQ==} engines: {node: '>=18.0.0'} - '@smithy/util-uri-escape@4.0.0': - resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} + '@smithy/util-uri-escape@4.1.0': + resolution: {integrity: sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==} engines: {node: '>=18.0.0'} '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@smithy/util-utf8@4.0.0': - resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} + '@smithy/util-utf8@4.1.0': + resolution: {integrity: sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==} engines: {node: '>=18.0.0'} - '@smithy/util-waiter@4.0.7': - resolution: {integrity: sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==} + '@smithy/util-waiter@4.1.0': + resolution: {integrity: sha512-IUuj2zpGdeKaY5OdGnU83BUJsv7OA9uw3rNVSOuvzLMXMpBTU+W6V0SsQh6iI32lKUJArlnEU4BIzp83hghR/g==} engines: {node: '>=18.0.0'} '@stablelib/base64@1.0.1': @@ -1186,8 +1195,8 @@ packages: '@supabase/realtime-js@2.15.4': resolution: {integrity: sha512-e/FYIWjvQJHOCNACWehnKvg26zosju3694k0NMUNb+JGLdvHJzEa29ZVVLmawd2kvx4hdbv8mxSqfttRnH3+DA==} - '@supabase/storage-js@2.11.0': - resolution: {integrity: sha512-Y+kx/wDgd4oasAgoAq0bsbQojwQ+ejIif8uczZ9qufRHWFLMU5cODT+ApHsSrDufqUcVKt+eyxtOXSkeh2v9ww==} + '@supabase/storage-js@2.11.1': + resolution: {integrity: sha512-kaKCJZcZrHDCO9L76bEPzNv2caCStOigOUioHw7CvdEzvcSKjVuomRfN2Y9EqXmJH4tEHoBi3tCs/Ye2e3HwDw==} '@supabase/supabase-js@2.57.0': resolution: {integrity: sha512-h9ttcL0MY4h+cGqZl95F/RuqccuRBjHU9B7Qqvw0Da+pPK2sUlU1/UdvyqUGj37UsnSphr9pdGfeXjesYkBcyA==} @@ -1212,8 +1221,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@20.19.12': - resolution: {integrity: sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==} + '@types/node@20.19.13': + resolution: {integrity: sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==} '@types/nodemailer@6.4.19': resolution: {integrity: sha512-Fi8DwmuAduTk1/1MpkR9EwS0SsDvYXx5RxivAVII1InDCIxmhj/iQm3W8S3EVb/0arnblr6PK0FK4wYa7bwdLg==} @@ -1575,6 +1584,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chart.js@4.5.0: + resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} + engines: {pnpm: '>=8'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2589,6 +2602,12 @@ packages: razorpay@2.9.6: resolution: {integrity: sha512-zsHAQzd6e1Cc6BNoCNZQaf65ElL6O6yw0wulxmoG5VQDr363fZC90Mp1V5EktVzG45yPyNomNXWlf4cQ3622gQ==} + react-chartjs-2@5.3.0: + resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -3066,197 +3085,197 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-ses@3.879.0': + '@aws-sdk/client-ses@3.882.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.879.0 - '@aws-sdk/credential-provider-node': 3.879.0 + '@aws-sdk/core': 3.882.0 + '@aws-sdk/credential-provider-node': 3.882.0 '@aws-sdk/middleware-host-header': 3.873.0 '@aws-sdk/middleware-logger': 3.876.0 '@aws-sdk/middleware-recursion-detection': 3.873.0 - '@aws-sdk/middleware-user-agent': 3.879.0 + '@aws-sdk/middleware-user-agent': 3.882.0 '@aws-sdk/region-config-resolver': 3.873.0 '@aws-sdk/types': 3.862.0 '@aws-sdk/util-endpoints': 3.879.0 '@aws-sdk/util-user-agent-browser': 3.873.0 - '@aws-sdk/util-user-agent-node': 3.879.0 - '@smithy/config-resolver': 4.1.5 - '@smithy/core': 3.9.2 - '@smithy/fetch-http-handler': 5.1.1 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.1.21 - '@smithy/middleware-retry': 4.1.22 - '@smithy/middleware-serde': 4.0.9 - '@smithy/middleware-stack': 4.0.5 - '@smithy/node-config-provider': 4.1.4 - '@smithy/node-http-handler': 4.1.1 - '@smithy/protocol-http': 5.1.3 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 - '@smithy/url-parser': 4.0.5 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.29 - '@smithy/util-defaults-mode-node': 4.0.29 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-retry': 4.0.7 - '@smithy/util-utf8': 4.0.0 - '@smithy/util-waiter': 4.0.7 + '@aws-sdk/util-user-agent-node': 3.882.0 + '@smithy/config-resolver': 4.2.0 + '@smithy/core': 3.10.0 + '@smithy/fetch-http-handler': 5.2.0 + '@smithy/hash-node': 4.1.0 + '@smithy/invalid-dependency': 4.1.0 + '@smithy/middleware-content-length': 4.1.0 + '@smithy/middleware-endpoint': 4.2.0 + '@smithy/middleware-retry': 4.2.0 + '@smithy/middleware-serde': 4.1.0 + '@smithy/middleware-stack': 4.1.0 + '@smithy/node-config-provider': 4.2.0 + '@smithy/node-http-handler': 4.2.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 + '@smithy/url-parser': 4.1.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-body-length-node': 4.1.0 + '@smithy/util-defaults-mode-browser': 4.1.0 + '@smithy/util-defaults-mode-node': 4.1.0 + '@smithy/util-endpoints': 3.1.0 + '@smithy/util-middleware': 4.1.0 + '@smithy/util-retry': 4.1.0 + '@smithy/util-utf8': 4.1.0 + '@smithy/util-waiter': 4.1.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.879.0': + '@aws-sdk/client-sso@3.882.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.879.0 + '@aws-sdk/core': 3.882.0 '@aws-sdk/middleware-host-header': 3.873.0 '@aws-sdk/middleware-logger': 3.876.0 '@aws-sdk/middleware-recursion-detection': 3.873.0 - '@aws-sdk/middleware-user-agent': 3.879.0 + '@aws-sdk/middleware-user-agent': 3.882.0 '@aws-sdk/region-config-resolver': 3.873.0 '@aws-sdk/types': 3.862.0 '@aws-sdk/util-endpoints': 3.879.0 '@aws-sdk/util-user-agent-browser': 3.873.0 - '@aws-sdk/util-user-agent-node': 3.879.0 - '@smithy/config-resolver': 4.1.5 - '@smithy/core': 3.9.2 - '@smithy/fetch-http-handler': 5.1.1 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.1.21 - '@smithy/middleware-retry': 4.1.22 - '@smithy/middleware-serde': 4.0.9 - '@smithy/middleware-stack': 4.0.5 - '@smithy/node-config-provider': 4.1.4 - '@smithy/node-http-handler': 4.1.1 - '@smithy/protocol-http': 5.1.3 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 - '@smithy/url-parser': 4.0.5 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.29 - '@smithy/util-defaults-mode-node': 4.0.29 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-retry': 4.0.7 - '@smithy/util-utf8': 4.0.0 + '@aws-sdk/util-user-agent-node': 3.882.0 + '@smithy/config-resolver': 4.2.0 + '@smithy/core': 3.10.0 + '@smithy/fetch-http-handler': 5.2.0 + '@smithy/hash-node': 4.1.0 + '@smithy/invalid-dependency': 4.1.0 + '@smithy/middleware-content-length': 4.1.0 + '@smithy/middleware-endpoint': 4.2.0 + '@smithy/middleware-retry': 4.2.0 + '@smithy/middleware-serde': 4.1.0 + '@smithy/middleware-stack': 4.1.0 + '@smithy/node-config-provider': 4.2.0 + '@smithy/node-http-handler': 4.2.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 + '@smithy/url-parser': 4.1.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-body-length-node': 4.1.0 + '@smithy/util-defaults-mode-browser': 4.1.0 + '@smithy/util-defaults-mode-node': 4.1.0 + '@smithy/util-endpoints': 3.1.0 + '@smithy/util-middleware': 4.1.0 + '@smithy/util-retry': 4.1.0 + '@smithy/util-utf8': 4.1.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.879.0': + '@aws-sdk/core@3.882.0': dependencies: '@aws-sdk/types': 3.862.0 '@aws-sdk/xml-builder': 3.873.0 - '@smithy/core': 3.9.2 - '@smithy/node-config-provider': 4.1.4 - '@smithy/property-provider': 4.0.5 - '@smithy/protocol-http': 5.1.3 - '@smithy/signature-v4': 5.1.3 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-utf8': 4.0.0 + '@smithy/core': 3.10.0 + '@smithy/node-config-provider': 4.2.0 + '@smithy/property-provider': 4.1.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/signature-v4': 5.2.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-middleware': 4.1.0 + '@smithy/util-utf8': 4.1.0 fast-xml-parser: 5.2.5 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.879.0': + '@aws-sdk/credential-provider-env@3.882.0': dependencies: - '@aws-sdk/core': 3.879.0 + '@aws-sdk/core': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/property-provider': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.879.0': + '@aws-sdk/credential-provider-http@3.882.0': dependencies: - '@aws-sdk/core': 3.879.0 + '@aws-sdk/core': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/fetch-http-handler': 5.1.1 - '@smithy/node-http-handler': 4.1.1 - '@smithy/property-provider': 4.0.5 - '@smithy/protocol-http': 5.1.3 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 - '@smithy/util-stream': 4.2.4 + '@smithy/fetch-http-handler': 5.2.0 + '@smithy/node-http-handler': 4.2.0 + '@smithy/property-provider': 4.1.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 + '@smithy/util-stream': 4.3.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.879.0': + '@aws-sdk/credential-provider-ini@3.882.0': dependencies: - '@aws-sdk/core': 3.879.0 - '@aws-sdk/credential-provider-env': 3.879.0 - '@aws-sdk/credential-provider-http': 3.879.0 - '@aws-sdk/credential-provider-process': 3.879.0 - '@aws-sdk/credential-provider-sso': 3.879.0 - '@aws-sdk/credential-provider-web-identity': 3.879.0 - '@aws-sdk/nested-clients': 3.879.0 + '@aws-sdk/core': 3.882.0 + '@aws-sdk/credential-provider-env': 3.882.0 + '@aws-sdk/credential-provider-http': 3.882.0 + '@aws-sdk/credential-provider-process': 3.882.0 + '@aws-sdk/credential-provider-sso': 3.882.0 + '@aws-sdk/credential-provider-web-identity': 3.882.0 + '@aws-sdk/nested-clients': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.0.7 - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/credential-provider-imds': 4.1.0 + '@smithy/property-provider': 4.1.0 + '@smithy/shared-ini-file-loader': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.879.0': + '@aws-sdk/credential-provider-node@3.882.0': dependencies: - '@aws-sdk/credential-provider-env': 3.879.0 - '@aws-sdk/credential-provider-http': 3.879.0 - '@aws-sdk/credential-provider-ini': 3.879.0 - '@aws-sdk/credential-provider-process': 3.879.0 - '@aws-sdk/credential-provider-sso': 3.879.0 - '@aws-sdk/credential-provider-web-identity': 3.879.0 + '@aws-sdk/credential-provider-env': 3.882.0 + '@aws-sdk/credential-provider-http': 3.882.0 + '@aws-sdk/credential-provider-ini': 3.882.0 + '@aws-sdk/credential-provider-process': 3.882.0 + '@aws-sdk/credential-provider-sso': 3.882.0 + '@aws-sdk/credential-provider-web-identity': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.0.7 - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/credential-provider-imds': 4.1.0 + '@smithy/property-provider': 4.1.0 + '@smithy/shared-ini-file-loader': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.879.0': + '@aws-sdk/credential-provider-process@3.882.0': dependencies: - '@aws-sdk/core': 3.879.0 + '@aws-sdk/core': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/property-provider': 4.1.0 + '@smithy/shared-ini-file-loader': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.879.0': + '@aws-sdk/credential-provider-sso@3.882.0': dependencies: - '@aws-sdk/client-sso': 3.879.0 - '@aws-sdk/core': 3.879.0 - '@aws-sdk/token-providers': 3.879.0 + '@aws-sdk/client-sso': 3.882.0 + '@aws-sdk/core': 3.882.0 + '@aws-sdk/token-providers': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/property-provider': 4.1.0 + '@smithy/shared-ini-file-loader': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.879.0': + '@aws-sdk/credential-provider-web-identity@3.882.0': dependencies: - '@aws-sdk/core': 3.879.0 - '@aws-sdk/nested-clients': 3.879.0 + '@aws-sdk/core': 3.882.0 + '@aws-sdk/nested-clients': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/property-provider': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -3264,72 +3283,72 @@ snapshots: '@aws-sdk/middleware-host-header@3.873.0': dependencies: '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 '@aws-sdk/middleware-logger@3.876.0': dependencies: '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 '@aws-sdk/middleware-recursion-detection@3.873.0': dependencies: '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.879.0': + '@aws-sdk/middleware-user-agent@3.882.0': dependencies: - '@aws-sdk/core': 3.879.0 + '@aws-sdk/core': 3.882.0 '@aws-sdk/types': 3.862.0 '@aws-sdk/util-endpoints': 3.879.0 - '@smithy/core': 3.9.2 - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 + '@smithy/core': 3.10.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.879.0': + '@aws-sdk/nested-clients@3.882.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.879.0 + '@aws-sdk/core': 3.882.0 '@aws-sdk/middleware-host-header': 3.873.0 '@aws-sdk/middleware-logger': 3.876.0 '@aws-sdk/middleware-recursion-detection': 3.873.0 - '@aws-sdk/middleware-user-agent': 3.879.0 + '@aws-sdk/middleware-user-agent': 3.882.0 '@aws-sdk/region-config-resolver': 3.873.0 '@aws-sdk/types': 3.862.0 '@aws-sdk/util-endpoints': 3.879.0 '@aws-sdk/util-user-agent-browser': 3.873.0 - '@aws-sdk/util-user-agent-node': 3.879.0 - '@smithy/config-resolver': 4.1.5 - '@smithy/core': 3.9.2 - '@smithy/fetch-http-handler': 5.1.1 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.1.21 - '@smithy/middleware-retry': 4.1.22 - '@smithy/middleware-serde': 4.0.9 - '@smithy/middleware-stack': 4.0.5 - '@smithy/node-config-provider': 4.1.4 - '@smithy/node-http-handler': 4.1.1 - '@smithy/protocol-http': 5.1.3 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 - '@smithy/url-parser': 4.0.5 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.29 - '@smithy/util-defaults-mode-node': 4.0.29 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-retry': 4.0.7 - '@smithy/util-utf8': 4.0.0 + '@aws-sdk/util-user-agent-node': 3.882.0 + '@smithy/config-resolver': 4.2.0 + '@smithy/core': 3.10.0 + '@smithy/fetch-http-handler': 5.2.0 + '@smithy/hash-node': 4.1.0 + '@smithy/invalid-dependency': 4.1.0 + '@smithy/middleware-content-length': 4.1.0 + '@smithy/middleware-endpoint': 4.2.0 + '@smithy/middleware-retry': 4.2.0 + '@smithy/middleware-serde': 4.1.0 + '@smithy/middleware-stack': 4.1.0 + '@smithy/node-config-provider': 4.2.0 + '@smithy/node-http-handler': 4.2.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 + '@smithy/url-parser': 4.1.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-body-length-node': 4.1.0 + '@smithy/util-defaults-mode-browser': 4.1.0 + '@smithy/util-defaults-mode-node': 4.1.0 + '@smithy/util-endpoints': 3.1.0 + '@smithy/util-middleware': 4.1.0 + '@smithy/util-retry': 4.1.0 + '@smithy/util-utf8': 4.1.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -3337,35 +3356,35 @@ snapshots: '@aws-sdk/region-config-resolver@3.873.0': dependencies: '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.1.4 - '@smithy/types': 4.3.2 - '@smithy/util-config-provider': 4.0.0 - '@smithy/util-middleware': 4.0.5 + '@smithy/node-config-provider': 4.2.0 + '@smithy/types': 4.4.0 + '@smithy/util-config-provider': 4.1.0 + '@smithy/util-middleware': 4.1.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.879.0': + '@aws-sdk/token-providers@3.882.0': dependencies: - '@aws-sdk/core': 3.879.0 - '@aws-sdk/nested-clients': 3.879.0 + '@aws-sdk/core': 3.882.0 + '@aws-sdk/nested-clients': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/property-provider': 4.1.0 + '@smithy/shared-ini-file-loader': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt '@aws-sdk/types@3.862.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 '@aws-sdk/util-endpoints@3.879.0': dependencies: '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.3.2 - '@smithy/url-parser': 4.0.5 - '@smithy/util-endpoints': 3.0.7 + '@smithy/types': 4.4.0 + '@smithy/url-parser': 4.1.0 + '@smithy/util-endpoints': 3.1.0 tslib: 2.8.1 '@aws-sdk/util-locate-window@3.873.0': @@ -3375,21 +3394,21 @@ snapshots: '@aws-sdk/util-user-agent-browser@3.873.0': dependencies: '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 bowser: 2.12.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.879.0': + '@aws-sdk/util-user-agent-node@3.882.0': dependencies: - '@aws-sdk/middleware-user-agent': 3.879.0 + '@aws-sdk/middleware-user-agent': 3.882.0 '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.1.4 - '@smithy/types': 4.3.2 + '@smithy/node-config-provider': 4.2.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 '@aws-sdk/xml-builder@3.873.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 '@clerk/backend@2.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': @@ -3645,6 +3664,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kurkle/color@0.3.4': {} + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -4058,197 +4079,197 @@ snapshots: '@rushstack/eslint-patch@1.12.0': {} - '@smithy/abort-controller@4.0.5': + '@smithy/abort-controller@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/config-resolver@4.1.5': + '@smithy/config-resolver@4.2.0': dependencies: - '@smithy/node-config-provider': 4.1.4 - '@smithy/types': 4.3.2 - '@smithy/util-config-provider': 4.0.0 - '@smithy/util-middleware': 4.0.5 + '@smithy/node-config-provider': 4.2.0 + '@smithy/types': 4.4.0 + '@smithy/util-config-provider': 4.1.0 + '@smithy/util-middleware': 4.1.0 tslib: 2.8.1 - '@smithy/core@3.9.2': + '@smithy/core@3.10.0': dependencies: - '@smithy/middleware-serde': 4.0.9 - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-stream': 4.2.4 - '@smithy/util-utf8': 4.0.0 + '@smithy/middleware-serde': 4.1.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-body-length-browser': 4.1.0 + '@smithy/util-middleware': 4.1.0 + '@smithy/util-stream': 4.3.0 + '@smithy/util-utf8': 4.1.0 '@types/uuid': 9.0.8 tslib: 2.8.1 uuid: 9.0.1 - '@smithy/credential-provider-imds@4.0.7': + '@smithy/credential-provider-imds@4.1.0': dependencies: - '@smithy/node-config-provider': 4.1.4 - '@smithy/property-provider': 4.0.5 - '@smithy/types': 4.3.2 - '@smithy/url-parser': 4.0.5 + '@smithy/node-config-provider': 4.2.0 + '@smithy/property-provider': 4.1.0 + '@smithy/types': 4.4.0 + '@smithy/url-parser': 4.1.0 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.1.1': + '@smithy/fetch-http-handler@5.2.0': dependencies: - '@smithy/protocol-http': 5.1.3 - '@smithy/querystring-builder': 4.0.5 - '@smithy/types': 4.3.2 - '@smithy/util-base64': 4.0.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/querystring-builder': 4.1.0 + '@smithy/types': 4.4.0 + '@smithy/util-base64': 4.1.0 tslib: 2.8.1 - '@smithy/hash-node@4.0.5': + '@smithy/hash-node@4.1.0': dependencies: - '@smithy/types': 4.3.2 - '@smithy/util-buffer-from': 4.0.0 - '@smithy/util-utf8': 4.0.0 + '@smithy/types': 4.4.0 + '@smithy/util-buffer-from': 4.1.0 + '@smithy/util-utf8': 4.1.0 tslib: 2.8.1 - '@smithy/invalid-dependency@4.0.5': + '@smithy/invalid-dependency@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 - '@smithy/is-array-buffer@4.0.0': + '@smithy/is-array-buffer@4.1.0': dependencies: tslib: 2.8.1 - '@smithy/middleware-content-length@4.0.5': + '@smithy/middleware-content-length@4.1.0': dependencies: - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.1.21': + '@smithy/middleware-endpoint@4.2.0': dependencies: - '@smithy/core': 3.9.2 - '@smithy/middleware-serde': 4.0.9 - '@smithy/node-config-provider': 4.1.4 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.3.2 - '@smithy/url-parser': 4.0.5 - '@smithy/util-middleware': 4.0.5 + '@smithy/core': 3.10.0 + '@smithy/middleware-serde': 4.1.0 + '@smithy/node-config-provider': 4.2.0 + '@smithy/shared-ini-file-loader': 4.1.0 + '@smithy/types': 4.4.0 + '@smithy/url-parser': 4.1.0 + '@smithy/util-middleware': 4.1.0 tslib: 2.8.1 - '@smithy/middleware-retry@4.1.22': + '@smithy/middleware-retry@4.2.0': dependencies: - '@smithy/node-config-provider': 4.1.4 - '@smithy/protocol-http': 5.1.3 - '@smithy/service-error-classification': 4.0.7 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-retry': 4.0.7 + '@smithy/node-config-provider': 4.2.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/service-error-classification': 4.1.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 + '@smithy/util-middleware': 4.1.0 + '@smithy/util-retry': 4.1.0 '@types/uuid': 9.0.8 tslib: 2.8.1 uuid: 9.0.1 - '@smithy/middleware-serde@4.0.9': + '@smithy/middleware-serde@4.1.0': dependencies: - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/middleware-stack@4.0.5': + '@smithy/middleware-stack@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/node-config-provider@4.1.4': + '@smithy/node-config-provider@4.2.0': dependencies: - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/property-provider': 4.1.0 + '@smithy/shared-ini-file-loader': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.1.1': + '@smithy/node-http-handler@4.2.0': dependencies: - '@smithy/abort-controller': 4.0.5 - '@smithy/protocol-http': 5.1.3 - '@smithy/querystring-builder': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/abort-controller': 4.1.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/querystring-builder': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/property-provider@4.0.5': + '@smithy/property-provider@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/protocol-http@5.1.3': + '@smithy/protocol-http@5.2.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/querystring-builder@4.0.5': + '@smithy/querystring-builder@4.1.0': dependencies: - '@smithy/types': 4.3.2 - '@smithy/util-uri-escape': 4.0.0 + '@smithy/types': 4.4.0 + '@smithy/util-uri-escape': 4.1.0 tslib: 2.8.1 - '@smithy/querystring-parser@4.0.5': + '@smithy/querystring-parser@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/service-error-classification@4.0.7': + '@smithy/service-error-classification@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 - '@smithy/shared-ini-file-loader@4.0.5': + '@smithy/shared-ini-file-loader@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/signature-v4@5.1.3': + '@smithy/signature-v4@5.2.0': dependencies: - '@smithy/is-array-buffer': 4.0.0 - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 - '@smithy/util-hex-encoding': 4.0.0 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-uri-escape': 4.0.0 - '@smithy/util-utf8': 4.0.0 + '@smithy/is-array-buffer': 4.1.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 + '@smithy/util-hex-encoding': 4.1.0 + '@smithy/util-middleware': 4.1.0 + '@smithy/util-uri-escape': 4.1.0 + '@smithy/util-utf8': 4.1.0 tslib: 2.8.1 - '@smithy/smithy-client@4.5.2': + '@smithy/smithy-client@4.6.0': dependencies: - '@smithy/core': 3.9.2 - '@smithy/middleware-endpoint': 4.1.21 - '@smithy/middleware-stack': 4.0.5 - '@smithy/protocol-http': 5.1.3 - '@smithy/types': 4.3.2 - '@smithy/util-stream': 4.2.4 + '@smithy/core': 3.10.0 + '@smithy/middleware-endpoint': 4.2.0 + '@smithy/middleware-stack': 4.1.0 + '@smithy/protocol-http': 5.2.0 + '@smithy/types': 4.4.0 + '@smithy/util-stream': 4.3.0 tslib: 2.8.1 - '@smithy/types@4.3.2': + '@smithy/types@4.4.0': dependencies: tslib: 2.8.1 - '@smithy/url-parser@4.0.5': + '@smithy/url-parser@4.1.0': dependencies: - '@smithy/querystring-parser': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/querystring-parser': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/util-base64@4.0.0': + '@smithy/util-base64@4.1.0': dependencies: - '@smithy/util-buffer-from': 4.0.0 - '@smithy/util-utf8': 4.0.0 + '@smithy/util-buffer-from': 4.1.0 + '@smithy/util-utf8': 4.1.0 tslib: 2.8.1 - '@smithy/util-body-length-browser@4.0.0': + '@smithy/util-body-length-browser@4.1.0': dependencies: tslib: 2.8.1 - '@smithy/util-body-length-node@4.0.0': + '@smithy/util-body-length-node@4.1.0': dependencies: tslib: 2.8.1 @@ -4257,66 +4278,66 @@ snapshots: '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 - '@smithy/util-buffer-from@4.0.0': + '@smithy/util-buffer-from@4.1.0': dependencies: - '@smithy/is-array-buffer': 4.0.0 + '@smithy/is-array-buffer': 4.1.0 tslib: 2.8.1 - '@smithy/util-config-provider@4.0.0': + '@smithy/util-config-provider@4.1.0': dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.0.29': + '@smithy/util-defaults-mode-browser@4.1.0': dependencies: - '@smithy/property-provider': 4.0.5 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 + '@smithy/property-provider': 4.1.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 bowser: 2.12.1 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.0.29': + '@smithy/util-defaults-mode-node@4.1.0': dependencies: - '@smithy/config-resolver': 4.1.5 - '@smithy/credential-provider-imds': 4.0.7 - '@smithy/node-config-provider': 4.1.4 - '@smithy/property-provider': 4.0.5 - '@smithy/smithy-client': 4.5.2 - '@smithy/types': 4.3.2 + '@smithy/config-resolver': 4.2.0 + '@smithy/credential-provider-imds': 4.1.0 + '@smithy/node-config-provider': 4.2.0 + '@smithy/property-provider': 4.1.0 + '@smithy/smithy-client': 4.6.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/util-endpoints@3.0.7': + '@smithy/util-endpoints@3.1.0': dependencies: - '@smithy/node-config-provider': 4.1.4 - '@smithy/types': 4.3.2 + '@smithy/node-config-provider': 4.2.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/util-hex-encoding@4.0.0': + '@smithy/util-hex-encoding@4.1.0': dependencies: tslib: 2.8.1 - '@smithy/util-middleware@4.0.5': + '@smithy/util-middleware@4.1.0': dependencies: - '@smithy/types': 4.3.2 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/util-retry@4.0.7': + '@smithy/util-retry@4.1.0': dependencies: - '@smithy/service-error-classification': 4.0.7 - '@smithy/types': 4.3.2 + '@smithy/service-error-classification': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/util-stream@4.2.4': + '@smithy/util-stream@4.3.0': dependencies: - '@smithy/fetch-http-handler': 5.1.1 - '@smithy/node-http-handler': 4.1.1 - '@smithy/types': 4.3.2 - '@smithy/util-base64': 4.0.0 - '@smithy/util-buffer-from': 4.0.0 - '@smithy/util-hex-encoding': 4.0.0 - '@smithy/util-utf8': 4.0.0 + '@smithy/fetch-http-handler': 5.2.0 + '@smithy/node-http-handler': 4.2.0 + '@smithy/types': 4.4.0 + '@smithy/util-base64': 4.1.0 + '@smithy/util-buffer-from': 4.1.0 + '@smithy/util-hex-encoding': 4.1.0 + '@smithy/util-utf8': 4.1.0 tslib: 2.8.1 - '@smithy/util-uri-escape@4.0.0': + '@smithy/util-uri-escape@4.1.0': dependencies: tslib: 2.8.1 @@ -4325,15 +4346,15 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.0.0': + '@smithy/util-utf8@4.1.0': dependencies: - '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-buffer-from': 4.1.0 tslib: 2.8.1 - '@smithy/util-waiter@4.0.7': + '@smithy/util-waiter@4.1.0': dependencies: - '@smithy/abort-controller': 4.0.5 - '@smithy/types': 4.3.2 + '@smithy/abort-controller': 4.1.0 + '@smithy/types': 4.4.0 tslib: 2.8.1 '@stablelib/base64@1.0.1': {} @@ -4366,7 +4387,7 @@ snapshots: - bufferutil - utf-8-validate - '@supabase/storage-js@2.11.0': + '@supabase/storage-js@2.11.1': dependencies: '@supabase/node-fetch': 2.6.15 @@ -4377,7 +4398,7 @@ snapshots: '@supabase/node-fetch': 2.6.15 '@supabase/postgrest-js': 1.21.3 '@supabase/realtime-js': 2.15.4 - '@supabase/storage-js': 2.11.0 + '@supabase/storage-js': 2.11.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -4405,14 +4426,14 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@20.19.12': + '@types/node@20.19.13': dependencies: undici-types: 6.21.0 '@types/nodemailer@6.4.19': dependencies: - '@aws-sdk/client-ses': 3.879.0 - '@types/node': 20.19.12 + '@aws-sdk/client-ses': 3.882.0 + '@types/node': 20.19.13 transitivePeerDependencies: - aws-crt @@ -4430,7 +4451,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.12 + '@types/node': 20.19.13 '@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.9.2))(eslint@9.34.0(jiti@1.21.7))(typescript@5.9.2)': dependencies: @@ -4769,6 +4790,10 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chart.js@4.5.0: + dependencies: + '@kurkle/color': 0.3.4 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -5912,6 +5937,11 @@ snapshots: transitivePeerDependencies: - debug + react-chartjs-2@5.3.0(chart.js@4.5.0)(react@19.1.1): + dependencies: + chart.js: 4.5.0 + react: 19.1.1 + react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -6220,7 +6250,7 @@ snapshots: stripe@16.12.0: dependencies: - '@types/node': 20.19.12 + '@types/node': 20.19.13 qs: 6.14.0 strnum@2.1.1: {} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index effa79c..d26f500 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,7 @@ // Updated schema.prisma with Payment Gateway Support generator client { provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] } datasource db { @@ -49,6 +50,7 @@ model Deck { user User @relation(fields: [userId], references: [clerkUserId]) @@index([userId]) + @@index([name]) } model Flashcard { @@ -82,6 +84,8 @@ model StudySession { // ✅ EXISTING: Relation points to clerkUserId user User @relation(fields: [userId], references: [clerkUserId]) + + @@index([userId, startTime]) } model StudyRecord { @@ -93,6 +97,10 @@ model StudyRecord { createdAt DateTime @default(now()) flashcard Flashcard @relation(fields: [flashcardId], references: [id]) studySession StudySession @relation(fields: [sessionId], references: [id]) + + @@index([flashcardId]) + @@index([sessionId]) + @@index([createdAt]) } // ✅ NEW: Payment tracking model diff --git a/src/app/(dashboard)/analytics/page.tsx b/src/app/(dashboard)/analytics/page.tsx new file mode 100644 index 0000000..5521e0b --- /dev/null +++ b/src/app/(dashboard)/analytics/page.tsx @@ -0,0 +1,131 @@ +import { currentUser } from "@clerk/nextjs/server"; +import { redirect } from "next/navigation"; +import { Filters } from "@/components/analytics/Filters"; +import { Suspense } from "react"; +import { LoadingSpinner } from "@/components/common/LoadingSpinner"; +import { RetentionCurveWrapper } from "@/components/analytics/RetentionCurveWrapper"; +import { SessionHeatmapWrapper } from "@/components/analytics/SessionHeatmapWrapper"; +import { SubjectProgressWrapper } from "@/components/analytics/SubjectProgressWrapper"; +import type { Metadata } from "next"; + +// Disable static generation for this page since it uses Clerk +export const dynamic = 'force-dynamic'; + +export const metadata: Metadata = { + title: "Analytics - Flash Fathom AI", + description: "In-depth performance analytics for your flashcard learning", +}; + +// Helper to build query string from filters +function buildQueryString(filters: { [key: string]: string | string[] | undefined }): string { + const params = new URLSearchParams(); + + Object.entries(filters).forEach(([key, value]) => { + if (value) { + if (Array.isArray(value)) { + // Handle array values - append each valid item + value.forEach((item) => { + const trimmedItem = item.trim(); + if (trimmedItem) { + params.append(key, trimmedItem); + } + }); + } else if (typeof value === 'string') { + // Handle string values - set if not empty + const trimmedValue = value.trim(); + if (trimmedValue) { + params.set(key, trimmedValue); + } + } + } + }); + + return params.toString(); +} + +// Fetch analytics data from API routes +async function fetchAnalyticsData(userId: string, filters: { [key: string]: string | string[] | undefined }) { + // Merge userId into filters for authentication/authorization + const filtersWithUserId = { ...filters, userId }; + const queryString = buildQueryString(filtersWithUserId); + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + + try { + const [subjectProgressRes, retentionCurveRes, sessionHeatmapRes] = await Promise.all([ + fetch(`${baseUrl}/api/analytics/subject-progress?${queryString}`, { + cache: 'no-store', + headers: { 'Content-Type': 'application/json' } + }), + fetch(`${baseUrl}/api/analytics/retention-curve?${queryString}`, { + cache: 'no-store', + headers: { 'Content-Type': 'application/json' } + }), + fetch(`${baseUrl}/api/analytics/session-heatmap?${queryString}`, { + cache: 'no-store', + headers: { 'Content-Type': 'application/json' } + }), + ]); + + const [subjectProgressData, retentionData, sessionData] = await Promise.all([ + subjectProgressRes.ok ? subjectProgressRes.json() : { labels: [], datasets: [] }, + retentionCurveRes.ok ? retentionCurveRes.json() : { labels: [], retention: [] }, + sessionHeatmapRes.ok ? sessionHeatmapRes.json() : Array.from({ length: 7 }, () => Array(24).fill(0)), + ]); + + return { subjectProgressData, retentionData, sessionData }; + } catch (error) { + console.error("Failed to fetch analytics data:", error); + // Return empty data on error + return { + subjectProgressData: { labels: [], datasets: [] }, + retentionData: { labels: [], retention: [] }, + sessionData: Array.from({ length: 7 }, () => Array(24).fill(0)), + }; + } +} + +export default async function AnalyticsPage({ + searchParams +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }> +}) { + const user = await currentUser(); + if (!user) { + redirect("/sign-in"); + } + + const resolvedSearchParams = await searchParams; + + // Fetch analytics data with filters + const { subjectProgressData, retentionData, sessionData } = await fetchAnalyticsData( + user.id, + resolvedSearchParams + ); + + return ( +
+

In-depth Performance Analytics

+ + + +
+ +
+ }> + + +
+
+ }> + + +
+
+ }> + + +
+
+
+ ); +} diff --git a/src/app/(dashboard)/generate/page.tsx b/src/app/(dashboard)/generate/page.tsx index ecf9164..c70e3dd 100644 --- a/src/app/(dashboard)/generate/page.tsx +++ b/src/app/(dashboard)/generate/page.tsx @@ -1,5 +1,8 @@ import Flashcard from "@/components/core/flash-card"; +// Disable static generation for this page since it uses Clerk +export const dynamic = 'force-dynamic'; + export default async function GeneratePage() { return (
diff --git a/src/app/api/analytics/retention-curve/route.ts b/src/app/api/analytics/retention-curve/route.ts new file mode 100644 index 0000000..fda4dff --- /dev/null +++ b/src/app/api/analytics/retention-curve/route.ts @@ -0,0 +1,183 @@ + +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/database"; +import { currentUser } from "@clerk/nextjs/server"; +import { Difficulty } from "@prisma/client"; +import { formatDateInTimezone, isValidTimezone } from "@/lib/utils/timezone"; + +// Helper function to safely map and validate difficulty parameter +function mapOrValidateDifficulty(difficultyParam: string | null): Difficulty | null { + if (!difficultyParam) return null; + + const upperDifficulty = difficultyParam.toUpperCase(); + if (upperDifficulty === "EASY") return Difficulty.EASY; + if (upperDifficulty === "MEDIUM") return Difficulty.MEDIUM; + if (upperDifficulty === "HARD") return Difficulty.HARD; + + return null; // Invalid difficulty +} + +export async function GET(req: NextRequest) { + const user = await currentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { + status: 401, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + }); + } + + const { searchParams } = new URL(req.url); + const subject = searchParams.get("subject")?.trim(); + const dateRange = searchParams.get("dateRange"); + const difficulty = searchParams.get("difficulty"); + const timezone = searchParams.get("timezone") || "UTC"; + + // Validate timezone parameter + if (!isValidTimezone(timezone)) { + return NextResponse.json( + { + error: "Invalid timezone. Must be a valid IANA timezone string (e.g., 'America/Los_Angeles', 'Europe/London', 'UTC')" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + + // Validate and normalize difficulty parameter + const safeDifficulty = mapOrValidateDifficulty(difficulty); + if (difficulty && !safeDifficulty) { + return NextResponse.json( + { + error: "Invalid difficulty value. Must be one of: EASY, MEDIUM, HARD" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + + // Parse and validate dateRange parameter + let dateFilter: { gte?: Date; lte?: Date } | undefined = undefined; + if (dateRange) { + try { + // Check if it's a single date or date range (comma-separated) + const dates = dateRange.split(',').map(d => d.trim()); + + if (dates.length === 1) { + // Single date - use as gte + const date = new Date(dates[0]); + if (isNaN(date.getTime())) { + throw new Error("Invalid date format"); + } + dateFilter = { gte: date }; + } else if (dates.length === 2) { + // Date range - use first as gte, second as lte + const startDate = new Date(dates[0]); + const endDate = new Date(dates[1]); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + throw new Error("Invalid date format"); + } + + if (startDate > endDate) { + return NextResponse.json( + { + error: "Start date must be before or equal to end date" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + + dateFilter = { gte: startDate, lte: endDate }; + } else { + throw new Error("Invalid date range format"); + } + } catch (error) { + return NextResponse.json( + { + error: "Invalid date format. Use ISO date format (YYYY-MM-DD) or comma-separated range (YYYY-MM-DD,YYYY-MM-DD)" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + } + + try { + const studyRecords = await prisma.studyRecord.findMany({ + where: { + flashcard: { + userId: user.id, + deck: subject ? { + is: { + name: { + equals: subject, + mode: "insensitive" + } + } + } : undefined, + difficulty: safeDifficulty ? { equals: safeDifficulty } : undefined, + }, + createdAt: dateFilter, + }, + orderBy: { createdAt: "asc" }, + select: { + createdAt: true, + isCorrect: true, + }, + }); + const retentionData = studyRecords.reduce((acc: { [key: string]: { correct: number; total: number } }, record: { createdAt: Date; isCorrect: boolean }) => { + // Format date in the user's timezone to ensure correct date grouping + const date = formatDateInTimezone(record.createdAt, timezone); + if (!acc[date]) { + acc[date] = { correct: 0, total: 0 }; + } + acc[date].total++; + if (record.isCorrect) { + acc[date].correct++; + } + return acc; + }, {} as { [key: string]: { correct: number; total: number } }); + + const labels = Object.keys(retentionData); + const retention = Object.values(retentionData).map((d: { correct: number; total: number }) => (d.correct / d.total) * 100); + + return NextResponse.json({ labels, retention }, { + status: 200, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + }, + }); + } catch (error) { + console.error("Failed to compute retention curve:", error); + return NextResponse.json( + { + error: "Failed to compute retention" + }, + { + status: 500, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + }, + } + ); + } +} diff --git a/src/app/api/analytics/review-interval-effectiveness/route.ts b/src/app/api/analytics/review-interval-effectiveness/route.ts new file mode 100644 index 0000000..e155f9c --- /dev/null +++ b/src/app/api/analytics/review-interval-effectiveness/route.ts @@ -0,0 +1,61 @@ +import { NextResponse } from 'next/server'; +import { auth } from '@clerk/nextjs/server'; +import { prisma } from '@/lib/database'; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const subject = searchParams.get('subject'); + const dateRange = searchParams.get('dateRange'); + + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const studyRecords = await prisma.studyRecord.findMany({ + where: { + flashcard: { + userId: userId, + ...(subject && { deck: { name: subject } }), + }, + ...(dateRange && { createdAt: { gte: new Date(dateRange) } }), + }, + orderBy: { + createdAt: 'asc', + }, + include: { + flashcard: true, + }, + }); + + const recordsByFlashcard = studyRecords.reduce((acc, record) => { + if (!acc[record.flashcardId]) { + acc[record.flashcardId] = []; + } + acc[record.flashcardId].push(record); + return acc; + }, {} as Record); + + const reviewIntervals = Object.values(recordsByFlashcard).flatMap(records => { + if (records.length < 2) { + return []; + } + + // Corrected logic: The 'index' in slice().map() is offset. + return records.slice(1).map((record, index) => { + // The previous record is at 'index' in the original 'records' array. + const previousRecord = records[index]; + const interval = (new Date(record.createdAt).getTime() - new Date(previousRecord.createdAt).getTime()) / (1000 * 3600 * 24); // Interval in days + return { + interval, + isCorrect: record.isCorrect, + }; + }); + }); + + return NextResponse.json(reviewIntervals); + } catch (error) { + console.error('[REVIEW_INTERVAL_API]', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/src/app/api/analytics/session-heatmap/route.ts b/src/app/api/analytics/session-heatmap/route.ts new file mode 100644 index 0000000..df7db9e --- /dev/null +++ b/src/app/api/analytics/session-heatmap/route.ts @@ -0,0 +1,102 @@ + +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/database"; +import { currentUser } from "@clerk/nextjs/server"; +import { getDayInTimezone, getHourInTimezone, isValidTimezone } from "@/lib/utils/timezone"; + +export async function GET(req: NextRequest) { + const user = await currentUser(); + if (!user) { + return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const subject = searchParams.get("subject")?.trim(); + const dateRange = searchParams.get("dateRange"); + const timezone = searchParams.get("timezone") || "UTC"; + + // Validate timezone parameter + if (!isValidTimezone(timezone)) { + return NextResponse.json( + { + error: "Invalid timezone. Must be a valid IANA timezone string (e.g., 'America/Los_Angeles', 'Europe/London', 'UTC')" + }, + { + status: 400, + headers: { + "cache-control": "no-store" + } + } + ); + } + + // Parse and validate dateRange parameter + let validatedDate: Date | undefined = undefined; + if (dateRange) { + try { + const parsedDate = new Date(dateRange); + if (!isFinite(parsedDate.getTime())) { + return NextResponse.json( + { error: "Invalid dateRange format. Use ISO date format (YYYY-MM-DD)" }, + { status: 400, headers: { "cache-control": "no-store" } } + ); + } + validatedDate = parsedDate; + } catch (error) { + return NextResponse.json( + { error: "Invalid dateRange format. Use ISO date format (YYYY-MM-DD)" }, + { status: 400, headers: { "cache-control": "no-store" } } + ); + } + } + + try { + const studySessions = await prisma.studySession.findMany({ + where: { + userId: user.id, + startTime: validatedDate ? { gte: validatedDate } : undefined, + records: subject + ? { + some: { + flashcard: { + deck: { + name: { + equals: subject, + mode: "insensitive" + } + }, + }, + }, + } + : undefined, + }, + select: { + startTime: true, + }, + }); + + const heatmapData = Array.from({ length: 7 }, () => Array(24).fill(0)); + + studySessions.forEach((session: { startTime: Date }) => { + // Get day and hour in the user's timezone to ensure correct heatmap placement + const day = getDayInTimezone(session.startTime, timezone); + const hour = getHourInTimezone(session.startTime, timezone); + if (heatmapData[day]) { + heatmapData[day][hour]++; + } + }); + + return new NextResponse(JSON.stringify(heatmapData), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Failed to fetch session heatmap data:", error); + return new NextResponse( + JSON.stringify({ + error: "Failed to fetch session heatmap data" + }), + { status: 500 } + ); + } +} diff --git a/src/app/api/analytics/subject-progress/route.ts b/src/app/api/analytics/subject-progress/route.ts new file mode 100644 index 0000000..55c5085 --- /dev/null +++ b/src/app/api/analytics/subject-progress/route.ts @@ -0,0 +1,232 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/database"; +import { currentUser } from "@clerk/nextjs/server"; +import { Difficulty } from "@prisma/client"; +import { formatDateInTimezone, isValidTimezone } from "@/lib/utils/timezone"; + +// Helper function to safely map and validate difficulty parameter +function mapOrValidateDifficulty(difficultyParam: string | null): Difficulty | null { + if (!difficultyParam) return null; + + const upperDifficulty = difficultyParam.toUpperCase(); + if (upperDifficulty === "EASY") return Difficulty.EASY; + if (upperDifficulty === "MEDIUM") return Difficulty.MEDIUM; + if (upperDifficulty === "HARD") return Difficulty.HARD; + + return null; // Invalid difficulty +} + +export async function GET(req: NextRequest) { + const user = await currentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { + status: 401, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + }); + } + + const { searchParams } = new URL(req.url); + const subject = searchParams.get("subject")?.trim(); + const dateRange = searchParams.get("dateRange"); + const difficulty = searchParams.get("difficulty"); + const timezone = searchParams.get("timezone") || "UTC"; + + // Validate timezone parameter + if (!isValidTimezone(timezone)) { + return NextResponse.json( + { + error: "Invalid timezone. Must be a valid IANA timezone string (e.g., 'America/Los_Angeles', 'Europe/London', 'UTC')" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + + // Validate and normalize difficulty parameter + const safeDifficulty = mapOrValidateDifficulty(difficulty); + if (difficulty && !safeDifficulty) { + return NextResponse.json( + { + error: "Invalid difficulty value. Must be one of: EASY, MEDIUM, HARD" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + + // Parse and validate dateRange parameter + let dateFilter: { gte?: Date; lte?: Date } | undefined = undefined; + if (dateRange) { + try { + const dates = dateRange.split(',').map(d => d.trim()); + + if (dates.length === 1) { + const date = new Date(dates[0]); + if (isNaN(date.getTime())) { + throw new Error("Invalid date format"); + } + dateFilter = { gte: date }; + } else if (dates.length === 2) { + const startDate = new Date(dates[0]); + const endDate = new Date(dates[1]); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + throw new Error("Invalid date format"); + } + + if (startDate > endDate) { + return NextResponse.json( + { + error: "Start date must be before or equal to end date" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + + dateFilter = { gte: startDate, lte: endDate }; + } else { + throw new Error("Invalid date range format"); + } + } catch (error) { + return NextResponse.json( + { + error: "Invalid date format. Use ISO date format (YYYY-MM-DD) or comma-separated range (YYYY-MM-DD,YYYY-MM-DD)" + }, + { + status: 400, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ); + } + } + + try { + // Fetch study records grouped by subject (deck name) + const studyRecords = await prisma.studyRecord.findMany({ + where: { + flashcard: { + userId: user.id, + deck: subject ? { + is: { + name: { + equals: subject, + mode: "insensitive" + } + } + } : undefined, + difficulty: safeDifficulty ? { equals: safeDifficulty } : undefined, + }, + createdAt: dateFilter, + }, + include: { + flashcard: { + include: { + deck: true, + } + }, + }, + orderBy: { createdAt: "asc" }, + }); + + // Group by deck and calculate progress over time + const subjectData: { [subject: string]: { [date: string]: { correct: number; total: number } } } = {}; + + studyRecords.forEach((record) => { + const deckName = record.flashcard.deck.name; + // Format date in the user's timezone to ensure correct date grouping + const date = formatDateInTimezone(record.createdAt, timezone); + + if (!subjectData[deckName]) { + subjectData[deckName] = {}; + } + + if (!subjectData[deckName][date]) { + subjectData[deckName][date] = { correct: 0, total: 0 }; + } + + subjectData[deckName][date].total++; + if (record.isCorrect) { + subjectData[deckName][date].correct++; + } + }); + + // Build lookup map for each subject: date -> accuracy + const subjectLookups = Object.entries(subjectData).map(([subject, dateData]) => { + const dateToAccuracy: { [date: string]: number } = {}; + + Object.entries(dateData).forEach(([date, stats]) => { + const { correct, total } = stats; + dateToAccuracy[date] = total > 0 ? (correct / total) * 100 : 0; + }); + + return { + label: subject, + dateToAccuracy, + dates: Object.keys(dateData), + }; + }); + + // If no data, return empty dataset + if (subjectLookups.length === 0) { + return NextResponse.json({ + labels: [], + datasets: [] + }, { + status: 200, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + }, + }); + } + + // Get all unique dates across all subjects + const allDates = [...new Set(subjectLookups.flatMap(d => d.dates))].sort(); + + // Build aligned datasets: each dataset.data has same length as allDates + const datasets = subjectLookups.map(({ label, dateToAccuracy }) => ({ + label, + data: allDates.map(date => dateToAccuracy[date] ?? null), + })); + + return NextResponse.json({ + labels: allDates, + datasets, + }, { + status: 200, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + }, + }); + } catch (error) { + console.error("Failed to compute subject progress:", error); + return NextResponse.json( + { + error: "Failed to compute subject progress" + }, + { + status: 500, + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate" + }, + } + ); + } +} + diff --git a/src/app/api/analytics/time-spent/route.ts b/src/app/api/analytics/time-spent/route.ts new file mode 100644 index 0000000..1325210 --- /dev/null +++ b/src/app/api/analytics/time-spent/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from 'next/server'; +import { auth } from '@clerk/nextjs/server'; +import { prisma } from '@/lib/database'; + +export async function GET(req: Request) { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const subject = searchParams.get('subject'); + const dateRange = searchParams.get('dateRange'); + + try { + const studySessions = await prisma.studySession.findMany({ + where: { + userId: userId, + ...(dateRange && { startTime: { gte: new Date(dateRange) } }), + ...(subject && { + records: { + some: { + flashcard: { + deck: { name: subject } + } + } + } + }) + }, + include: { + records: true, + }, + }); + + const timeSpentData = studySessions.map(session => { + const sessionDuration = session.endTime ? (new Date(session.endTime).getTime() - new Date(session.startTime).getTime()) / 60000 : 0; // Duration in minutes + const totalCards = session.records.length; + const averageTimePerCard = totalCards > 0 ? session.records.reduce((acc, record) => acc + record.timeSpent, 0) / totalCards : 0; // Average in seconds + + return { + date: session.startTime, + sessionDuration, + averageTimePerCard, + }; + }); + + return NextResponse.json(timeSpentData); + } catch (error) { + console.error('[TIME_SPENT_API]', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/src/app/api/checkout-sessions/route.ts b/src/app/api/checkout-sessions/route.ts index e0aa930..de79445 100644 --- a/src/app/api/checkout-sessions/route.ts +++ b/src/app/api/checkout-sessions/route.ts @@ -1,9 +1,17 @@ import { NextResponse } from "next/server"; import Stripe from "stripe"; -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2024-06-20", -}); +function getStripeInstance() { + const secretKey = process.env.STRIPE_SECRET_KEY; + + if (!secretKey) { + throw new Error('Stripe configuration is missing. Please set STRIPE_SECRET_KEY environment variable.'); + } + + return new Stripe(secretKey, { + apiVersion: "2024-06-20", + }); +} export async function POST(req: Request) { const { subscriptionType } = await req.json(); @@ -21,6 +29,7 @@ export async function POST(req: Request) { } try { + const stripe = getStripeInstance(); const session = await stripe.checkout.sessions.create({ mode: "subscription", payment_method_types: ["card"], @@ -53,6 +62,7 @@ export async function GET(req: Request) { } try { + const stripe = getStripeInstance(); const session = await stripe.checkout.sessions.retrieve(session_id); return NextResponse.json(session); } catch (error) { diff --git a/src/app/api/razorpay/create-order/route.ts b/src/app/api/razorpay/create-order/route.ts index b742047..3136013 100644 --- a/src/app/api/razorpay/create-order/route.ts +++ b/src/app/api/razorpay/create-order/route.ts @@ -3,11 +3,20 @@ import { auth } from '@clerk/nextjs/server'; import { prisma } from '@/lib/database'; import Razorpay from 'razorpay'; -// Initialize Razorpay -const razorpay = new Razorpay({ - key_id: process.env.RAZORPAY_KEY_ID!, - key_secret: process.env.RAZORPAY_KEY_SECRET!, -}); +// Initialize Razorpay function +function getRazorpayInstance() { + const keyId = process.env.RAZORPAY_KEY_ID; + const keySecret = process.env.RAZORPAY_KEY_SECRET; + + if (!keyId || !keySecret) { + throw new Error('Razorpay configuration is missing. Please set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET environment variables.'); + } + + return new Razorpay({ + key_id: keyId, + key_secret: keySecret, + }); +} // ✅ UPDATED: Complete pricing configuration const PRICING = { @@ -123,6 +132,7 @@ export async function POST(req: NextRequest) { // ✅ FIXED: Create Razorpay order with better error handling let order; try { + const razorpay = getRazorpayInstance(); order = await razorpay.orders.create({ amount: amount, currency: 'INR', diff --git a/src/app/flashcards/[id]/page.tsx b/src/app/flashcards/[id]/page.tsx index 9471ac7..96f919f 100644 --- a/src/app/flashcards/[id]/page.tsx +++ b/src/app/flashcards/[id]/page.tsx @@ -24,6 +24,9 @@ import { useUser } from "@clerk/nextjs"; import StudyMode from "@/components/study/StudyMode"; import { FlashcardSkeleton } from "@/components/ui/skeleton-cards"; +// Disable static generation for this page since it uses Clerk +export const dynamic = 'force-dynamic'; + export default function FlashcardSetPage() { const [flashcards, setFlashcards] = useState([]); const [loading, setLoading] = useState(true); diff --git a/src/app/flashcards/page.tsx b/src/app/flashcards/page.tsx index 0cd29c0..083b40c 100644 --- a/src/app/flashcards/page.tsx +++ b/src/app/flashcards/page.tsx @@ -4,11 +4,14 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { useUser } from "@clerk/nextjs"; import { Button } from "@/components/ui/button"; -import { Loader2, Plus } from "lucide-react"; +import { Loader2, Plus, BarChart2 } from "lucide-react"; import DeckList from "@/components/decks/DeckList"; import type { Deck } from "@/types"; import GlobalSearch from '@/components/search/GlobalSearch' +// Disable static generation for this page since it uses Clerk +export const dynamic = 'force-dynamic'; + // Single Responsibility: Manage flashcards overview page export default function FlashcardsPage() { const [decks, setDecks] = useState([]); @@ -66,11 +69,18 @@ export default function FlashcardsPage() {

Your Flashcards

- - + - +
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7acd3b6..4bfff3a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,10 +8,12 @@ import { ClerkProvider } from "@clerk/nextjs"; import { ThemeProvider } from "../components/ThemeProvider"; import Footer from "@/components/Footer"; import 'react-toastify/dist/ReactToastify.css' -import { ToastContainer } from 'react-toastify' import { Toaster } from "@/components/ui/toaster" import BacktoTop from "@/components/BacktoTop"; +// Disable static generation globally to avoid Clerk issues during build +export const dynamic = 'force-dynamic'; + const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { @@ -28,12 +30,33 @@ export default async function RootLayout({ // const headersList = await headers(); // const cookieStore = await cookies(); + // Validate Clerk publishable key + const clerkPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + + if (!clerkPublishableKey) { + if (process.env.NODE_ENV === 'production') { + throw new Error( + 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY environment variable is required in production. ' + + 'Please set this variable in your production environment.' + ); + } else { + // Development/test fallback with warning + console.warn( + '⚠️ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is not set. Using development fallback. ' + + 'Please set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env.local file for proper authentication.' + ); + } + } + + const publishableKey = clerkPublishableKey || 'pk_dev_fallback_please_set_env_var'; + return ( - diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..ed3fb43 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,34 @@ +'use client' + +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { Home, ArrowLeft } from 'lucide-react' + +export default function NotFound() { + return ( +
+
+
+

404

+

Page Not Found

+

+ The page you're looking for doesn't exist or has been moved. +

+
+ +
+ + +
+
+
+ ) +} diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index 0f9ab56..46de7b8 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -19,6 +19,9 @@ import { useTheme } from "next-themes"; // ✅ ADDED: Import toast functionality import { useToast } from "@/components/ui/use-toast"; +// Disable static generation for this page since it uses Clerk +export const dynamic = 'force-dynamic'; + // TypeScript interfaces interface Feature { text: string; diff --git a/src/app/result/page.tsx b/src/app/result/page.tsx index 7fc87c4..54b4ee4 100644 --- a/src/app/result/page.tsx +++ b/src/app/result/page.tsx @@ -9,6 +9,9 @@ import { Loader2, CreditCard, CheckCircle, XCircle } from 'lucide-react' import { Skeleton } from "@/components/ui/skeleton" import Link from 'next/link' +// Disable static generation for this page since it uses Clerk +export const dynamic = 'force-dynamic'; + interface Session { payment_status: string; // Add other session properties as needed diff --git a/src/components/analytics/DifficultyInsights.tsx b/src/components/analytics/DifficultyInsights.tsx new file mode 100644 index 0000000..ffcc43d --- /dev/null +++ b/src/components/analytics/DifficultyInsights.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { useSearchParams } from 'next/navigation'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +interface DifficultyData { + EASY: { correct: number; total: number }; + MEDIUM: { correct: number; total: number }; + HARD: { correct: number; total: number }; +} + +export function DifficultyInsights() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const searchParams = useSearchParams(); + + useEffect(() => { + const fetchData = async () => { + setError(null); + try { + const params = new URLSearchParams(searchParams.toString()); + const response = await fetch(`/api/analytics/difficulty-insights?${params.toString()}`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.details || 'Failed to fetch data'); + } + + const result = await response.json(); + setData(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'An unknown error occurred'); + } + }; + + fetchData(); + }, [searchParams]); + + if (error) { + return
Error: {error}
; + } + + if (!data) { + return
Loading...
; + } + + const chartData = { + labels: ['Easy', 'Medium', 'Hard'], + datasets: [ + { + label: 'Correct', + data: [data.EASY.correct, data.MEDIUM.correct, data.HARD.correct], + backgroundColor: 'rgba(75, 192, 192, 0.6)', + }, + { + label: 'Incorrect', + data: [ + data.EASY.total - data.EASY.correct, + data.MEDIUM.total - data.MEDIUM.correct, + data.HARD.total - data.HARD.correct, + ], + backgroundColor: 'rgba(255, 99, 132, 0.6)', + }, + ], + }; + + const chartOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Performance by Difficulty Level', + }, + tooltip: { + callbacks: { + label: function (context: any) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + const total = context.chart.data.datasets.reduce((acc: number, dataset: any) => acc + dataset.data[context.dataIndex], 0); + const value = context.raw; + const percentage = total > 0 ? ((value / total) * 100).toFixed(2) + '%' : '0%'; + return `${label}${value} (${percentage})`; + } + } + } + }, + scales: { + x: { + stacked: true, + }, + y: { + stacked: true, + }, + }, + }; + + return ; +} diff --git a/src/components/analytics/Filters.tsx b/src/components/analytics/Filters.tsx new file mode 100644 index 0000000..a62e44e --- /dev/null +++ b/src/components/analytics/Filters.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useEffect } from 'react'; + +export function Filters() { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Automatically detect and set user's timezone only if not already set + useEffect(() => { + const currentTimezone = searchParams.get('timezone'); + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // Only set timezone if it's absent, preserving user selections on remounts + if (!currentTimezone) { + const params = new URLSearchParams(searchParams.toString()); + params.set('timezone', userTimezone); + router.replace(`?${params.toString()}`); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Run once on mount - intentionally empty to avoid infinite loops + + const handleFilterChange = (event: React.ChangeEvent) => { + const params = new URLSearchParams(searchParams.toString()); + params.set(event.target.name, event.target.value); + router.push(`?${params.toString()}`); + }; + + return ( +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/analytics/Recommendations.tsx b/src/components/analytics/Recommendations.tsx new file mode 100644 index 0000000..2d18ce9 --- /dev/null +++ b/src/components/analytics/Recommendations.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface RecommendationData { + bestTimeToStudy: number; + urgentRevisionDecks: { name: string; retention: number }[]; +} + +export function Recommendations() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch(`/api/analytics/recommendations`); + if (!response.ok) { + throw new Error('Failed to fetch recommendations'); + } + const result = await response.json(); + setData(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'An unknown error occurred'); + } + }; + + fetchData(); + }, []); + + if (error) { + return
Error: {error}
; + } + + if (!data) { + return
Loading recommendations...
; + } + + const formatHour = (hour: number) => { + if (hour === -1) return "Not enough data"; + const ampm = hour >= 12 ? 'PM' : 'AM'; + const formattedHour = hour % 12 || 12; + return `${formattedHour} ${ampm}`; + } + + return ( +
+

Personalized Recommendations

+
+
+

Best Time to Study

+

{formatHour(data.bestTimeToStudy)}

+
+
+

Subjects for Urgent Revision

+
    + {data.urgentRevisionDecks.map(deck => ( +
  • {deck.name} ({deck.retention.toFixed(2)}% retention)
  • + ))} +
+
+
+
+ ); +} diff --git a/src/components/analytics/RetentionCurve.tsx b/src/components/analytics/RetentionCurve.tsx new file mode 100644 index 0000000..d7268d9 --- /dev/null +++ b/src/components/analytics/RetentionCurve.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +interface RetentionCurveProps { + data: { + labels: string[]; + retention: number[]; + }; +} + +export function RetentionCurve({ data }: RetentionCurveProps) { + // Transform data for chart + const chartData = { + labels: data.labels.length > 0 ? data.labels : ['No data'], + datasets: [ + { + label: 'Retention Rate', + data: data.retention.length > 0 ? data.retention : [0], + fill: false, + borderColor: data.retention.length > 0 ? 'rgb(75, 192, 192)' : 'rgb(200, 200, 200)', + backgroundColor: 'rgba(75, 192, 192, 0.5)', + tension: 0.1, + }, + ], + }; + + const options = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Retention Rate Over Time', + }, + }, + scales: { + y: { + min: 0, + max: 100, + ticks: { + callback: function(value: number | string) { + return value + '%'; + } + } + } + } + }; + + return ; +} diff --git a/src/components/analytics/RetentionCurveWrapper.tsx b/src/components/analytics/RetentionCurveWrapper.tsx new file mode 100644 index 0000000..efc1985 --- /dev/null +++ b/src/components/analytics/RetentionCurveWrapper.tsx @@ -0,0 +1,13 @@ +import { RetentionCurve } from './RetentionCurve'; + +interface RetentionCurveWrapperProps { + data: { + labels: string[]; + retention: number[]; + }; +} + +export function RetentionCurveWrapper({ data }: RetentionCurveWrapperProps) { + return ; +} + diff --git a/src/components/analytics/ReviewIntervalEffectiveness.tsx b/src/components/analytics/ReviewIntervalEffectiveness.tsx new file mode 100644 index 0000000..f6031d5 --- /dev/null +++ b/src/components/analytics/ReviewIntervalEffectiveness.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Scatter } from 'react-chartjs-2'; +import { + Chart as ChartJS, + LinearScale, + PointElement, + LineElement, + Tooltip, + Legend, +} from 'chart.js'; +import { useSearchParams } from 'next/navigation'; + +ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend); + +interface ReviewIntervalData { + interval: number; + isCorrect: boolean; +} + +export function ReviewIntervalEffectiveness() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const searchParams = useSearchParams(); + + useEffect(() => { + const fetchData = async () => { + try { + const params = new URLSearchParams(searchParams.toString()); + const response = await fetch(`/api/analytics/review-interval-effectiveness?${params.toString()}`); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = await response.json(); + setData(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'An unknown error occurred'); + } + }; + + fetchData(); + }, [searchParams]); + + if (error) { + return
Error: {error}
; + } + + if (!data) { + return
Loading...
; + } + + const chartData = { + datasets: [ + { + label: 'Correct', + data: data.filter(d => d.isCorrect).map(d => ({ x: d.interval, y: 1 })), + backgroundColor: 'rgba(75, 192, 192, 0.6)', + }, + { + label: 'Incorrect', + data: data.filter(d => !d.isCorrect).map(d => ({ x: d.interval, y: 0 })), + backgroundColor: 'rgba(255, 99, 132, 0.6)', + }, + ], + }; + + const chartOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Review Interval Effectiveness', + }, + tooltip: { + callbacks: { + label: function (context: any) { + return `Interval: ${context.raw.x.toFixed(2)} days`; + } + } + } + }, + scales: { + x: { + title: { + display: true, + text: 'Review Interval (days)', + }, + }, + y: { + title: { + display: true, + text: 'Outcome (1 = Correct, 0 = Incorrect)', + }, + min: -0.1, + max: 1.1, + ticks: { + stepSize: 1, + } + }, + }, + }; + + return ; +} diff --git a/src/components/analytics/SessionHeatmap.tsx b/src/components/analytics/SessionHeatmap.tsx new file mode 100644 index 0000000..f712ebe --- /dev/null +++ b/src/components/analytics/SessionHeatmap.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + TooltipItem, +} from 'chart.js'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +interface SessionHeatmapProps { + data: number[][]; // 7 days x 24 hours +} + +const dayLabels = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + +export function SessionHeatmap({ data }: SessionHeatmapProps) { + // Validate input + if (!data || data.length !== 7) { + console.error('SessionHeatmap: expected data with 7 days'); + return null; + } + if (data.some(dayData => !Array.isArray(dayData) || dayData.length !== 24)) { + console.error('SessionHeatmap: expected each day to have 24 hours'); + return null; + } + + // Calculate total sessions per day + const dailyTotals = data.map((dayData, dayIndex) => ({ + day: dayLabels[dayIndex], + total: dayData.reduce((sum, val) => sum + val, 0), + })); + // Create chart data + const chartData = { + labels: dailyTotals.map(d => d.day), + datasets: [ + { + label: 'Study Sessions', + data: dailyTotals.map(d => d.total), + backgroundColor: 'rgba(75, 192, 192, 0.5)', + borderColor: 'rgb(75, 192, 192)', + borderWidth: 1, + }, + ], + }; + + const options = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Study Sessions by Day of Week', + }, + tooltip: { + callbacks: { + label: function(context: TooltipItem<'bar'>) { + const dayIndex = context.dataIndex; + const dayData = data[dayIndex]; + + // Defensive checks for dayData + if (!dayData || !Array.isArray(dayData) || dayData.length === 0) { + return [ + `Sessions: ${context.parsed?.y ?? 0}`, + `Peak hour: N/A`, + ]; + } + + // Compute peak hour only when we have valid data + const maxValue = Math.max(...dayData); + const maxIndex = dayData.indexOf(maxValue); + const peakHour = maxIndex >= 0 ? `${maxIndex}:00` : 'N/A'; + + return [ + `Sessions: ${context.parsed?.y ?? 0}`, + `Peak hour: ${peakHour}`, + ]; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1, + } + } + } + }; + + return ; +} diff --git a/src/components/analytics/SessionHeatmapWrapper.tsx b/src/components/analytics/SessionHeatmapWrapper.tsx new file mode 100644 index 0000000..a544a77 --- /dev/null +++ b/src/components/analytics/SessionHeatmapWrapper.tsx @@ -0,0 +1,10 @@ +import { SessionHeatmap } from './SessionHeatmap'; + +interface SessionHeatmapWrapperProps { + data: number[][]; +} + +export function SessionHeatmapWrapper({ data }: SessionHeatmapWrapperProps) { + return ; +} + diff --git a/src/components/analytics/SubjectProgress.tsx b/src/components/analytics/SubjectProgress.tsx new file mode 100644 index 0000000..4c4e37c --- /dev/null +++ b/src/components/analytics/SubjectProgress.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +interface SubjectProgressProps { + data: { + labels: string[]; + datasets: Array<{ + label: string; + data: number[]; + }>; + }; +} + +// Color palette for different subjects +const colors = [ + 'rgb(75, 192, 192)', + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(153, 102, 255)', + 'rgb(255, 159, 64)', +]; + +export function SubjectProgress({ data }: SubjectProgressProps) { + // Transform data for chart + const chartData = { + labels: data.labels.length > 0 ? data.labels : ['No data'], + datasets: data.datasets.length > 0 + ? data.datasets.map((dataset, index) => ({ + label: dataset.label, + data: dataset.data, + fill: false, + borderColor: colors[index % colors.length], + tension: 0.1, + })) + : [{ + label: 'No data available', + data: [0], + fill: false, + borderColor: 'rgb(200, 200, 200)', + tension: 0.1, + }], + }; + + const options = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Subject Progress (Accuracy %)', + }, + }, + scales: { + y: { + min: 0, + max: 100, + ticks: { + callback: function(value: number | string) { + return value + '%'; + } + } + } + } + }; + + return ; +} diff --git a/src/components/analytics/SubjectProgressWrapper.tsx b/src/components/analytics/SubjectProgressWrapper.tsx new file mode 100644 index 0000000..caf2369 --- /dev/null +++ b/src/components/analytics/SubjectProgressWrapper.tsx @@ -0,0 +1,16 @@ +import { SubjectProgress } from './SubjectProgress'; + +interface SubjectProgressWrapperProps { + data: { + labels: string[]; + datasets: Array<{ + label: string; + data: number[]; + }>; + }; +} + +export function SubjectProgressWrapper({ data }: SubjectProgressWrapperProps) { + return ; +} + diff --git a/src/components/analytics/TimeSpentAnalysis.tsx b/src/components/analytics/TimeSpentAnalysis.tsx new file mode 100644 index 0000000..993fc40 --- /dev/null +++ b/src/components/analytics/TimeSpentAnalysis.tsx @@ -0,0 +1,127 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { useSearchParams } from 'next/navigation'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +interface TimeSpentData { + date: string; + sessionDuration: number; + averageTimePerCard: number; +} + +export function TimeSpentAnalysis() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const searchParams = useSearchParams(); + + useEffect(() => { + const fetchData = async () => { + try { + const params = new URLSearchParams(searchParams.toString()); + const response = await fetch(`/api/analytics/time-spent?${params.toString()}`); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = await response.json(); + setData(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'An unknown error occurred'); + } + }; + + fetchData(); + }, [searchParams]); + + if (error) { + return
Error: {error}
; + } + + if (!data) { + return
Loading...
; + } + + const chartData = { + labels: data.map(d => new Date(d.date).toLocaleDateString()), + datasets: [ + { + label: 'Session Duration (minutes)', + data: data.map(d => d.sessionDuration), + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + yAxisID: 'y', + }, + { + label: 'Avg. Time per Card (seconds)', + data: data.map(d => d.averageTimePerCard), + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + yAxisID: 'y1', + }, + ], + }; + + const chartOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Time Spent Analysis', + }, + }, + scales: { + x: { + title: { + display: true, + text: 'Date' + } + }, + y: { + type: 'linear' as const, + display: true, + position: 'left' as const, + title: { + display: true, + text: 'Session Duration (minutes)' + } + }, + y1: { + type: 'linear' as const, + display: true, + position: 'right' as const, + title: { + display: true, + text: 'Avg. Time per Card (seconds)' + }, + grid: { + drawOnChartArea: false, // only draw grid lines for the first Y axis + }, + }, + }, + }; + + return ; +} diff --git a/src/components/core/flash-card.tsx b/src/components/core/flash-card.tsx index 185167e..35ca66c 100644 --- a/src/components/core/flash-card.tsx +++ b/src/components/core/flash-card.tsx @@ -67,12 +67,18 @@ export default function Flashcard() { )} )} - - - + - + diff --git a/src/components/flashcards/FlashcardGenerator.tsx b/src/components/flashcards/FlashcardGenerator.tsx index 1e6b3d8..cd54bd7 100644 --- a/src/components/flashcards/FlashcardGenerator.tsx +++ b/src/components/flashcards/FlashcardGenerator.tsx @@ -51,7 +51,8 @@ export default function FlashcardGenerator({ }); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const errorData = await response.json(); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.error}`); } const data = await response.json(); @@ -104,4 +105,4 @@ export default function FlashcardGenerator({ ); -} +} \ No newline at end of file diff --git a/src/lib/utils/timezone.ts b/src/lib/utils/timezone.ts new file mode 100644 index 0000000..21980c0 --- /dev/null +++ b/src/lib/utils/timezone.ts @@ -0,0 +1,49 @@ +import { format, toZonedTime } from 'date-fns-tz'; + +/** + * Formats a date to YYYY-MM-DD in the specified timezone + * @param date - The date to format + * @param timezone - IANA timezone string (e.g., 'America/Los_Angeles', 'UTC') + * @returns Date string in YYYY-MM-DD format in the user's timezone + */ +export function formatDateInTimezone(date: Date, timezone: string): string { + const zonedDate = toZonedTime(date, timezone); + return format(zonedDate, 'yyyy-MM-dd', { timeZone: timezone }); +} + +/** + * Gets the day of week (0-6) for a date in the specified timezone + * @param date - The date + * @param timezone - IANA timezone string + * @returns Day of week (0 = Sunday, 6 = Saturday) + */ +export function getDayInTimezone(date: Date, timezone: string): number { + const zonedDate = toZonedTime(date, timezone); + return zonedDate.getDay(); +} + +/** + * Gets the hour (0-23) for a date in the specified timezone + * @param date - The date + * @param timezone - IANA timezone string + * @returns Hour (0-23) + */ +export function getHourInTimezone(date: Date, timezone: string): number { + const zonedDate = toZonedTime(date, timezone); + return zonedDate.getHours(); +} + +/** + * Validates a timezone string + * @param timezone - The timezone to validate + * @returns true if valid, false otherwise + */ +export function isValidTimezone(timezone: string): boolean { + try { + Intl.DateTimeFormat(undefined, { timeZone: timezone }); + return true; + } catch { + return false; + } +} +