Update signup success message and clean up workspace
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
53
Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Set environment variables for build
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
16
Dockerfile.dev
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Set memory limit to prevent Next.js dev server from eating RAM
|
||||
ENV NODE_OPTIONS="--max-old-space-size=512"
|
||||
|
||||
# Start the Next.js development server
|
||||
CMD ["npm", "run", "dev"]
|
||||
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: pragativeer_landing
|
||||
ports:
|
||||
- "3001:3000"
|
||||
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
15
next.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: "http://backend:8000/api/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
8961
package-lock.json
generated
Normal file
46
package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "pragativeer-v2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-datepicker": "^6.2.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.4.0",
|
||||
"framer-motion": "^12.41.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"idb-keyval": "^6.2.6",
|
||||
"katex": "^0.17.0",
|
||||
"lucide-react": "^1.21.0",
|
||||
"next": "16.2.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.4",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react-katex": "^3.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^3.9.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"zod": "^4.4.3",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.9",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 450 KiB |
BIN
public/PragatiVeer_Logo/PNG_Logos/Square_dark_background.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
public/PragatiVeer_Logo/PNG_Logos/Square_light_background.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/course_business.png
Normal file
|
After Width: | Height: | Size: 440 KiB |
BIN
public/course_design.png
Normal file
|
After Width: | Height: | Size: 581 KiB |
BIN
public/course_tech.png
Normal file
|
After Width: | Height: | Size: 524 KiB |
BIN
public/dpps/dps-bio-ch4-20260707.pdf
Normal file
BIN
public/dpps/dps-che-ch4-20260707.pdf
Normal file
BIN
public/dpps/dps-mth-ch4-20260707.pdf
Normal file
BIN
public/dpps/dps-phy-ch4-20260707.pdf
Normal file
1
public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
449
public/streak-animation.html
Normal file
@@ -0,0 +1,449 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PRAGATIVEER · Reveal</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--navy-900:#000000; --navy-800:#000000; --navy-700:#000000;
|
||||
--gold-200:#fdeeb8; --gold-300:#f6dd93; --gold-400:#e8c463; --gold-500:#d4af37;
|
||||
--gold-600:#b1892a; --gold-700:#7d5f18;
|
||||
--ink:#e9eefb; --badge:240px; --grow:1.18; --halo-w:170%;
|
||||
--font:"Space Grotesk", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0; font-family:var(--font); color:var(--ink);
|
||||
background:radial-gradient(120% 90% at 50% -10%, #000000 0%, var(--navy-800) 42%, var(--navy-900) 100%);
|
||||
display:flex; align-items:center; justify-content:center; overflow:hidden;
|
||||
-webkit-font-smoothing:antialiased; transition:background .6s ease;
|
||||
}
|
||||
/* sad sky */
|
||||
body.sad{background:radial-gradient(120% 90% at 50% -10%, #000000 0%, #000000 46%, #000000 100%)}
|
||||
body::after{
|
||||
content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
|
||||
background:radial-gradient(70% 60% at 50% 44%, transparent 55%, rgba(0,0,0,.45) 100%);
|
||||
mix-blend-mode:multiply;
|
||||
}
|
||||
|
||||
#fw{position:fixed; inset:0; z-index:5; pointer-events:none}
|
||||
|
||||
/* euphoric gold flash */
|
||||
.flash{position:fixed; inset:0; z-index:6; pointer-events:none; opacity:0;
|
||||
background:radial-gradient(circle at 50% 46%, rgba(255,240,190,.6), rgba(212,175,55,.15) 45%, transparent 70%)}
|
||||
.stage.play.tier3 .flash{animation:flash 760ms ease-out 2600ms}
|
||||
|
||||
.stage{
|
||||
position:relative; z-index:10; width:min(92vw,560px); height:100dvh;
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
gap:26px; padding:24px; text-align:center;
|
||||
}
|
||||
|
||||
.scene{
|
||||
position:relative; width:var(--badge); height:var(--badge);
|
||||
perspective:1300px; perspective-origin:50% 46%;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.stage.play.tier3 .scene{animation:shake 520ms ease-out 2620ms}
|
||||
|
||||
.halo{
|
||||
position:absolute; top:50%; left:50%; width:var(--halo-w); aspect-ratio:1;
|
||||
transform:translate(-50%,-50%) scale(.3); opacity:0; border-radius:50%; filter:blur(4px); z-index:0;
|
||||
background:radial-gradient(circle,
|
||||
rgba(246,221,147,.55) 0%, rgba(212,175,55,.35) 34%, rgba(212,175,55,.08) 58%, transparent 72%);
|
||||
}
|
||||
.stage.play.happy .halo{animation:bloom 850ms cubic-bezier(.2,.8,.3,1) 1850ms forwards, pulse 2.8s ease-in-out 2700ms infinite}
|
||||
.stage.sad .halo{
|
||||
background:radial-gradient(circle, rgba(100,100,100,.30) 0%, rgba(80,80,80,.14) 42%, transparent 70%);
|
||||
}
|
||||
.stage.play.sad .halo{animation:bloomcold 1000ms ease-out 1900ms forwards}
|
||||
|
||||
.scaler{position:relative; width:100%; height:100%; z-index:1; transform:scale(1)}
|
||||
.stage.play.happy .scaler{animation:grow 680ms cubic-bezier(.34,1.56,.5,1) 2050ms forwards, litglow 700ms ease-out 1900ms forwards}
|
||||
.stage.play.sad .scaler{animation:droop 950ms cubic-bezier(.3,.7,.4,1) 2050ms forwards, dimglow 800ms ease-out 1900ms forwards}
|
||||
|
||||
.spinner{position:absolute; inset:0; will-change:transform; transform:rotateY(0deg) scale(0)}
|
||||
.stage.play .spinner{animation:spin 1800ms cubic-bezier(.14,.62,.2,1) 150ms forwards}
|
||||
|
||||
.badge{
|
||||
position:absolute; inset:0; border-radius:50%;
|
||||
background: url('/PragatiVeer_Logo/SVG_Logos/Square_dark_background.svg') center/cover no-repeat !important;
|
||||
}
|
||||
.stage.sad .badge{
|
||||
position:absolute; inset:0; border-radius:50%;
|
||||
background: url('/PragatiVeer_Logo/SVG_Logos/Square_dark_background.svg') center/cover no-repeat !important;
|
||||
}
|
||||
|
||||
.count{
|
||||
position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; gap:6px;
|
||||
opacity:0; transform:translateY(14px) scale(.9);
|
||||
}
|
||||
.stage.play .count{animation:reveal 620ms cubic-bezier(.34,1.4,.5,1) 2650ms forwards}
|
||||
.number{
|
||||
font-size:clamp(56px,17vw,88px); font-weight:700; line-height:.92; letter-spacing:-.02em;
|
||||
background:linear-gradient(180deg,var(--gold-200),var(--gold-500) 55%,var(--gold-600));
|
||||
-webkit-background-clip:text; background-clip:text; color:transparent;
|
||||
font-variant-numeric:tabular-nums; filter:drop-shadow(0 4px 18px rgba(212,175,55,.35));
|
||||
}
|
||||
.stage.sad .number{
|
||||
background:linear-gradient(180deg,#c7d0e0,#8b97ad 58%,#68738a);
|
||||
-webkit-background-clip:text; background-clip:text; filter:drop-shadow(0 3px 12px rgba(80,105,140,.3));
|
||||
}
|
||||
.caption{
|
||||
font-size:12px; font-weight:600; letter-spacing:.34em; text-transform:uppercase;
|
||||
color:var(--gold-400); text-indent:.34em; min-height:14px; transition:color .5s ease;
|
||||
}
|
||||
.stage.sad .caption{color:#95a2ba}
|
||||
|
||||
.panel{
|
||||
position:absolute; bottom:22px; left:50%; transform:translateX(-50%);
|
||||
display:flex; gap:8px; align-items:center; flex-wrap:wrap; justify-content:center;
|
||||
background:rgba(9,20,40,.55); border:1px solid rgba(232,196,99,.22);
|
||||
padding:8px 8px 8px 14px; border-radius:14px; backdrop-filter:blur(8px); box-shadow:0 10px 30px rgba(0,0,0,.4);
|
||||
}
|
||||
.panel label{font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:rgba(233,238,251,.55)}
|
||||
.panel input{
|
||||
width:110px; background:rgba(255,255,255,.06); border:1px solid rgba(232,196,99,.25);
|
||||
color:var(--ink); font-family:var(--font); font-size:14px; font-weight:500; padding:8px 10px; border-radius:9px; outline:none;
|
||||
}
|
||||
.panel input.cap{width:104px; text-transform:uppercase; letter-spacing:.1em; font-size:12px}
|
||||
.panel input:focus{border-color:var(--gold-400); box-shadow:0 0 0 3px rgba(212,175,55,.18)}
|
||||
.replay{
|
||||
border:none; cursor:pointer; font-family:var(--font); font-weight:600; font-size:13px; letter-spacing:.06em;
|
||||
color:#20130a; padding:9px 16px; border-radius:9px;
|
||||
background:linear-gradient(180deg,var(--gold-300),var(--gold-500)); box-shadow:0 4px 14px rgba(212,175,55,.35);
|
||||
transition:transform .12s ease, box-shadow .12s ease;
|
||||
}
|
||||
.replay:hover{transform:translateY(-1px); box-shadow:0 7px 18px rgba(212,175,55,.45)}
|
||||
.replay:active{transform:translateY(0)}
|
||||
.replay:focus-visible, .panel input:focus-visible{outline:2px solid var(--gold-300); outline-offset:2px}
|
||||
.hint{font-size:10.5px; color:rgba(233,238,251,.4); letter-spacing:.02em; margin-top:2px}
|
||||
|
||||
@media (max-width:460px){ :root{--badge:180px} .panel input{width:94px} .panel input.cap{width:88px} }
|
||||
|
||||
@keyframes spin{ 0%{transform:rotateY(0deg) scale(0)} 100%{transform:rotateY(1440deg) scale(1)} }
|
||||
@keyframes bloom{
|
||||
0%{opacity:0; transform:translate(-50%,-50%) scale(.3)}
|
||||
60%{opacity:1; transform:translate(-50%,-50%) scale(1.06)}
|
||||
100%{opacity:.9; transform:translate(-50%,-50%) scale(1)}
|
||||
}
|
||||
@keyframes bloomcold{
|
||||
0%{opacity:0; transform:translate(-50%,-50%) scale(.3)}
|
||||
100%{opacity:.5; transform:translate(-50%,-50%) scale(.82)}
|
||||
}
|
||||
@keyframes pulse{ 0%,100%{opacity:.75; transform:translate(-50%,-50%) scale(1)} 50%{opacity:1; transform:translate(-50%,-50%) scale(1.05)} }
|
||||
@keyframes grow{ 0%{transform:scale(1)} 70%{transform:scale(calc(var(--grow) + .06))} 100%{transform:scale(var(--grow))} }
|
||||
@keyframes droop{ 0%{transform:scale(1) translateY(0)} 100%{transform:scale(.93) translateY(9px)} }
|
||||
@keyframes litglow{ 0%{filter:drop-shadow(0 0 0 rgba(212,175,55,0))} 100%{filter:drop-shadow(0 0 26px rgba(233,196,99,.6))} }
|
||||
@keyframes dimglow{ 0%{filter:drop-shadow(0 0 0 rgba(120,150,190,0))} 100%{filter:drop-shadow(0 6px 16px rgba(20,30,45,.5))} }
|
||||
@keyframes reveal{ 0%{opacity:0; transform:translateY(14px) scale(.9)} 100%{opacity:1; transform:translateY(0) scale(1)} }
|
||||
@keyframes flash{ 0%{opacity:0} 18%{opacity:.85} 100%{opacity:0} }
|
||||
@keyframes shake{
|
||||
0%,100%{transform:translate(0,0)} 20%{transform:translate(-4px,2px)} 40%{transform:translate(4px,-2px)}
|
||||
60%{transform:translate(-3px,-1px)} 80%{transform:translate(3px,1px)}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
.stage.play .spinner{animation:none; transform:rotateY(0) scale(1)}
|
||||
.stage.play.happy .scaler{animation:none; transform:scale(var(--grow)); filter:drop-shadow(0 0 26px rgba(233,196,99,.6))}
|
||||
.stage.play.sad .scaler{animation:none; transform:scale(.93) translateY(9px)}
|
||||
.stage.play.happy .halo{animation:none; opacity:.9; transform:translate(-50%,-50%) scale(1)}
|
||||
.stage.play.sad .halo{animation:none; opacity:.5; transform:translate(-50%,-50%) scale(.82)}
|
||||
.stage.play .count{animation:none; opacity:1; transform:none}
|
||||
.stage.play.tier3 .scene,.stage.play.tier3 .flash{animation:none}
|
||||
}
|
||||
|
||||
body, html {
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
body.sad {
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
body::after {
|
||||
display: none !important;
|
||||
}
|
||||
.stage {
|
||||
height: 100vh !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.count, .panel, .hint {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<canvas id="fw"></canvas>
|
||||
<div class="flash"></div>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
<div class="scene">
|
||||
<div class="halo"></div>
|
||||
<div class="scaler"><div class="spinner"><div class="badge"></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="count">
|
||||
<div class="number" id="number">0</div>
|
||||
<div class="caption" id="caption">GROWTH SCORE</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<label for="num">Number</label>
|
||||
<input id="num" type="text" inputmode="numeric" value="2,026" aria-label="Target number">
|
||||
<input id="cap" class="cap" type="text" value="GROWTH SCORE" aria-label="Caption" maxlength="16">
|
||||
<button class="replay" id="replay">Replay</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint" style="position:absolute;bottom:6px;left:50%;transform:translateX(-50%);z-index:11">
|
||||
under 5 = sad · higher = bigger celebration
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
let isLight = false;
|
||||
try {
|
||||
isLight = !window.parent.document.documentElement.classList.contains('dark');
|
||||
} catch (e) {
|
||||
const p = new URLSearchParams(window.location.search);
|
||||
isLight = p.get('theme') === 'light';
|
||||
}
|
||||
window.isLightTheme = isLight;
|
||||
})();
|
||||
|
||||
const stage=document.getElementById('stage');
|
||||
const body=document.body;
|
||||
const numEl=document.getElementById('number');
|
||||
const capEl=document.getElementById('caption');
|
||||
const numInput=document.getElementById('num');
|
||||
const capInput=document.getElementById('cap');
|
||||
const replay=document.getElementById('replay');
|
||||
const reduce=window.matchMedia('(prefers-reduced-motion:reduce)');
|
||||
|
||||
/* ---------- particle canvas (fireworks + rain + confetti) ---------- */
|
||||
const cv=document.getElementById('fw'), ctx=cv.getContext('2d');
|
||||
let W,H,DPR;
|
||||
function resize(){
|
||||
DPR=Math.min(2,window.devicePixelRatio||1);
|
||||
W=cv.width=Math.floor(innerWidth*DPR); H=cv.height=Math.floor(innerHeight*DPR);
|
||||
cv.style.width=innerWidth+'px'; cv.style.height=innerHeight+'px';
|
||||
}
|
||||
resize(); addEventListener('resize',resize);
|
||||
|
||||
const SPARK=window.isLightTheme?['#0891b2','#2563eb','#db2777','#ea580c','#7c3aed','#059669']:['#f6dd93','#e8c463','#d4af37','#fff4cf','#ffffff','#8fb4ff'];
|
||||
const CONF=window.isLightTheme?['#0891b2','#2563eb','#db2777','#ea580c','#7c3aed','#059669','#d97706']:['#f6dd93','#e8c463','#d4af37','#ffffff','#ffd34d'];
|
||||
let parts=[], running=false, mode='fireworks', rafFW=null, fwTimers=[], fadeCol=(window.isLightTheme ? 'rgba(255,255,255,0.22)' : 'rgba(0,0,0,0.22)');
|
||||
|
||||
function burst(xr,yr,count){
|
||||
const x=xr*W, y=yr*H, n=count|0;
|
||||
for(let i=0;i<n;i++){
|
||||
const a=Math.PI*2*(i/n)+Math.random()*0.25, sp=(2+Math.random()*4.4)*DPR;
|
||||
parts.push({t:'spark', x,y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-1.1*DPR,
|
||||
life:1, decay:0.010+Math.random()*0.013, col:SPARK[(Math.random()*SPARK.length)|0], size:(1.3+Math.random()*1.9)*DPR});
|
||||
}
|
||||
}
|
||||
function confetti(m){
|
||||
for(let i=0;i<m;i++){
|
||||
parts.push({t:'conf', x:(0.3+Math.random()*0.4)*W, y:-20*DPR,
|
||||
vx:(-1.2+Math.random()*2.4)*DPR, vy:(1.5+Math.random()*2.5)*DPR,
|
||||
rot:Math.random()*7, vr:(-0.2+Math.random()*0.4), life:1,
|
||||
w:(4+Math.random()*4)*DPR, h:(7+Math.random()*6)*DPR, col:CONF[(Math.random()*CONF.length)|0]});
|
||||
}
|
||||
}
|
||||
// s = sadness 0..1 (1 = most sad). One persistent face per side.
|
||||
function drawSadFace(p){
|
||||
const r=p.r, s=p.s, alpha=Math.min(1,p.fi);
|
||||
const yb=Math.sin(p.phase)*3*DPR + s*3*DPR; // gentle bob + slight downward droop
|
||||
ctx.save(); ctx.translate(p.x, p.y+yb); ctx.globalAlpha=alpha;
|
||||
const line=Math.max(1.6,r*0.075), stroke=(window.isLightTheme ? 'rgba(40,60,90,0.92)' : 'rgba(182,200,228,0.92)');
|
||||
|
||||
// face
|
||||
ctx.beginPath(); ctx.arc(0,0,r,0,7);
|
||||
ctx.fillStyle=(window.isLightTheme ? 'rgba(40,60,90,0.15)' : 'rgba(150,175,210,0.13)'); ctx.fill();
|
||||
ctx.lineWidth=line; ctx.strokeStyle=stroke; ctx.stroke();
|
||||
|
||||
const ex=r*0.42, ey=-r*0.12, er=r*0.10;
|
||||
// worried brows: inner ends raised, steeper when sadder
|
||||
const bl=r*0.30, tilt=(0.15+s*0.55);
|
||||
ctx.lineWidth=Math.max(1.6,r*0.08); ctx.strokeStyle=stroke; ctx.lineCap='round';
|
||||
ctx.beginPath(); ctx.moveTo(-ex-bl*0.5, ey-r*0.30 + bl*tilt); ctx.lineTo(-ex+bl*0.5, ey-r*0.30 - bl*tilt*0.6); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo( ex-bl*0.5, ey-r*0.30 - bl*tilt*0.6); ctx.lineTo( ex+bl*0.5, ey-r*0.30 + bl*tilt); ctx.stroke();
|
||||
|
||||
// eyes (droopier when sadder: a lid line cuts the top)
|
||||
ctx.fillStyle=(window.isLightTheme ? 'rgba(40,60,90,0.95)' : 'rgba(205,218,238,0.95)');
|
||||
ctx.beginPath(); ctx.arc(-ex,ey,er,0,7); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc( ex,ey,er,0,7); ctx.fill();
|
||||
|
||||
// mouth: frown depth scales with sadness (quadratic, peak above corners)
|
||||
const mw=r*0.42, my=r*0.5, dip=r*(0.10 + s*0.55);
|
||||
ctx.beginPath(); ctx.moveTo(-mw,my); ctx.quadraticCurveTo(0, my-dip, mw, my);
|
||||
ctx.lineWidth=line; ctx.strokeStyle=stroke; ctx.stroke();
|
||||
|
||||
// tears: none at low sadness, one, then two — with a looping drip
|
||||
const tears = s>0.66 ? 2 : s>0.3 ? 1 : 0;
|
||||
if(tears){
|
||||
const drop=(p.tp % 1);
|
||||
ctx.fillStyle=(window.isLightTheme ? 'rgba(30,144,255,0.92)' : 'rgba(150,205,244,0.92)');
|
||||
const cheeks = tears===2 ? [-ex, ex] : [-ex];
|
||||
for(const cx of cheeks){
|
||||
const ty=ey+er + drop*r*0.9, tr=r*0.085*(1-drop*0.35);
|
||||
ctx.globalAlpha=alpha*(1-drop*0.7);
|
||||
ctx.beginPath(); ctx.arc(cx, ty, tr, 0, 7); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha=alpha;
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function tick(){
|
||||
if(mode==='faces'){ ctx.clearRect(0,0,W,H); }
|
||||
else { ctx.globalCompositeOperation='source-over'; ctx.fillStyle=fadeCol; ctx.fillRect(0,0,W,H); }
|
||||
for(const p of parts){
|
||||
if(p.t==='spark'){
|
||||
p.vx*=0.986; p.vy=p.vy*0.986+0.05*DPR; p.x+=p.vx; p.y+=p.vy; p.life-=p.decay;
|
||||
if(p.life<=0) continue;
|
||||
ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.col;
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,p.size*p.life+0.4,0,7); ctx.fill();
|
||||
} else if(p.t==='sadface'){
|
||||
p.fi+=0.03; p.phase+=0.03; p.tp+=0.010; // persists until replay
|
||||
drawSadFace(p);
|
||||
} else { // conf
|
||||
p.vy+=0.06*DPR; p.x+=p.vx; p.y+=p.vy; p.rot+=p.vr; p.life-=0.006;
|
||||
if(p.y>H+20||p.life<=0){p.life=0; continue;}
|
||||
ctx.globalCompositeOperation='source-over'; ctx.globalAlpha=Math.max(0,Math.min(1,p.life));
|
||||
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot); ctx.fillStyle=p.col;
|
||||
ctx.fillRect(-p.w/2,-p.h/2,p.w,p.h); ctx.restore();
|
||||
}
|
||||
}
|
||||
parts=parts.filter(p=>p.life>0);
|
||||
ctx.globalAlpha=1; ctx.globalCompositeOperation='source-over';
|
||||
if(running||parts.length){ rafFW=requestAnimationFrame(tick); }
|
||||
else { ctx.clearRect(0,0,W,H); rafFW=null; }
|
||||
}
|
||||
function stopFw(){ fwTimers.forEach(clearTimeout); fwTimers=[]; running=false; parts=[]; if(W&&H) ctx.clearRect(0,0,W,H); }
|
||||
|
||||
function celebrate(t){ // t: 0..1 intensity
|
||||
stopFw(); parts=[]; mode='fireworks'; fadeCol=(window.isLightTheme ? 'rgba(255,255,255,0.22)' : 'rgba(0,0,0,0.22)'); running=true;
|
||||
if(!rafFW) rafFW=requestAnimationFrame(tick);
|
||||
const rounds=Math.round(5+t*10); // 5..15
|
||||
const gap=Math.round(345-t*155); // 345..190 ms
|
||||
const base=Math.round(34+t*46); // particles per burst
|
||||
for(let k=0;k<rounds;k++){
|
||||
const d=k*gap;
|
||||
fwTimers.push(setTimeout(()=>burst(0.10+Math.random()*0.07, 0.30+Math.random()*0.24, base), d)); // left
|
||||
fwTimers.push(setTimeout(()=>burst(0.83+Math.random()*0.07, 0.30+Math.random()*0.24, base), d)); // right
|
||||
if(t>0.4) fwTimers.push(setTimeout(()=>burst(0.44+Math.random()*0.12, 0.20+Math.random()*0.14, base+8), d+90)); // center-top
|
||||
if(t>0.75) confetti(6);
|
||||
}
|
||||
fwTimers.push(setTimeout(()=>{ running=false; }, rounds*gap+400));
|
||||
}
|
||||
function sadFaces(s){ // s: 0..1 sadness (1 = most sad)
|
||||
stopFw(); parts=[]; mode='faces'; running=true;
|
||||
if(!rafFW) rafFW=requestAnimationFrame(tick);
|
||||
const r=Math.min(W,H)*0.085 + s*Math.min(W,H)*0.02; // slightly bigger when sadder
|
||||
const y=H*0.42;
|
||||
parts.push({t:'sadface', x:W*0.17, y, r, s, phase:0, tp:Math.random(), fi:0, life:1});
|
||||
parts.push({t:'sadface', x:W*0.83, y, r, s, phase:1.6, tp:Math.random()+0.4, fi:0, life:1});
|
||||
// keep running so the two faces persist (with idle bob + tears) until replay
|
||||
}
|
||||
|
||||
/* ---------- number ---------- */
|
||||
let countRAF=null, countTimer=null, moodTimer=null;
|
||||
|
||||
const parseTarget=()=>{
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const s = params.get('streak');
|
||||
const v=parseFloat(s);
|
||||
return isFinite(v)?v:0;
|
||||
};
|
||||
|
||||
function countUp(target){
|
||||
const dur=1300, start=performance.now(), ease=t=>1-Math.pow(1-t,3);
|
||||
const decimals=(String(target).split('.')[1]||'').length;
|
||||
(function frame(now){
|
||||
const t=Math.min(1,(now-start)/dur), val=target*ease(t);
|
||||
numEl.textContent=val.toLocaleString(undefined,{minimumFractionDigits:decimals,maximumFractionDigits:decimals});
|
||||
if(t<1) countRAF=requestAnimationFrame(frame);
|
||||
})(start);
|
||||
}
|
||||
|
||||
function play(){
|
||||
clearTimeout(countTimer); clearTimeout(moodTimer); cancelAnimationFrame(countRAF); stopFw();
|
||||
const target=parseTarget();
|
||||
const sad = target<=2;
|
||||
const t = sad ? 0 : Math.min(1, Math.log10(Math.max(target,3)/3)/3); // 5→0 … 5000+→1
|
||||
|
||||
// set mood + intensity
|
||||
stage.classList.remove('play','happy','sad','tier1','tier2','tier3'); body.classList.remove('sad');
|
||||
if(sad){ stage.classList.add('sad'); body.classList.add('sad'); }
|
||||
else{
|
||||
stage.classList.add('happy');
|
||||
stage.classList.add(t<0.34?'tier1':t<0.7?'tier2':'tier3');
|
||||
stage.style.setProperty('--grow',(1.12+t*0.16).toFixed(3));
|
||||
stage.style.setProperty('--halo-w',(160+t*70).toFixed(0)+'%');
|
||||
}
|
||||
|
||||
capEl.textContent=(capInput.value||'').trim();
|
||||
numEl.textContent='0';
|
||||
void stage.offsetWidth; stage.classList.add('play');
|
||||
|
||||
const sadS = Math.max(0, Math.min(1, (3 - target) / 3)); // 1→1 (most), 4→0 (least)
|
||||
|
||||
if(reduce.matches){ numEl.textContent=target.toLocaleString(); if(sad){ setTimeout(()=>sadFaces(sadS),0);} return; }
|
||||
moodTimer=setTimeout(()=> sad ? sadFaces(sadS) : celebrate(t), 2650);
|
||||
countTimer=setTimeout(()=>countUp(target), 2750);
|
||||
}
|
||||
|
||||
replay.addEventListener('click', play);
|
||||
numInput.addEventListener('keydown', e=>{ if(e.key==='Enter') play(); });
|
||||
capInput.addEventListener('keydown', e=>{ if(e.key==='Enter') play(); });
|
||||
window.addEventListener('load', ()=>setTimeout(play,180));
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
setInterval(() => {
|
||||
let isLight = false;
|
||||
try {
|
||||
isLight = !window.parent.document.documentElement.classList.contains('dark');
|
||||
} catch (e) {
|
||||
const p = new URLSearchParams(window.location.search);
|
||||
isLight = p.get('theme') === 'light';
|
||||
}
|
||||
window.isLightTheme = isLight;
|
||||
|
||||
if (isLight) {
|
||||
document.body.style.setProperty('background', 'transparent', 'important');
|
||||
document.documentElement.style.setProperty('background', 'transparent', 'important');
|
||||
const b = document.querySelector('.badge');
|
||||
if (b) b.style.setProperty('background', "url('/PragatiVeer_Logo/SVG_Logos/Square_light_background.svg') center/cover no-repeat", 'important');
|
||||
if (typeof fadeCol !== 'undefined') {
|
||||
fadeCol = 'rgba(255,255,255,0.22)';
|
||||
}
|
||||
window.sadFaceStroke = 'rgba(40,60,90,0.92)';
|
||||
window.sadFaceFill = 'rgba(40,60,90,0.15)';
|
||||
window.sadFaceEyes = 'rgba(40,60,90,0.95)';
|
||||
window.sadFaceTears = 'rgba(30,144,255,0.92)';
|
||||
} else {
|
||||
document.body.style.setProperty('background', 'transparent', 'important');
|
||||
document.documentElement.style.setProperty('background', 'transparent', 'important');
|
||||
const b = document.querySelector('.badge');
|
||||
if (b) b.style.setProperty('background', "url('/PragatiVeer_Logo/SVG_Logos/Square_dark_background.svg') center/cover no-repeat", 'important');
|
||||
if (typeof fadeCol !== 'undefined') {
|
||||
fadeCol = 'rgba(0,0,0,0.22)';
|
||||
}
|
||||
window.sadFaceStroke = 'rgba(182,200,228,0.92)';
|
||||
window.sadFaceFill = 'rgba(150,175,210,0.13)';
|
||||
window.sadFaceEyes = 'rgba(205,218,238,0.95)';
|
||||
window.sadFaceTears = 'rgba(150,205,244,0.92)';
|
||||
}
|
||||
}, 100);
|
||||
})();
|
||||
</script>
|
||||
|
||||
1
public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
24
src/app/about/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||
<header className="border-b border-border bg-background px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<Link href="/" className="text-muted-foreground hover:text-foreground flex items-center gap-2 transition-colors font-medium">
|
||||
<ArrowLeft size={16} /> Back to Home
|
||||
</Link>
|
||||
</header>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-8 text-center animate-in fade-in duration-700">
|
||||
<h1 className="text-5xl font-bold tracking-tighter mb-6">About PragatiVeer</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl leading-relaxed mb-8">
|
||||
We are a team of educators and engineers dedicated to re-engineering NEET/JEE preparation through the power of Artificial Intelligence.
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<Button size="lg" className="rounded-full font-bold px-8 shadow-xl">Join the Revolution</Button>
|
||||
</Link>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
src/app/api/og/progress/route.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
|
||||
// Extract dynamic data from the URL query parameters
|
||||
const streak = searchParams.get('streak') || '0';
|
||||
const accuracy = searchParams.get('accuracy') || '0';
|
||||
const name = searchParams.get('name') || 'Student';
|
||||
|
||||
// Renders a high-end, minimalist graphic using pure Tailwind and React
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#0a0a0a', // Deep premium dark mode
|
||||
color: '#ffffff',
|
||||
fontFamily: 'sans-serif',
|
||||
padding: '40px',
|
||||
border: '8px solid #333',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', fontSize: 60, fontWeight: 800, marginBottom: 20, letterSpacing: '-0.05em' }}>
|
||||
PragatiVeer
|
||||
</div>
|
||||
<div style={{ display: 'flex', fontSize: 32, color: '#a3a3a3', marginBottom: 60 }}>
|
||||
{name}'s JEE Preparation Progress
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-around' }}>
|
||||
{/* Streak Metric */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 80, fontWeight: 900, color: '#f59e0b' }}>{streak}🔥</span>
|
||||
<span style={{ fontSize: 24, color: '#d4d4d4', marginTop: 10 }}>Day Streak</span>
|
||||
</div>
|
||||
|
||||
{/* Accuracy Metric */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 80, fontWeight: 900, color: '#10b981' }}>{accuracy}%</span>
|
||||
<span style={{ fontSize: 24, color: '#d4d4d4', marginTop: 10 }}>Top Accuracy</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', position: 'absolute', bottom: 40, fontSize: 20, color: '#525252' }}>
|
||||
Join me on pragativeer.com/invite
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
}
|
||||
);
|
||||
} catch (e: any) {
|
||||
return new Response(`Failed to generate image`, { status: 500 });
|
||||
}
|
||||
}
|
||||
85
src/app/cancellation-policy/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function CancellationPolicyPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<h1 className="text-4xl md:text-5xl font-black tracking-tight mb-12 text-foreground">
|
||||
PragatiVeer Cancellation Policy
|
||||
</h1>
|
||||
<div className="prose prose-invert prose-lg max-w-none text-muted-foreground">
|
||||
|
||||
<p className="text-muted-foreground mb-8"><strong>Last Updated:</strong> 27 July 2026<br/>
|
||||
<strong>Version:</strong> 1.0<br/>
|
||||
<strong>Document Ref:</strong> PV-LEG-CP-001<br/>
|
||||
<strong>Applicable To:</strong> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">1. Overview</h2>
|
||||
<p>This Cancellation Policy governs cancellation of paid subscriptions on PragatiVeer, operated by <strong>Gyannetra Pvt Ltd</strong> (CIN: U72900PN2015PTC156021).</p>
|
||||
<p>By purchasing a subscription, you agree to the terms of this policy.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">2. Subscription Cancellation by You</h2>
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">2.1 How to Cancel</h3>
|
||||
<p>You may cancel your subscription at any time by:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Going to <strong>Account Settings</strong> on the PragatiVeer website and disabling auto-renewal</li>
|
||||
<li>Emailing <strong>contact@pragativeer.com</strong> with subject “Cancellation Request” along with your registered email or phone number</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">2.2 Effect of Cancellation</h3>
|
||||
<p>Upon cancellation:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Your subscription will <strong>not</strong> auto-renew at the end of the current billing period</li>
|
||||
<li>You will continue to have full access to your paid features until the end of the current billing period</li>
|
||||
<li>No refund will be issued for the remaining days of the current billing period, except as provided in our <a href="/refund-policy" className="text-[#00F0FF] hover:underline font-semibold">Refund Policy</a></li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">2.3 Timing</h3>
|
||||
<p>To avoid being charged for the next billing cycle, cancellation must be requested at least <strong>24 hours</strong> before the renewal date.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">3. Auto-Renewal</h2>
|
||||
<p>All subscriptions auto-renew at the end of each billing period unless cancelled at least 24 hours before renewal. You authorise us to charge the applicable subscription fee to your chosen payment method at each renewal.</p>
|
||||
<p>You will receive a reminder before renewal. If you do not wish to renew, please cancel before the renewal date.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">4. Cancellation by PragatiVeer</h2>
|
||||
<p>We reserve the right to cancel or suspend your subscription immediately if:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>You violate our <a href="/terms-and-conditions" className="text-[#00F0FF] hover:underline font-semibold">Terms & Conditions</a></li>
|
||||
<li>You engage in fraudulent, abusive, or illegal activity</li>
|
||||
<li>We are required to do so by law</li>
|
||||
</ul>
|
||||
<p>In such cases, fees paid are non-refundable.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">5. Account Deletion</h2>
|
||||
<p>Cancelling your subscription does <strong>not</strong> delete your account. To delete your account and all associated data, please follow the process outlined in our <a href="/privacy-policy" className="text-[#00F0FF] hover:underline font-semibold">Privacy Policy</a>.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">6. Contact</h2>
|
||||
<p>For any questions regarding this Cancellation Policy, contact us at <strong>contact@pragativeer.com</strong> or reach out to our Grievance Officer:</p>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Detail</th>
|
||||
<th className="p-3 border">Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Grievance Officer</td><td className="p-3 border">Himani Kachhawaha</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Email</td><td className="p-3 border">grievance@pragativeer.com</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Phone</td><td className="p-3 border">+91 9079337961</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-6 bg-secondary/20 rounded-xl">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>CIN: U72900PN2015PTC156021</p>
|
||||
<p>OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
<p>pragativeer.com</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
131
src/app/cancellation/page.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { PublicLayout } from "@/components/Landing/PublicLayout";
|
||||
|
||||
export default function CancellationPolicyPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="bg-background min-h-screen pt-24 pb-16">
|
||||
<div className="container max-w-4xl mx-auto px-4 md:px-8">
|
||||
<div className="bg-secondary/20 p-8 md:p-12 rounded-3xl border border-border shadow-sm">
|
||||
<div className="mb-10 border-b border-border pb-6">
|
||||
<h1 className="text-3xl md:text-5xl font-black mb-4">PragatiVeer Cancellation Policy</h1>
|
||||
<p className="text-muted-foreground font-medium">Gyannetra Pvt Ltd</p>
|
||||
<div className="mt-4 space-y-1 text-sm text-muted-foreground/80">
|
||||
<p><span className="font-semibold text-muted-foreground">Last Updated:</span> 27 July 2026</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Version:</span> 1.0</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Document Ref:</span> PV-LEG-CP-001</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Applicable To:</span> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 prose prose-invert max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-li:text-muted-foreground">
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">1. Overview</h2>
|
||||
<p>
|
||||
This Cancellation Policy governs cancellation of paid subscriptions on PragatiVeer, operated by <strong>Gyannetra Pvt Ltd</strong> (CIN: U72900PN2015PTC156021).
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
By purchasing a subscription, you agree to the terms of this policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">2. Subscription Cancellation by You</h2>
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">2.1 How to Cancel</h3>
|
||||
<p>You may cancel your subscription at any time by:</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>Going to <strong>Account Settings</strong> on the PragatiVeer website and disabling auto-renewal</li>
|
||||
<li>Emailing <strong>contact@pragativeer.com</strong> with subject “Cancellation Request” along with your registered email or phone number</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-6 mb-2">2.2 Effect of Cancellation</h3>
|
||||
<p>Upon cancellation:</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>Your subscription will <strong>not</strong> auto-renew at the end of the current billing period</li>
|
||||
<li>You will continue to have full access to your paid features until the end of the current billing period</li>
|
||||
<li>No refund will be issued for the remaining days of the current billing period, except as provided in our <strong>Refund Policy</strong></li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-6 mb-2">2.3 Timing</h3>
|
||||
<p>
|
||||
To avoid being charged for the next billing cycle, cancellation must be requested at least <strong>24 hours</strong> before the renewal date.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">3. Auto-Renewal</h2>
|
||||
<p>
|
||||
All subscriptions auto-renew at the end of each billing period unless cancelled at least 24 hours before renewal. You authorise us to charge the applicable subscription fee to your chosen payment method at each renewal.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
You will receive a reminder before renewal. If you do not wish to renew, please cancel before the renewal date.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">4. Cancellation by PragatiVeer</h2>
|
||||
<p>We reserve the right to cancel or suspend your subscription immediately if:</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>You violate our <strong>Terms & Conditions</strong></li>
|
||||
<li>You engage in fraudulent, abusive, or illegal activity</li>
|
||||
<li>We are required to do so by law</li>
|
||||
</ul>
|
||||
<p className="mt-4">
|
||||
In such cases, fees paid are non-refundable.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">5. Account Deletion</h2>
|
||||
<p>
|
||||
Cancelling your subscription does <strong>not</strong> delete your account. To delete your account and all associated data, please follow the process outlined in our <strong>Privacy Policy</strong>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">6. Contact</h2>
|
||||
<p>
|
||||
For any questions regarding this Cancellation Policy, contact us at <strong>contact@pragativeer.com</strong> or reach out to our Grievance Officer:
|
||||
</p>
|
||||
<div className="overflow-x-auto my-4">
|
||||
<table className="min-w-full border-collapse border border-border text-sm">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50">
|
||||
<th className="border border-border px-4 py-2 text-left">Detail</th>
|
||||
<th className="border border-border px-4 py-2 text-left">Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">Grievance Officer</td>
|
||||
<td className="border border-border px-4 py-2">Himani Kachhawaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">Email</td>
|
||||
<td className="border border-border px-4 py-2">grievance@pragativeer.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">Phone</td>
|
||||
<td className="border border-border px-4 py-2">+91 9079337961</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mt-8 p-4 bg-background rounded-xl border border-border text-sm">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>CIN: U72900PN2015PTC156021</p>
|
||||
<p>OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
<p>pragativeer.com</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
48
src/app/contact/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, Mail, MapPin, Phone } from "lucide-react"
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||
<header className="border-b border-border bg-background px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<Link href="/" className="text-muted-foreground hover:text-foreground flex items-center gap-2 transition-colors font-medium">
|
||||
<ArrowLeft size={16} /> Back to Home
|
||||
</Link>
|
||||
</header>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-8 animate-in fade-in duration-700">
|
||||
<h1 className="text-5xl font-bold tracking-tighter mb-4 text-center">Contact Us</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl text-center mb-12">
|
||||
Have a question or need support? We're here to help you ace your preparation.
|
||||
</p>
|
||||
|
||||
<Card className="w-full max-w-md bg-secondary/10 border-border backdrop-blur-md rounded-3xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Send us a message</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">Your Email</label>
|
||||
<input type="email" placeholder="student@example.com" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">Message</label>
|
||||
<textarea rows={4} placeholder="How can we help?" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"></textarea>
|
||||
</div>
|
||||
<Button className="w-full rounded-xl h-12 font-bold text-base">Send Message</Button>
|
||||
|
||||
<div className="pt-6 border-t border-border mt-6 space-y-3">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground font-medium">
|
||||
<Mail size={16} className="text-primary" /> support@pragativeer.com
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground font-medium">
|
||||
<MapPin size={16} className="text-primary" /> Tech Hub, Bengaluru, India
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
87
src/app/disclaimer/page.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { PublicLayout } from "@/components/Landing/PublicLayout";
|
||||
|
||||
export default function DisclaimerPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="bg-background min-h-screen pt-24 pb-16">
|
||||
<div className="container max-w-4xl mx-auto px-4 md:px-8">
|
||||
<div className="bg-secondary/20 p-8 md:p-12 rounded-3xl border border-border shadow-sm">
|
||||
<div className="mb-10 border-b border-border pb-6">
|
||||
<h1 className="text-3xl md:text-5xl font-black mb-4">PragatiVeer Disclaimer</h1>
|
||||
<p className="text-muted-foreground font-medium">Gyannetra Pvt Ltd</p>
|
||||
<div className="mt-4 space-y-1 text-sm text-muted-foreground/80">
|
||||
<p><span className="font-semibold text-muted-foreground">Last Updated:</span> 27 July 2026</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Version:</span> 1.0</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Document Ref:</span> PV-LEG-DIS-001</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Applicable To:</span> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 prose prose-invert max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-li:text-muted-foreground">
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">1. General Information</h2>
|
||||
<p>
|
||||
The information provided by Gyannetra Pvt Ltd ("we," "us," or "our") on the PragatiVeer website (the "Site") and our mobile application is for general informational and educational purposes only. All information on the Site and our mobile application is provided in good faith; however, we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability, or completeness of any information on the Site or our mobile application.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
UNDER NO CIRCUMSTANCE SHALL WE HAVE ANY LIABILITY TO YOU FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF THE SITE OR OUR MOBILE APPLICATION OR RELIANCE ON ANY INFORMATION PROVIDED ON THE SITE AND OUR MOBILE APPLICATION. YOUR USE OF THE SITE AND OUR MOBILE APPLICATION AND YOUR RELIANCE ON ANY INFORMATION ON THE SITE AND OUR MOBILE APPLICATION IS SOLELY AT YOUR OWN RISK.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">2. Educational and Test Preparation Disclaimer</h2>
|
||||
<p>
|
||||
PragatiVeer provides simulated Computer-Based Tests (CBT) and Daily Practice Problems (DPP) intended to assist students in preparing for competitive exams like NEET and JEE. However:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li><strong>No Guarantee of Results:</strong> We do not guarantee admission to any college, university, or program, nor do we guarantee specific scores or ranks on any competitive exam.</li>
|
||||
<li><strong>Not an Official Body:</strong> PragatiVeer is not affiliated with, endorsed by, or sponsored by the National Testing Agency (NTA), the Joint Seat Allocation Authority (JoSAA), or any other official examination conducting body.</li>
|
||||
<li><strong>Dynamic Content:</strong> Exam patterns, syllabi, and guidelines are subject to change by the official conducting bodies. While we strive to keep our content updated, we cannot guarantee that our materials perfectly reflect the most current official exam patterns at all times.</li>
|
||||
<li><strong>AI Predictions:</strong> Any predictions made by our AI Coach (such as "Expected Score" or "Estimated AIR") are based on historical data and probabilistic models. They are estimations for motivational and tracking purposes only and should not be considered definitive forecasts of actual exam performance.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">3. External Links Disclaimer</h2>
|
||||
<p>
|
||||
The Site and our mobile application may contain (or you may be sent through the Site or our mobile application) links to other websites or content belonging to or originating from third parties or links to websites and features in banners or other advertising. Such external links are not investigated, monitored, or checked for accuracy, adequacy, validity, reliability, availability, or completeness by us.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
WE DO NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR THE ACCURACY OR RELIABILITY OF ANY INFORMATION OFFERED BY THIRD-PARTY WEBSITES LINKED THROUGH THE SITE OR ANY WEBSITE OR FEATURE LINKED IN ANY BANNER OR OTHER ADVERTISING. WE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR MONITORING ANY TRANSACTION BETWEEN YOU AND THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">4. Testimonials Disclaimer</h2>
|
||||
<p>
|
||||
The Site may contain testimonials by users of our products and/or services. These testimonials reflect the real-life experiences and opinions of such users. However, the experiences are personal to those particular users, and may not necessarily be representative of all users of our products and/or services. We do not claim, and you should not assume, that all users will have the same experiences. YOUR INDIVIDUAL RESULTS MAY VARY.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
The testimonials on the Site are submitted in various forms such as text, audio, and/or video, and are reviewed by us before being posted. They appear on the Site verbatim as given by the users, except for the correction of grammar or typing errors. Some testimonials may have been shortened for the sake of brevity where the full testimonial contained extraneous information not relevant to the general public.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
The views and opinions contained in the testimonials belong solely to the individual user and do not reflect our views and opinions. We are not affiliated with users who provide testimonials, and users are not paid or otherwise compensated for their testimonials.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">5. Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions about this Disclaimer, please contact us at:
|
||||
</p>
|
||||
<div className="mt-4 p-4 bg-background rounded-xl border border-border text-sm">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>Email: contact@pragativeer.com</p>
|
||||
<p>Phone: +91 9079337961</p>
|
||||
<p>Address: OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
129
src/app/globals.css
Normal file
@@ -0,0 +1,129 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--border: #e5e5e5;
|
||||
--input: #e5e5e5;
|
||||
--ring: #171717;
|
||||
|
||||
--primary: #171717;
|
||||
--primary-foreground: #fafafa;
|
||||
--secondary: #f5f5f5;
|
||||
--secondary-foreground: #171717;
|
||||
--accent: #f5f5f5;
|
||||
--accent-foreground: #171717;
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #737373;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--border: #1f1f1f;
|
||||
--input: #1f1f1f;
|
||||
--ring: #D1FF4D;
|
||||
|
||||
--primary: #D1FF4D;
|
||||
--primary-foreground: #0a0a0a;
|
||||
--secondary: #171717;
|
||||
--secondary-foreground: #ededed;
|
||||
--accent: #00F0FF;
|
||||
--accent-foreground: #0a0a0a;
|
||||
--muted: #171717;
|
||||
--muted-foreground: #a3a3a3;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
||||
@apply antialiased selection:bg-primary/20;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
background-size: 40px 40px;
|
||||
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.animate-gradient {
|
||||
background-size: 200% auto;
|
||||
animation: gradient-shift 4s linear infinite;
|
||||
}
|
||||
|
||||
.animate-marquee {
|
||||
animation: marquee 30s linear infinite;
|
||||
}
|
||||
|
||||
.animate-shine {
|
||||
animation: shine 3s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
0% { transform: translateX(0%); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { transform: translateX(-100%) skewX(-15deg); }
|
||||
30% { transform: translateX(200%) skewX(-15deg); }
|
||||
100% { transform: translateX(200%) skewX(-15deg); }
|
||||
}
|
||||
|
||||
/* Custom Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--background);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--muted-foreground);
|
||||
}
|
||||
1
src/app/icon.svg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
57
src/app/layout.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PragatiVeer - AI Powered NEET/JEE Preparation",
|
||||
description: "Experience the most accurate Computer-Based Test Exam Engine combined with adaptive Daily Practice Problems generated specifically for your weak areas in real-time.",
|
||||
keywords: "NEET, JEE, Prep, AI, CBT, DPP, Online Test, Study",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<Script
|
||||
id="theme-script"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
try {
|
||||
if (window.location.pathname.startsWith('/dashboard') || window.location.pathname.startsWith('/cbt')) {
|
||||
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
} else {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
} catch (_) {}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
103
src/app/login/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ArrowLeft, Eye, EyeOff } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Logo } from "@/components/Logo"
|
||||
|
||||
export default function LoginPage() {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (email === "demo@pragativeer.com" && password === "Pv9#kL2$mQ8!vX4@") {
|
||||
router.push("/dashboard")
|
||||
} else {
|
||||
setError("Invalid email or password.")
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-6 selection:bg-[#00F0FF]/20">
|
||||
<Link href="/" className="absolute top-8 left-8 text-muted-foreground hover:text-foreground flex items-center gap-2 transition-colors font-medium">
|
||||
<ArrowLeft size={16} /> Back to Home
|
||||
</Link>
|
||||
|
||||
<div className="w-full max-w-md space-y-8 animate-in fade-in slide-in-from-bottom-8 duration-700">
|
||||
<div className="flex justify-center mb-8">
|
||||
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
||||
</div>
|
||||
|
||||
<Card className="border-border bg-secondary/10 backdrop-blur-xl shadow-2xl shadow-[#00F0FF]/5 rounded-[2rem] overflow-hidden">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<CardTitle className="text-2xl font-bold tracking-tight">Welcome back</CardTitle>
|
||||
<CardDescription className="text-muted-foreground">Enter your credentials to access your dashboard.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 pt-4">
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="student@example.com"
|
||||
suppressHydrationWarning
|
||||
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF] focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-bold text-foreground">
|
||||
Password
|
||||
</label>
|
||||
<Link href="#" className="text-xs font-bold text-[#00F0FF] hover:text-[#00F0FF]/80 transition-colors">Forgot password?</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
suppressHydrationWarning
|
||||
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF] focus:border-transparent transition-all pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-500 text-sm font-medium text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full rounded-xl h-12 font-bold text-base bg-foreground text-background hover:bg-foreground/90 transition-all shadow-[0_0_15px_-5px_#00F0FF]">
|
||||
Sign In
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground font-medium">
|
||||
Don't have an account? <Link href="/signup" className="font-bold text-foreground hover:text-primary transition-colors">Sign up</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
623
src/app/page.tsx
Normal file
@@ -0,0 +1,623 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { BrainCircuit, Activity, Zap, CheckCircle2, Shield, ArrowRight, Play, BookOpen, LineChart, Trophy, Sparkles, MessageSquare, X, Minus } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { motion, useScroll, useTransform, Variants } from "framer-motion"
|
||||
import { useRef, useEffect } from "react"
|
||||
|
||||
export default function MarketingPage() {
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.add('dark');
|
||||
}, []);
|
||||
|
||||
const containerRef = useRef(null)
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
offset: ["start start", "end start"]
|
||||
})
|
||||
|
||||
// Slower background parallax
|
||||
const yBg = useTransform(scrollYProgress, [0, 1], ["0%", "50%"])
|
||||
|
||||
// Stagger variants for the features
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const itemVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { type: "spring", stiffness: 100, damping: 12 }
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="min-h-screen bg-background text-foreground overflow-x-hidden selection:bg-primary/20 relative">
|
||||
|
||||
{/* Animated Deep Background */}
|
||||
<div className="fixed inset-0 z-0 pointer-events-none bg-grid opacity-30"></div>
|
||||
<div className="fixed inset-0 z-0 pointer-events-none" style={{ background: 'radial-gradient(circle at 50% 0%, rgba(209, 255, 77, 0.05) 0%, transparent 50%)' }}></div>
|
||||
<div className="fixed inset-0 z-0 pointer-events-none" style={{ background: 'radial-gradient(circle at 100% 50%, rgba(0, 240, 255, 0.03) 0%, transparent 40%)' }}></div>
|
||||
|
||||
{/* Floating Orbs (Using Framer Motion) */}
|
||||
<motion.div
|
||||
className="fixed top-1/4 left-1/4 size-[400px] rounded-full bg-primary/20 blur-[120px] pointer-events-none mix-blend-screen -z-10"
|
||||
animate={{ x: [0, 50, -20, 0], y: [0, -30, 40, 0] }}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<motion.div
|
||||
className="fixed bottom-1/4 right-1/4 size-[500px] rounded-full bg-[#00F0FF]/10 blur-[150px] pointer-events-none mix-blend-screen -z-10"
|
||||
animate={{ x: [0, -60, 30, 0], y: [0, 40, -50, 0] }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="fixed top-0 w-full z-50 bg-background/50 backdrop-blur-xl border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<Link href="/" className="relative h-14 w-60 hover:opacity-90 transition-opacity block">
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left dark:hidden" priority
|
||||
/>
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left hidden dark:block" priority
|
||||
/>
|
||||
</Link>
|
||||
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-muted-foreground">
|
||||
<Link href="#features" className="hover:text-foreground transition-colors">Features</Link>
|
||||
<Link href="#pricing" className="hover:text-foreground transition-colors">Pricing</Link>
|
||||
<Link href="/about" className="hover:text-foreground transition-colors">About</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/login" className="text-sm font-bold hover:text-primary transition-colors hidden sm:block">Log in</Link>
|
||||
<Link href="/signup">
|
||||
<Button className="rounded-full font-bold px-6 shadow-[0_0_15px_-5px_var(--color-primary)] hover:shadow-[0_0_25px_-5px_var(--color-primary)] transition-all">Get Started</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="relative z-10 pt-32 pb-24">
|
||||
{/* Hero Section */}
|
||||
<section className="px-6 max-w-7xl mx-auto text-center space-y-8 pt-12 pb-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<Badge variant="outline" className="border-primary/50 text-primary bg-primary/10 px-4 py-1.5 rounded-full text-xs font-bold tracking-widest uppercase shadow-[0_0_15px_-5px_var(--color-primary)] mb-6">
|
||||
The Next Evolution of Prep
|
||||
</Badge>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, type: "spring" }}
|
||||
className="text-6xl md:text-8xl font-bold tracking-tighter max-w-5xl mx-auto leading-[1.1]"
|
||||
>
|
||||
NEET/JEE Prep, <br className="hidden md:block" />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-[#00F0FF] to-primary animate-gradient pb-2 drop-shadow-[0_0_20px_rgba(209,255,77,0.3)]">Re-engineered by AI.</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="text-xl md:text-2xl text-muted-foreground max-w-2xl mx-auto leading-relaxed font-medium"
|
||||
>
|
||||
Experience the most accurate CBT Exam Engine combined with adaptive DPPs generated specifically for your weak areas in real-time.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-8"
|
||||
>
|
||||
<Link href="/signup">
|
||||
<Button size="lg" className="group rounded-full h-14 px-8 text-base font-bold shadow-[0_0_40px_-10px_var(--color-primary)] hover:shadow-[0_0_60px_-10px_var(--color-primary)] transition-all w-full sm:w-auto relative overflow-hidden">
|
||||
<span className="relative z-10 flex items-center">Start Free Trial <ArrowRight className="ml-2 group-hover:translate-x-1 transition-transform" size={18} /></span>
|
||||
<div className="absolute inset-0 -translate-x-full animate-shine bg-gradient-to-r from-transparent via-white/20 to-transparent z-0"></div>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/signup">
|
||||
<Button size="lg" variant="outline" className="rounded-full h-14 px-8 text-base border-border bg-background/50 backdrop-blur-sm w-full sm:w-auto hover:bg-secondary/50 hover:text-foreground transition-colors group">
|
||||
<Play className="mr-2 group-hover:scale-110 transition-transform text-[#00F0FF]" size={18} fill="currentColor" /> Watch Demo
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Dashboard Preview Mockup */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 1, delay: 0.6, type: "spring" }}
|
||||
className="relative max-w-5xl mx-auto mt-20 rounded-[2rem] border border-border/60 bg-secondary/5 backdrop-blur-xl shadow-[0_0_50px_-15px_rgba(0,240,255,0.15)] overflow-hidden"
|
||||
>
|
||||
{/* Top Window Chrome Bar */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-background/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-3 rounded-full bg-red-500/80"></div>
|
||||
<div className="size-3 rounded-full bg-yellow-500/80"></div>
|
||||
<div className="size-3 rounded-full bg-green-500/80"></div>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center bg-background/80 border border-border/50 px-6 py-1.5 rounded-full text-xs font-mono text-muted-foreground w-80 justify-center gap-2">
|
||||
<div className="size-2 rounded-full bg-[#00F0FF] animate-pulse"></div>
|
||||
pragativeer.com/dashboard/student
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="border-[#00F0FF]/30 text-[#00F0FF] bg-[#00F0FF]/5 px-2 py-0.5 rounded-md text-[10px] uppercase tracking-wider font-bold">
|
||||
AI Active
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dashboard Inner Screen */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 min-h-[480px]">
|
||||
{/* Mock Sidebar */}
|
||||
<div className="md:col-span-1 border-r border-border/50 p-6 space-y-6 hidden md:block bg-background/20">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-primary/10 text-primary border border-primary/20 text-sm font-semibold">
|
||||
<Activity size={18} />
|
||||
Overview
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||
<Play size={18} className="text-muted-foreground" />
|
||||
Exams (CBT)
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||
<BrainCircuit size={18} />
|
||||
Daily Practice
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||
<BookOpen size={18} />
|
||||
Study Materials
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-6 border-t border-border/50">
|
||||
<p className="px-3 text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-3">AI Intelligence</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||
<LineChart size={18} />
|
||||
Analytics
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||
<Trophy size={18} />
|
||||
Rank Predictor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mock Main Content Area */}
|
||||
<div className="md:col-span-3 p-6 md:p-8 space-y-6 text-left">
|
||||
{/* Greeting & Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold tracking-tight">Welcome, Demo Student</h3>
|
||||
<p className="text-xs text-muted-foreground font-medium mt-1">Your adaptive NEET path is optimized. Next review in 2 hours.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-secondary/30 border border-border/50 px-3 py-1.5 rounded-xl text-xs font-semibold">
|
||||
<span className="size-2 rounded-full bg-emerald-500 shadow-[0_0_8px_#10b981]"></span>
|
||||
Subscription: <span className="text-primary font-bold ml-1">AI Pro Plan</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dashboard Grid Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
{/* Card 1: Weakness Analysis */}
|
||||
<div className="bg-background/40 border border-border/50 rounded-2xl p-5 space-y-4 shadow-sm hover:border-[#00F0FF]/30 transition-all duration-300 relative group">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-muted-foreground">Focus Topic</h4>
|
||||
<Badge variant="outline" className="border-primary/30 text-primary bg-primary/5 rounded-md text-[10px] font-bold">Physics</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm font-bold">
|
||||
<span>Rotational Dynamics</span>
|
||||
<span className="text-primary">42% Mastery</span>
|
||||
</div>
|
||||
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary rounded-full w-[42%] shadow-[0_0_10px_var(--color-primary)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
<strong className="text-foreground font-semibold">AI recommendation:</strong> Solve 5 intermediate DPP questions on Moment of Inertia to unlock the next level and boost your expected score.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Live Score & AIR Rank Predictor */}
|
||||
<div className="bg-background/40 border border-border/50 rounded-2xl p-5 space-y-4 shadow-sm hover:border-[#00F0FF]/30 transition-all duration-300 relative group">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-[#00F0FF]">Rank Simulator</h4>
|
||||
<Badge variant="outline" className="border-[#00F0FF]/30 text-[#00F0FF] bg-[#00F0FF]/5 rounded-md text-[10px] font-bold">NEET Target</Badge>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-bold text-muted-foreground uppercase">Estimated AIR</p>
|
||||
<p className="text-3xl font-bold tracking-tight text-[#00F0FF] drop-shadow-[0_0_8px_rgba(0,240,255,0.3)]">1,240</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[10px] font-bold text-muted-foreground uppercase">Expected Score</p>
|
||||
<p className="text-xl font-bold tracking-tight text-foreground">685 <span className="text-xs font-medium text-muted-foreground">/ 720</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Mini Sparkline Chart representation */}
|
||||
<div className="flex items-center gap-1 h-8 pt-2">
|
||||
{[30, 45, 35, 55, 70, 60, 85].map((val, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 bg-[#00F0FF]/20 hover:bg-[#00F0FF]/60 transition-colors rounded-t"
|
||||
style={{ height: `${val}%` }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Tutor Consultation Interface */}
|
||||
<div className="bg-secondary/20 border border-border/40 rounded-2xl p-5 flex gap-4 items-start relative overflow-hidden">
|
||||
<div className="size-10 rounded-xl bg-primary text-primary-foreground flex items-center justify-center font-bold text-sm shadow-[0_0_15px_-3px_var(--color-primary)] shrink-0 mt-1">
|
||||
<Sparkles size={20} className="animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-3 text-left w-full">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<h4 className="text-sm font-bold text-foreground">AI Tutor Agent</h4>
|
||||
<span className="text-[10px] font-mono text-muted-foreground">Response generated instantly</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground leading-relaxed max-w-xl">
|
||||
"I have analyzed your last CBT physics test. You excelled in Mechanics but hesitated on electromagnetic induction questions, spending an average of 42 seconds longer per problem. I have generated a custom 10-question DPP focusing exclusively on induction. Let's tackle it now."
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Link href="/dashboard/dpp">
|
||||
<Button size="sm" className="h-8 rounded-lg px-4 text-xs font-bold">
|
||||
Start Custom DPP
|
||||
</Button>
|
||||
</Link>
|
||||
<Button size="sm" variant="outline" className="h-8 rounded-lg px-4 text-xs border-border bg-background/50">
|
||||
<MessageSquare size={12} className="mr-1.5 text-[#00F0FF]" /> Ask AI to explain
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Infinite Trust Bar */}
|
||||
<section className="mt-32 border-y border-border/50 bg-secondary/10 backdrop-blur-sm overflow-hidden py-8 relative">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-32 bg-gradient-to-r from-background to-transparent z-10"></div>
|
||||
<div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-background to-transparent z-10"></div>
|
||||
<div className="flex w-[200%] animate-marquee opacity-70 hover:opacity-100 transition-opacity">
|
||||
{/* First Set */}
|
||||
<div className="flex-1 flex justify-around items-center px-4">
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><Shield size={28} className="text-[#00F0FF] drop-shadow-[0_0_10px_rgba(0,240,255,0.5)]" /> NTA Compliant</div>
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><Activity size={28} className="text-primary drop-shadow-[0_0_10px_var(--color-primary)]" /> 99.9% Uptime</div>
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><BrainCircuit size={28} className="text-[#00F0FF] drop-shadow-[0_0_10px_rgba(0,240,255,0.5)]" /> 10M+ AI Models</div>
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><CheckCircle2 size={28} className="text-primary drop-shadow-[0_0_10px_var(--color-primary)]" /> 50k+ Selections</div>
|
||||
</div>
|
||||
{/* Duplicate Set for seamless scroll */}
|
||||
<div className="flex-1 flex justify-around items-center px-4">
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><Shield size={28} className="text-[#00F0FF] drop-shadow-[0_0_10px_rgba(0,240,255,0.5)]" /> NTA Compliant</div>
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><Activity size={28} className="text-primary drop-shadow-[0_0_10px_var(--color-primary)]" /> 99.9% Uptime</div>
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><BrainCircuit size={28} className="text-[#00F0FF] drop-shadow-[0_0_10px_rgba(0,240,255,0.5)]" /> 10M+ AI Models</div>
|
||||
<div className="flex items-center gap-3 font-bold text-xl tracking-tight"><CheckCircle2 size={28} className="text-primary drop-shadow-[0_0_10px_var(--color-primary)]" /> 50k+ Selections</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Animated DPP Engine Section */}
|
||||
<section id="features" className="py-32 px-6 max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<h2 className="text-4xl md:text-6xl font-bold tracking-tighter mb-6">Adaptive Engine</h2>
|
||||
<p className="text-muted-foreground text-xl max-w-2xl mx-auto">Our AI analyzes every keystroke and hesitation to generate the exact problems you need to solve next.</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-8"
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="bg-secondary/10 backdrop-blur-xl border-border/50 hover:border-primary/50 transition-all duration-300 rounded-[2rem] h-full shadow-lg group">
|
||||
<CardContent className="p-8 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:-rotate-12 duration-500">
|
||||
<Activity size={120} />
|
||||
</div>
|
||||
<div className="size-14 rounded-2xl bg-primary/20 text-primary flex items-center justify-center mb-8 shadow-[0_0_20px_-5px_var(--color-primary)]">
|
||||
<Activity size={28} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold mb-4 tracking-tight">Real-time Analysis</h3>
|
||||
<p className="text-muted-foreground leading-relaxed text-lg">
|
||||
Every test attempt updates your knowledge graph instantly, identifying micro-concepts where you struggle.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="bg-[#00F0FF]/5 backdrop-blur-xl border-[#00F0FF]/20 hover:border-[#00F0FF]/60 transition-all duration-300 rounded-[2rem] h-full shadow-[0_0_30px_-15px_#00F0FF] group">
|
||||
<CardContent className="p-8 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:rotate-12 duration-500">
|
||||
<BrainCircuit size={120} />
|
||||
</div>
|
||||
<div className="size-14 rounded-2xl bg-[#00F0FF]/20 text-[#00F0FF] flex items-center justify-center mb-8 shadow-[0_0_20px_-5px_#00F0FF]">
|
||||
<BrainCircuit size={28} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold mb-4 tracking-tight">Dynamic Generation</h3>
|
||||
<p className="text-muted-foreground leading-relaxed text-lg">
|
||||
Custom problems are generated on the fly, matching the exact difficulty and NTA pattern required for growth.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="bg-secondary/10 backdrop-blur-xl border-border/50 hover:border-primary/50 transition-all duration-300 rounded-[2rem] h-full shadow-lg group">
|
||||
<CardContent className="p-8 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity transform group-hover:scale-110 group-hover:-rotate-12 duration-500">
|
||||
<Zap size={120} />
|
||||
</div>
|
||||
<div className="size-14 rounded-2xl bg-primary/20 text-primary flex items-center justify-center mb-8 shadow-[0_0_20px_-5px_var(--color-primary)]">
|
||||
<Zap size={28} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold mb-4 tracking-tight">Instant Resolution</h3>
|
||||
<p className="text-muted-foreground leading-relaxed text-lg">
|
||||
Get interactive, step-by-step AI breakdowns for every wrong answer. Ask follow-up questions until you master it.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Animated Pricing */}
|
||||
<section id="pricing" className="py-24 px-6 max-w-7xl mx-auto relative">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-[600px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -z-10"></div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<h2 className="text-4xl md:text-6xl font-bold tracking-tighter mb-6">Simple, Transparent Pricing</h2>
|
||||
<p className="text-muted-foreground text-xl max-w-2xl mx-auto">Unlock the ultimate unfair advantage for your NEET/JEE preparation.</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-4xl mx-auto items-stretch">
|
||||
{/* Free Plan
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, type: "spring" }}
|
||||
>
|
||||
<Card className="bg-background/80 backdrop-blur-md border-border p-6 rounded-[2rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
||||
<h3 className="text-3xl font-bold mb-2">Free</h3>
|
||||
<p className="text-muted-foreground mb-6 text-base">Perfect to get started.</p>
|
||||
<div className="text-6xl font-bold tracking-tighter mb-6">₹0<span className="text-xl text-white/60 font-medium tracking-normal ml-1">/year</span></div>
|
||||
<ul className="space-y-3 mb-6 flex-1 font-medium text-lg">
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> 1 Daily Practice Sheet</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Basic Performance Analytics</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-white" /> Community Access</li>
|
||||
</ul>
|
||||
<Link href="/signup">
|
||||
<Button variant="outline" size="lg" className="w-full rounded-2xl h-12 border-white text-white font-bold text-lg hover:bg-secondary hover:text-white">Get Started Free</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</motion.div>
|
||||
*/}
|
||||
|
||||
{/* Pro Plan */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, type: "spring", delay: 0.1 }}
|
||||
>
|
||||
<Card className="bg-background/80 backdrop-blur-md border-border p-6 rounded-[2rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
||||
<h3 className="text-3xl font-bold mb-2 text-yellow-500">Pro</h3>
|
||||
<p className="text-muted-foreground mb-6 text-base">Perfect for self-paced practice.</p>
|
||||
<div className="text-6xl font-bold tracking-tighter mb-6 text-yellow-500">₹3,499<span className="text-xl text-yellow-500/60 font-medium tracking-normal ml-1">/year</span></div>
|
||||
<ul className="space-y-3 mb-6 flex-1 font-medium text-lg">
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Unlimited Daily Practice Sheets</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Weekly Test Series & Mock Tests</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Detailed Performance Analytics</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-yellow-500" /> Parent Dashboard Access</li>
|
||||
</ul>
|
||||
<Link href="/signup">
|
||||
<Button size="lg" className="w-full rounded-2xl h-12 bg-yellow-500 text-black font-bold text-lg hover:bg-yellow-400">Get Pro</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* AI Coach Plan */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, type: "spring", delay: 0.2 }}
|
||||
className="relative mt-8 lg:mt-0"
|
||||
>
|
||||
<Card className="bg-background/80 backdrop-blur-md border-border p-6 rounded-[2rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
|
||||
<h3 className="text-3xl font-bold mb-2 text-[#00F0FF]">AI Coach</h3>
|
||||
<p className="text-muted-foreground mb-6 text-base">The complete AI-driven system.</p>
|
||||
<div className="text-6xl font-bold tracking-tighter mb-6 text-[#00F0FF]">₹11,999<span className="text-xl text-[#00F0FF]/60 font-medium tracking-normal ml-1">/year</span></div>
|
||||
<ul className="space-y-3 mb-6 flex-1 font-medium text-lg">
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Everything in Pro</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> AI Spaced Repetition (Mistakes)</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> 24/7 Streaming AI Tutor Chat</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Predictive Rank & College Predictor</li>
|
||||
</ul>
|
||||
<Link href="/signup">
|
||||
<Button size="lg" className="w-full rounded-2xl h-12 font-bold text-lg bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 transition-all">Get AI Coach</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
<p className="text-center mt-8 text-sm text-muted-foreground font-medium w-full">
|
||||
* All above prices are inclusive of GST.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Tier Comparison Matrix */}
|
||||
<section className="py-24 px-6 max-w-5xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-5xl font-bold tracking-tighter mb-4">Compare Features</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">See exactly what you get with each PragatiVeer plan.</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-2xl border border-border/50 bg-secondary/10 backdrop-blur-sm p-3 md:p-6">
|
||||
<table className="w-full text-left border-collapse min-w-[700px]">
|
||||
<thead>
|
||||
<tr className="border-b border-border/50 bg-background/50">
|
||||
<th className="p-4 md:p-6 font-bold text-lg w-1/2 border-l border-t border-border/50">Feature</th>
|
||||
{/* <th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 border-x border-t border-white">Free</th> */}
|
||||
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/4 text-yellow-500 border-x border-t border-yellow-500">Pro</th>
|
||||
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/4 text-[#00F0FF] bg-[#00F0FF]/5 border-x border-t border-[#00F0FF]">AI Coach</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border/30 text-sm md:text-base">
|
||||
{[
|
||||
{ name: "Daily Practice Sheets", free: "1/day", pro: "Unlimited", ai: "Unlimited" },
|
||||
{ name: "Answer Keys", free: true, pro: true, ai: true },
|
||||
{ name: "Basic Progress Dashboard", free: true, pro: true, ai: true },
|
||||
{ name: "Streak Tracking", free: true, pro: true, ai: true },
|
||||
{ name: "Leaderboard (City/School)", free: true, pro: true, ai: true },
|
||||
{ name: "Topic-wise Micro-tests", free: "5/day", pro: "Unlimited", ai: "Unlimited" },
|
||||
{ name: "Weekly Test Series", free: false, pro: true, ai: true },
|
||||
{ name: "Full-Length Mock Tests", free: false, pro: "4/month", ai: "Unlimited" },
|
||||
{ name: "Previous Year Papers", free: false, pro: true, ai: true },
|
||||
{ name: "Detailed Performance Analytics", free: false, pro: true, ai: true },
|
||||
{ name: "Chapter-wise Progress", free: false, pro: true, ai: true },
|
||||
{ name: "Doubt Resolution (Community)", free: false, pro: true, ai: true },
|
||||
{ name: "Parent Dashboard", free: false, pro: true, ai: true },
|
||||
{ name: "Offline Mode (Download DPS)", free: false, pro: true, ai: true },
|
||||
{ name: "AI Mistake Tracking", free: false, pro: false, ai: true },
|
||||
{ name: "Spaced Repetition", free: false, pro: false, ai: true },
|
||||
{ name: "Personalized Daily Study Plan", free: false, pro: false, ai: true },
|
||||
{ name: "AI Tutor Chat", free: false, pro: false, ai: true },
|
||||
{ name: "Exam Readiness Score", free: false, pro: false, ai: true },
|
||||
{ name: "Exam Day Strategy", free: false, pro: false, ai: true },
|
||||
{ name: "College Predictor", free: false, pro: false, ai: true },
|
||||
{ name: "Study Time Optimization", free: false, pro: false, ai: true },
|
||||
].map((feature, idx) => (
|
||||
<tr key={idx} className="hover:bg-background/40 transition-colors">
|
||||
<td className="p-4 md:p-6 font-medium border-y border-l border-border/30">{feature.name}</td>
|
||||
{/* <td className="p-4 md:p-6 text-center text-muted-foreground border border-white">
|
||||
{typeof feature.free === 'boolean' ? (feature.free ? <CheckCircle2 className="mx-auto text-white" size={20} /> : <Minus className="mx-auto text-white/30" size={20} />) : <span className="font-semibold text-white">{feature.free}</span>}
|
||||
</td> */}
|
||||
<td className="p-4 md:p-6 text-center border border-yellow-500">
|
||||
{typeof feature.pro === 'boolean' ? (feature.pro ? <CheckCircle2 className="mx-auto text-yellow-500" size={20} /> : <Minus className="mx-auto text-yellow-500/30" size={20} />) : <span className="font-semibold text-yellow-500">{feature.pro}</span>}
|
||||
</td>
|
||||
<td className="p-4 md:p-6 text-center bg-[#00F0FF]/5 border border-[#00F0FF]">
|
||||
{typeof feature.ai === 'boolean' ? (feature.ai ? <CheckCircle2 className="mx-auto text-[#00F0FF]" size={20} /> : <Minus className="mx-auto text-[#00F0FF]/30" size={20} />) : <span className="font-semibold text-[#00F0FF]">{feature.ai}</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Global Footer (Static) */}
|
||||
<footer className="relative z-10 bg-background/90 backdrop-blur-md border-t border-border pt-16 pb-8">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
||||
<div className="md:col-span-1 space-y-4">
|
||||
<div className="relative h-12 w-64 mb-6">
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-medium leading-relaxed">
|
||||
Re-engineering NEET & JEE preparation with adaptive AI models. Learn faster, rank higher.
|
||||
</p>
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
{/*
|
||||
<Link href="#" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg>
|
||||
</Link>
|
||||
<Link href="#" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
|
||||
</Link>
|
||||
<Link href="#" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
|
||||
</Link>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-bold mb-6 tracking-tight text-foreground">Product</h4>
|
||||
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
|
||||
<li><Link href="#features" className="hover:text-primary transition-colors">Features</Link></li>
|
||||
<li><Link href="#pricing" className="hover:text-primary transition-colors">Pricing</Link></li>
|
||||
<li><Link href="/dashboard" className="hover:text-primary transition-colors">Student Dashboard</Link></li>
|
||||
<li><Link href="/admin" className="hover:text-[#00F0FF] transition-colors">Admin Panel</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-bold mb-6 tracking-tight text-foreground">Company</h4>
|
||||
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
|
||||
<li><Link href="/about" className="hover:text-primary transition-colors">About Us</Link></li>
|
||||
<li><Link href="/contact" className="hover:text-primary transition-colors">Contact</Link></li>
|
||||
<li><Link href="#" className="hover:text-primary transition-colors">Careers</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-bold mb-6 tracking-tight text-foreground">Legal</h4>
|
||||
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
|
||||
<li><Link href="/terms" className="hover:text-primary transition-colors">Terms & Conditions</Link></li>
|
||||
<li><Link href="/privacy" className="hover:text-primary transition-colors">Privacy Policy</Link></li>
|
||||
<li><Link href="/refund" className="hover:text-primary transition-colors">Refund Policy</Link></li>
|
||||
<li><Link href="/cancellation" className="hover:text-primary transition-colors">Cancellation Policy</Link></li>
|
||||
<li><Link href="/disclaimer" className="hover:text-primary transition-colors">Disclaimer</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-8 flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p className="text-sm text-muted-foreground font-medium">© {new Date().getFullYear()} PragatiVeer. All rights reserved.</p>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground font-medium">
|
||||
Made with <div className="size-2 rounded-full bg-primary animate-pulse"></div> by AI Agents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
200
src/app/privacy-policy/page.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<h1 className="text-4xl md:text-5xl font-black tracking-tight mb-12 text-foreground">
|
||||
PragatiVeer Privacy Policy
|
||||
</h1>
|
||||
<div className="prose prose-invert prose-lg max-w-none text-muted-foreground">
|
||||
|
||||
<p className="text-muted-foreground mb-8"><strong>Last Updated:</strong> 27 July 2026<br/>
|
||||
<strong>Version:</strong> 3.0<br/>
|
||||
<strong>Document Ref:</strong> PV-LEG-PP-001<br/>
|
||||
<strong>Applicable To:</strong> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">1. Introduction</h2>
|
||||
<p>PragatiVeer (“we”, “our”, “us”) is an exam preparation platform operated by <strong>Gyannetra Pvt Ltd</strong> (CIN: U72900PN2015PTC156021), registered office at OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India.</p>
|
||||
<p>This Privacy Policy explains how we collect, use, store, share, and protect your personal data when you use PragatiVeer. It covers your rights under the <strong>Digital Personal Data Protection Act, 2023 (DPDP Act)</strong> of India.</p>
|
||||
<p>By using PragatiVeer, you acknowledge that you have read and understood this policy.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">2. What Data We Collect</h2>
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">2.1 Information You Provide Directly</h3>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Category</th>
|
||||
<th className="p-3 border">Examples</th>
|
||||
<th className="p-3 border">Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Account Information</td><td className="p-3 border">Name, email address, phone number</td><td className="p-3 border">Account creation, service delivery, communication</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Academic Information</td><td className="p-3 border">Class/grade, target exam, school name, city</td><td className="p-3 border">Personalising content, weak-area analysis</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Payment Information</td><td className="p-3 border">Transaction ID, billing name (we do <strong>not</strong> store full card or UPI details)</td><td className="p-3 border">Subscription billing, invoicing</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Communications</td><td className="p-3 border">Messages sent via email or contact forms</td><td className="p-3 border">Support, feedback, grievance handling</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">2.2 Information Collected Automatically</h3>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Category</th>
|
||||
<th className="p-3 border">Examples</th>
|
||||
<th className="p-3 border">Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Usage Data</td><td className="p-3 border">DPS attempts, scores, time spent, weak-area progression</td><td className="p-3 border">Personalising the learning experience</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Technical Data</td><td className="p-3 border">IP address, browser type, device type, operating system</td><td className="p-3 border">Service optimisation, fraud prevention, analytics</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">2.3 Information from Third Parties</h3>
|
||||
<p>We may receive transaction confirmation data from payment gateways but <strong>not</strong> full financial instrument details.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">3. How We Use Your Data</h2>
|
||||
<p>We use your personal data only for:</p>
|
||||
<ol className="list-decimal pl-6 mb-4 space-y-2">
|
||||
<li><strong>Service Delivery</strong> — Personalised DPS, weak-area tracking, content delivery</li>
|
||||
<li><strong>Personalisation</strong> — Adapting question difficulty based on performance</li>
|
||||
<li><strong>Communication</strong> — Subscription reminders, service updates, support</li>
|
||||
<li><strong>Payment Processing</strong> — Billing and subscription management</li>
|
||||
<li><strong>Improvement</strong> — Analysing aggregate data to improve content quality</li>
|
||||
<li><strong>Legal Compliance</strong> — Fulfilling obligations under DPDP Act and tax laws</li>
|
||||
<li><strong>Security</strong> — Detecting fraud, abuse, and unauthorised access</li>
|
||||
</ol>
|
||||
<p>We do <strong>NOT</strong>:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Sell your personal data</li>
|
||||
<li>Use your data to train AI models without explicit consent</li>
|
||||
<li>Share your data for advertising</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">4. Users Under 18 Years of Age</h2>
|
||||
<p>PragatiVeer is designed for students preparing for competitive exams, including those under 18 years of age.</p>
|
||||
<p>By using PragatiVeer, users under 18 years of age represent and warrant that they have obtained the necessary consent from their parent or lawful guardian to use the Platform and share their personal data. PragatiVeer relies on this representation and shall not be liable for verifying parental consent.</p>
|
||||
<p>We encourage parents and guardians to monitor their child’s online activities and review this Privacy Policy.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">5. Data Sharing</h2>
|
||||
<p>We share your data only with trusted service providers bound by data protection agreements:</p>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Provider</th>
|
||||
<th className="p-3 border">Purpose</th>
|
||||
<th className="p-3 border">Safeguards</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Payment Gateway</td><td className="p-3 border">Payment processing</td><td className="p-3 border">PCI-DSS compliant; no full payment details stored</td></tr>
|
||||
<tr><td className="p-3 border font-bold">AI Inference Providers</td><td className="p-3 border">Weak-area analysis, DPS generation</td><td className="p-3 border">Data anonymised before processing</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Self-hosted Infrastructure</td><td className="p-3 border">Storage, backups, disaster recovery</td><td className="p-3 border">No third-party access</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>We do <strong>not</strong> share your data with advertising networks, data brokers, or for monetisation.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">6. Data Retention & Deletion</h2>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Data Category</th>
|
||||
<th className="p-3 border">Retention Period</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Account Information</td><td className="p-3 border">Account duration + 12 months</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Academic Performance Data</td><td className="p-3 border">Account duration + 24 months</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Payment Records</td><td className="p-3 border">7 years (as required by tax laws)</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Communication Records</td><td className="p-3 border">12 months</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>You may request account deletion by emailing <strong>contact@pragativeer.com</strong> with subject “Account Deletion Request”. We will delete your data within <strong>30 days</strong>.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">7. Data Security</h2>
|
||||
<p>We implement:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Encryption in transit (TLS 1.2+)</li>
|
||||
<li>Encryption at rest</li>
|
||||
<li>Role-based access controls</li>
|
||||
<li>Self-hosted infrastructure</li>
|
||||
<li>Documented breach notification process</li>
|
||||
</ul>
|
||||
<p>In the event of a data breach, we will notify the Data Protection Board of India and affected users within <strong>72 hours</strong>.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">8. Your Rights Under DPDP Act 2023</h2>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Right</th>
|
||||
<th className="p-3 border">How to Exercise</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Access your data</td><td className="p-3 border">Email contact@pragativeer.com</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Correct your data</td><td className="p-3 border">Update in account settings or email us</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Delete your data</td><td className="p-3 border">Email with subject “Account Deletion Request”</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Withdraw consent</td><td className="p-3 border">Email contact@pragativeer.com</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Lodge a grievance</td><td className="p-3 border">Contact Grievance Officer (Section 9)</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Nominate a representative</td><td className="p-3 border">Email with subject “Nomination Request”</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>We respond to all requests within <strong>30 days</strong>.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">9. Grievance Officer</h2>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Detail</th>
|
||||
<th className="p-3 border">Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Name</td><td className="p-3 border">Himani Kachhawaha</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Email</td><td className="p-3 border">grievance@pragativeer.com</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Phone</td><td className="p-3 border">+91 9079337961</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Address</td><td className="p-3 border">OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Response Time</td><td className="p-3 border">Acknowledged within 24 hours; resolved within 30 days</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">10. Cookies</h2>
|
||||
<p>Our website uses essential cookies for functionality and anonymous analytics cookies. We do <strong>not</strong> use advertising cookies, cross-site tracking, or fingerprinting scripts.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">11. Changes to This Policy</h2>
|
||||
<p>We will notify you of material changes via email with at least <strong>7 days’ notice</strong>.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">12. Contact</h2>
|
||||
<p><strong>Email:</strong> contact@pragativeer.com<br/>
|
||||
<strong>Address:</strong> OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India<br/>
|
||||
<strong>Phone:</strong> +91 9079337961</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">13. Governing Law</h2>
|
||||
<p>This Policy is governed by the laws of India. Disputes are subject to the exclusive jurisdiction of courts in Pune, Maharashtra.</p>
|
||||
|
||||
<div className="mt-8 p-6 bg-secondary/20 rounded-xl">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>CIN: U72900PN2015PTC156021</p>
|
||||
<p>OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
<p>pragativeer.com</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
src/app/privacy/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { PublicLayout } from "@/components/Landing/PublicLayout";
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="bg-background min-h-screen pt-24 pb-16">
|
||||
<div className="container max-w-4xl mx-auto px-4 md:px-8">
|
||||
<div className="bg-secondary/20 p-8 md:p-12 rounded-3xl border border-border shadow-sm">
|
||||
<div className="mb-10 border-b border-border pb-6">
|
||||
<h1 className="text-3xl md:text-5xl font-black mb-4">PragatiVeer Privacy Policy</h1>
|
||||
<p className="text-muted-foreground font-medium">Gyannetra Pvt Ltd</p>
|
||||
<div className="mt-4 space-y-1 text-sm text-muted-foreground/80">
|
||||
<p><span className="font-semibold text-muted-foreground">Last Updated:</span> 27 July 2026</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Version:</span> 3.0</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Document Ref:</span> PV-LEG-PP-003</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Applicable To:</span> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 prose prose-invert max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-li:text-muted-foreground">
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">1. Introduction</h2>
|
||||
<p>
|
||||
Gyannetra Pvt Ltd ("we," "us," or "our") respects the privacy of our users ("user" or "you"). This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website pragativeer.com, including any other media form, media channel, mobile website, or mobile application related or connected thereto (collectively, the "Site").
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Please read this privacy policy carefully. If you do not agree with the terms of this privacy policy, please do not access the site.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">2. Collection of Your Information</h2>
|
||||
<p>
|
||||
We may collect information about you in a variety of ways. The information we may collect on the Site includes:
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">2.1 Personal Data</h3>
|
||||
<p>
|
||||
Personally identifiable information, such as your name, shipping address, email address, and telephone number, and demographic information, such as your age, gender, hometown, and educational interests, that you voluntarily give to us when you register with the Site or when you choose to participate in various activities related to the Site, such as online chat and message boards.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">2.2 Educational and Performance Data</h3>
|
||||
<p>
|
||||
Because PragatiVeer is an adaptive learning platform, we collect detailed data regarding your test performance (CBT), daily practice problems (DPPs) attempted, time spent on questions, accuracy rates, and identified weak areas. This data is core to providing our AI-driven personalization services.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">2.3 Derivative Data</h3>
|
||||
<p>
|
||||
Information our servers automatically collect when you access the Site, such as your IP address, your browser type, your operating system, your access times, and the pages you have viewed directly before and after accessing the Site.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">2.4 Financial Data</h3>
|
||||
<p>
|
||||
Financial information, such as data related to your payment method (e.g., valid credit card number, card brand, expiration date) that we may collect when you purchase, order, return, exchange, or request information about our services from the Site. We store only very limited, if any, financial information that we collect. Otherwise, all financial information is stored by our payment processor, and you are encouraged to review their privacy policy and contact them directly for responses to your questions.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">3. Use of Your Information</h2>
|
||||
<p>
|
||||
Having accurate information about you permits us to provide you with a smooth, efficient, and customized experience. Specifically, we may use information collected about you via the Site to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>Create and manage your account.</li>
|
||||
<li>Deliver targeted educational content, customized CBT exams, and adaptive DPPs based on your performance history.</li>
|
||||
<li>Process your purchases, subscriptions, and payments.</li>
|
||||
<li>Email you regarding your account, subscription status, or test results.</li>
|
||||
<li>Fulfill and manage purchases, orders, payments, and other transactions related to the Site.</li>
|
||||
<li>Generate a personal profile about you to make future visits to the Site more personalized.</li>
|
||||
<li>Increase the efficiency and operation of the Site.</li>
|
||||
<li>Monitor and analyze usage and trends to improve your experience with the Site.</li>
|
||||
<li>Notify you of updates to the Site or our educational offerings.</li>
|
||||
<li>Request feedback and contact you about your use of the Site.</li>
|
||||
<li>Resolve disputes and troubleshoot problems.</li>
|
||||
<li>Respond to product and customer service requests.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">4. Disclosure of Your Information</h2>
|
||||
<p>
|
||||
We may share information we have collected about you in certain situations. Your information may be disclosed as follows:
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">4.1 By Law or to Protect Rights</h3>
|
||||
<p>
|
||||
If we believe the release of information about you is necessary to respond to legal process, to investigate or remedy potential violations of our policies, or to protect the rights, property, and safety of others, we may share your information as permitted or required by any applicable law, rule, or regulation.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">4.2 Third-Party Service Providers</h3>
|
||||
<p>
|
||||
We may share your information with third parties that perform services for us or on our behalf, including payment processing, data analysis, email delivery, hosting services, customer service, and marketing assistance.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-4 mb-2">4.3 Educational Institutions/Parents (If applicable)</h3>
|
||||
<p>
|
||||
If your account was provisioned through a school/coaching institute partnership, or if a parent/guardian account is linked to your student account, we may share your performance and usage data with those authorized parties.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">5. Data Security</h2>
|
||||
<p>
|
||||
We use administrative, technical, and physical security measures to help protect your personal information. While we have taken reasonable steps to secure the personal information you provide to us, please be aware that despite our efforts, no security measures are perfect or impenetrable, and no method of data transmission can be guaranteed against any interception or other type of misuse.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">6. Policy for Children</h2>
|
||||
<p>
|
||||
We do not knowingly solicit information from or market to children under the age of 13. If you become aware of any data we have collected from children under age 13 without parental consent, please contact us using the contact information provided below. We encourage parents and legal guardians to monitor their children's Internet usage and to help enforce this Policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">7. Contact Us</h2>
|
||||
<p>
|
||||
If you have questions or comments about this Privacy Policy, please contact us at:
|
||||
</p>
|
||||
<div className="mt-4 p-4 bg-background rounded-xl border border-border text-sm">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>Email: contact@pragativeer.com</p>
|
||||
<p>Phone: +91 9079337961</p>
|
||||
<p>Address: OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
96
src/app/refund-policy/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function RefundPolicyPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<h1 className="text-4xl md:text-5xl font-black tracking-tight mb-12 text-foreground">
|
||||
PragatiVeer Refund Policy
|
||||
</h1>
|
||||
<div className="prose prose-invert prose-lg max-w-none text-muted-foreground">
|
||||
|
||||
<p className="text-muted-foreground mb-8"><strong>Last Updated:</strong> 27 July 2026<br/>
|
||||
<strong>Version:</strong> 1.0<br/>
|
||||
<strong>Document Ref:</strong> PV-LEG-RP-001<br/>
|
||||
<strong>Applicable To:</strong> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">1. Overview</h2>
|
||||
<p>This Refund Policy governs all paid subscriptions on PragatiVeer, operated by <strong>Gyannetra Pvt Ltd</strong> (CIN: U72900PN2015PTC156021).</p>
|
||||
<p>By purchasing a subscription, you agree to the terms of this policy.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">2. Subscription Plans</h2>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Plan</th>
|
||||
<th className="p-3 border">Price (incl. GST)</th>
|
||||
<th className="p-3 border">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Pro</td><td className="p-3 border">₹3,499</td><td className="p-3 border">Per annum</td></tr>
|
||||
<tr><td className="p-3 border font-bold">AI Coach</td><td className="p-3 border">₹11,999</td><td className="p-3 border">Per annum</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">3. Refund Eligibility</h2>
|
||||
<p>You may request a full refund within <strong>7 days</strong> of purchase provided that you have <strong>not accessed more than 5 DPS sheets</strong> during that period.</p>
|
||||
<p>Refunds will <strong>not</strong> be issued if:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>More than 7 days have passed since purchase</li>
|
||||
<li>You have accessed more than 5 DPS sheets</li>
|
||||
<li>The request is made after the subscription has been active beyond the refund window</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">4. How to Request a Refund</h2>
|
||||
<p>To request a refund, email <strong>contact@pragativeer.com</strong> with:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li><strong>Subject:</strong> “Refund Request”</li>
|
||||
<li><strong>Your registered email address or phone number</strong></li>
|
||||
<li><strong>Order/transaction ID</strong> (if available)</li>
|
||||
</ul>
|
||||
<p>We will process your request within <strong>7-10 business days</strong> and the refund will be credited to the original payment method.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">5. Non-Refundable Items</h2>
|
||||
<p>The following are non-refundable:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Subscription fees beyond the 7-day refund window</li>
|
||||
<li>Discounted or promotional purchases (unless otherwise stated)</li>
|
||||
<li>Any add-on services purchased separately</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">6. Cancellation</h2>
|
||||
<p>You may cancel your subscription at any time. However, cancellation does not entitle you to a refund unless it falls within the refund window described in Section 3. Upon cancellation, your access will continue until the end of the current billing period.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">7. Contact</h2>
|
||||
<p>For any questions regarding this Refund Policy, contact us at <strong>contact@pragativeer.com</strong> or reach out to our Grievance Officer:</p>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Detail</th>
|
||||
<th className="p-3 border">Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Grievance Officer</td><td className="p-3 border">Himani Kachhawaha</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Email</td><td className="p-3 border">grievance@pragativeer.com</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Phone</td><td className="p-3 border">+91 9079337961</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-6 bg-secondary/20 rounded-xl">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>CIN: U72900PN2015PTC156021</p>
|
||||
<p>OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
<p>pragativeer.com</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
src/app/refund/page.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { PublicLayout } from "@/components/Landing/PublicLayout";
|
||||
|
||||
export default function RefundPolicyPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="bg-background min-h-screen pt-24 pb-16">
|
||||
<div className="container max-w-4xl mx-auto px-4 md:px-8">
|
||||
<div className="bg-secondary/20 p-8 md:p-12 rounded-3xl border border-border shadow-sm">
|
||||
<div className="mb-10 border-b border-border pb-6">
|
||||
<h1 className="text-3xl md:text-5xl font-black mb-4">PragatiVeer Refund Policy</h1>
|
||||
<p className="text-muted-foreground font-medium">Gyannetra Pvt Ltd</p>
|
||||
<div className="mt-4 space-y-1 text-sm text-muted-foreground/80">
|
||||
<p><span className="font-semibold text-muted-foreground">Last Updated:</span> 27 July 2026</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Version:</span> 1.0</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Document Ref:</span> PV-LEG-RP-001</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Applicable To:</span> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 prose prose-invert max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-li:text-muted-foreground">
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">1. Overview</h2>
|
||||
<p>
|
||||
This Refund Policy governs all paid subscriptions on PragatiVeer, operated by <strong>Gyannetra Pvt Ltd</strong> (CIN: U72900PN2015PTC156021).
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
By purchasing a subscription, you agree to the terms of this policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">2. Subscription Plans</h2>
|
||||
<div className="overflow-x-auto my-4">
|
||||
<table className="min-w-full border-collapse border border-border text-sm">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50">
|
||||
<th className="border border-border px-4 py-2 text-left">Plan</th>
|
||||
<th className="border border-border px-4 py-2 text-left">Price (incl. GST)</th>
|
||||
<th className="border border-border px-4 py-2 text-left">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">Pro</td>
|
||||
<td className="border border-border px-4 py-2">₹3,499</td>
|
||||
<td className="border border-border px-4 py-2">Per annum</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">AI Coach</td>
|
||||
<td className="border border-border px-4 py-2">₹11,999</td>
|
||||
<td className="border border-border px-4 py-2">Per annum</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">3. Refund Eligibility</h2>
|
||||
<p>
|
||||
You may request a full refund within <strong>7 days</strong> of purchase provided that you have <strong>not accessed more than 5 DPS sheets</strong> during that period.
|
||||
</p>
|
||||
<p className="mt-4">Refunds will <strong>not</strong> be issued if:</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>More than 7 days have passed since purchase</li>
|
||||
<li>You have accessed more than 5 DPS sheets</li>
|
||||
<li>The request is made after the subscription has been active beyond the refund window</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">4. How to Request a Refund</h2>
|
||||
<p>To request a refund, email <strong>contact@pragativeer.com</strong> with:</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li><strong>Subject:</strong> “Refund Request”</li>
|
||||
<li><strong>Your registered email address or phone number</strong></li>
|
||||
<li><strong>Order/transaction ID</strong> (if available)</li>
|
||||
</ul>
|
||||
<p className="mt-4">
|
||||
We will process your request within <strong>7-10 business days</strong> and the refund will be credited to the original payment method.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">5. Non-Refundable Items</h2>
|
||||
<p>The following are non-refundable:</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>Subscription fees beyond the 7-day refund window</li>
|
||||
<li>Discounted or promotional purchases (unless otherwise stated)</li>
|
||||
<li>Any add-on services purchased separately</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">6. Cancellation</h2>
|
||||
<p>
|
||||
You may cancel your subscription at any time. However, cancellation does not entitle you to a refund unless it falls within the refund window described in Section 3. Upon cancellation, your access will continue until the end of the current billing period.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">7. Contact</h2>
|
||||
<p>
|
||||
For any questions regarding this Refund Policy, contact us at <strong>contact@pragativeer.com</strong> or reach out to our Grievance Officer:
|
||||
</p>
|
||||
<div className="overflow-x-auto my-4">
|
||||
<table className="min-w-full border-collapse border border-border text-sm">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50">
|
||||
<th className="border border-border px-4 py-2 text-left">Detail</th>
|
||||
<th className="border border-border px-4 py-2 text-left">Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">Grievance Officer</td>
|
||||
<td className="border border-border px-4 py-2">Himani Kachhawaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">Email</td>
|
||||
<td className="border border-border px-4 py-2">grievance@pragativeer.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-2 font-medium">Phone</td>
|
||||
<td className="border border-border px-4 py-2">+91 9079337961</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mt-8 p-4 bg-background rounded-xl border border-border text-sm">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>CIN: U72900PN2015PTC156021</p>
|
||||
<p>OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
<p>pragativeer.com</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
215
src/app/signup/page.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ArrowLeft, Eye, EyeOff, CheckCircle2 } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Logo } from "@/components/Logo"
|
||||
|
||||
export default function SignupPage() {
|
||||
const [step, setStep] = useState(1)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showOTP, setShowOTP] = useState(false)
|
||||
const [password, setPassword] = useState("")
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const [firstName, setFirstName] = useState("")
|
||||
const [lastName, setLastName] = useState("")
|
||||
const [phone, setPhone] = useState("")
|
||||
const [termsAccepted, setTermsAccepted] = useState(false)
|
||||
|
||||
const isStep1Valid = firstName.trim() !== "" && lastName.trim() !== "" && phone.length === 10 && termsAccepted
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (step === 1) setStep(2)
|
||||
else setIsSubmitted(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-6 selection:bg-primary/20">
|
||||
<Link href="/" className="absolute top-8 left-8 text-muted-foreground hover:text-foreground flex items-center gap-2 transition-colors font-medium">
|
||||
<ArrowLeft size={16} /> Back to Home
|
||||
</Link>
|
||||
|
||||
<div className="w-full max-w-md space-y-8 animate-in fade-in slide-in-from-bottom-8 duration-700">
|
||||
<div className="flex justify-center mb-8">
|
||||
<Logo href="/" className="w-72" imageClassName="object-contain" height={80} />
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between text-xs font-bold text-muted-foreground">
|
||||
<span className={step >= 1 ? "text-primary" : ""}>Account Details</span>
|
||||
<span className={step >= 2 ? "text-primary" : ""}>Personalize Profile</span>
|
||||
</div>
|
||||
<div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="bg-primary h-full transition-all duration-500 ease-in-out"
|
||||
style={{ width: step === 1 ? '50%' : '100%' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSubmitted ? (
|
||||
<Card className="border-border bg-secondary/10 backdrop-blur-xl shadow-[0_0_50px_-20px_var(--color-primary)] rounded-[2rem] overflow-hidden text-center py-8">
|
||||
<CardContent className="space-y-6 pt-4 flex flex-col items-center">
|
||||
<CheckCircle2 size={64} className="text-primary mb-4" />
|
||||
<h2 className="text-3xl font-black tracking-tight text-foreground">Thanks for registering, {firstName}!</h2>
|
||||
<p className="text-muted-foreground text-sm font-medium px-4 leading-relaxed">
|
||||
We are currently integrating our payment gateway and will be live soon.
|
||||
</p>
|
||||
<Link href="/login" className="w-full mt-4">
|
||||
<Button className="w-full rounded-xl h-12 font-bold text-base bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-[0_0_20px_-5px_var(--color-primary)]">
|
||||
Go to Login
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="border-border bg-secondary/10 backdrop-blur-xl shadow-[0_0_50px_-20px_var(--color-primary)] rounded-[2rem] overflow-hidden">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<CardTitle className="text-2xl font-bold tracking-tight">
|
||||
{step === 1 ? "Create an account" : "Complete your profile"}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-muted-foreground">
|
||||
{step === 1 ? "Start your AI-powered preparation journey today." : "Help us personalize your study plan."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 pt-4">
|
||||
|
||||
{step === 1 ? (
|
||||
<div className="space-y-4 animate-in fade-in zoom-in-95">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">First Name</label>
|
||||
<input type="text" placeholder="John" value={firstName} onChange={(e) => setFirstName(e.target.value)} className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">Last Name</label>
|
||||
<input type="text" placeholder="Doe" value={lastName} onChange={(e) => setLastName(e.target.value)} className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">Phone Number</label>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center justify-center bg-secondary/50 border border-border rounded-xl px-3 text-sm font-bold text-muted-foreground">
|
||||
+91
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
maxLength={10}
|
||||
pattern="[0-9]*"
|
||||
value={phone}
|
||||
onInput={(e) => {
|
||||
e.currentTarget.value = e.currentTarget.value.replace(/[^0-9]/g, '');
|
||||
}}
|
||||
onChange={(e) => setPhone(e.target.value.replace(/[^0-9]/g, ''))}
|
||||
placeholder="9876543210"
|
||||
className="flex-1 bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
|
||||
/>
|
||||
{!showOTP && (
|
||||
<Button variant="secondary" onClick={() => setShowOTP(true)} className="h-[46px] rounded-xl font-bold">Send OTP</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showOTP && (
|
||||
<div className="space-y-2 animate-in slide-in-from-top-2">
|
||||
<label className="text-sm font-bold text-foreground flex justify-between">
|
||||
<span>Verification Code</span>
|
||||
<span className="text-primary text-xs">Resend in 30s</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{[1,2,3,4,5,6].map((i) => (
|
||||
<input key={i} type="text" maxLength={1} className="w-12 h-12 text-center text-lg font-bold bg-background border border-border rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-bold text-foreground">Password (Optional if using OTP)</label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all pr-10"
|
||||
/>
|
||||
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors">
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 pt-2">
|
||||
<input type="checkbox" id="terms" checked={termsAccepted} onChange={(e) => setTermsAccepted(e.target.checked)} className="mt-1 w-4 h-4 rounded border-border bg-background focus:ring-primary focus:ring-2 cursor-pointer" />
|
||||
<label htmlFor="terms" className="text-xs text-muted-foreground leading-relaxed">
|
||||
I agree to the <Link href="/terms-and-conditions" className="text-primary hover:underline">Terms & Conditions</Link> and <Link href="/privacy-policy" className="text-primary hover:underline">Privacy Policy</Link>.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-right-8">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">Target Exam</label>
|
||||
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
|
||||
<option value="jee_main" className="bg-background text-foreground font-medium">JEE Main</option>
|
||||
<option value="jee_adv" className="bg-background text-foreground font-medium">JEE Advanced</option>
|
||||
<option value="neet" className="bg-background text-foreground font-medium">NEET</option>
|
||||
<option value="jee_neet" className="bg-background text-foreground font-medium">JEE + NEET</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">Current Class</label>
|
||||
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
|
||||
<option value="11" className="bg-background text-foreground font-medium">Class 11th</option>
|
||||
<option value="12" className="bg-background text-foreground font-medium">Class 12th</option>
|
||||
<option value="dropper" className="bg-background text-foreground font-medium">Dropper</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">Study Medium</label>
|
||||
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all cursor-pointer">
|
||||
<option value="en" className="bg-background text-foreground font-medium">English</option>
|
||||
<option value="hi" className="bg-background text-foreground font-medium">Hindi (हिंदी)</option>
|
||||
<option value="bi" className="bg-background text-foreground font-medium">Bilingual (English + Hindi)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-foreground">School / Coaching Name <span className="text-muted-foreground font-normal">(Optional)</span></label>
|
||||
<input type="text" placeholder="e.g. DPS R.K. Puram" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleNextStep}
|
||||
disabled={step === 1 && !isStep1Valid}
|
||||
className="w-full rounded-xl h-12 font-bold text-base bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-[0_0_20px_-5px_var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none"
|
||||
>
|
||||
{step === 1 ? "Continue" : "Complete Setup"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{step === 1 ? (
|
||||
<p className="text-center text-sm text-muted-foreground font-medium">
|
||||
Already have an account? <Link href="/login" className="font-bold text-foreground hover:text-primary transition-colors">Log in</Link>
|
||||
</p>
|
||||
) : (
|
||||
<button onClick={() => setStep(1)} className="w-full text-center text-sm font-bold text-muted-foreground hover:text-foreground transition-colors">
|
||||
Go back
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
128
src/app/terms-and-conditions/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function TermsAndConditionsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<h1 className="text-4xl md:text-5xl font-black tracking-tight mb-12 text-foreground">
|
||||
PragatiVeer Terms & Conditions
|
||||
</h1>
|
||||
<div className="prose prose-invert prose-lg max-w-none text-muted-foreground">
|
||||
|
||||
<p className="text-muted-foreground mb-8"><strong>Last Updated:</strong> 27 July 2026<br/>
|
||||
<strong>Version:</strong> 3.0<br/>
|
||||
<strong>Document Ref:</strong> PV-LEG-TC-001<br/>
|
||||
<strong>Applicable To:</strong> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">1. Introduction</h2>
|
||||
<p>PragatiVeer (“we”, “our”, “us”) is an exam preparation platform operated by <strong>Gyannetra Pvt Ltd</strong> (CIN: U72900PN2015PTC156021), registered office at OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India.</p>
|
||||
<p>By accessing or using PragatiVeer, you agree to these Terms. If you do not agree, do not use the Platform.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">2. Eligibility</h2>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>You must be at least <strong>13 years of age</strong>.</li>
|
||||
<li>If you are between <strong>13 and 18 years</strong>, by using the Platform you represent and warrant that you have obtained the necessary consent from your parent or lawful guardian. PragatiVeer is not liable for verifying such consent.</li>
|
||||
<li>Parents or guardians are encouraged to supervise their child’s use of the Platform.</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">3. Account</h2>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>You are responsible for your account credentials and all activity under your account.</li>
|
||||
<li>Notify us immediately at <strong>contact@pragativeer.com</strong> of any unauthorised use.</li>
|
||||
<li>We may suspend or terminate your account if you violate these Terms or engage in harmful, fraudulent, or illegal activity.</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">4. Subscriptions & Payments</h2>
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">4.1 Plans</h3>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Plan</th>
|
||||
<th className="p-3 border">Price (incl. GST)</th>
|
||||
<th className="p-3 border">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Pro</td><td className="p-3 border">₹3,499</td><td className="p-3 border">Per annum</td></tr>
|
||||
<tr><td className="p-3 border font-bold">AI Coach</td><td className="p-3 border">₹11,999</td><td className="p-3 border">Per annum</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">4.2 Payment</h3>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>All payments are processed through our <strong>payment gateway partner</strong>. We do not store your full card or UPI details.</li>
|
||||
<li>Fees are non-refundable except as stated in our Refund Policy (Section 5).</li>
|
||||
<li>Prices may change with 30 days’ notice.</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-bold mt-6 mb-3">4.3 Auto-Renewal</h3>
|
||||
<p>Subscriptions auto-renew at the end of each billing period unless cancelled at least <strong>24 hours</strong> before renewal. You may cancel auto-renewal from your account settings or by emailing <strong>contact@pragativeer.com</strong>.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">5. Refund Policy</h2>
|
||||
<p>You may request a full refund within <strong>7 days</strong> of purchase if you have not accessed more than 5 DPS sheets. No refunds after 7 days. To request a refund, email <strong>contact@pragativeer.com</strong> with subject “Refund Request”.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">6. User Conduct</h2>
|
||||
<p>You agree to:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Use the Platform only for lawful purposes</li>
|
||||
<li>Not harass, abuse, or harm other users</li>
|
||||
<li>Not use bots, scrapers, or automated tools to access the Platform</li>
|
||||
<li>Not attempt to hack, reverse engineer, or disrupt the Platform</li>
|
||||
</ul>
|
||||
<p>Violation may result in account suspension or termination.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">7. Intellectual Property</h2>
|
||||
<p>All Content on PragatiVeer — questions, DPS, materials, logos, software — is owned by Gyannetra Pvt Ltd. You may not reproduce, distribute, or modify it without our written consent.</p>
|
||||
<p>You retain ownership of your responses and data. By submitting content, you grant us a license to store, process, and use anonymised data for service improvement.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">8. Privacy</h2>
|
||||
<p>Our data practices are governed by our <strong>Privacy Policy</strong> and the <strong>Digital Personal Data Protection Act, 2023</strong>. We do not sell your data.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">9. No Guarantee of Results</h2>
|
||||
<p>PragatiVeer is a practice tool. We do <strong>not</strong> guarantee any specific exam score, rank, or admission.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">10. Limitation of Liability</h2>
|
||||
<p>The Platform is provided “as is”. Our total liability shall not exceed the fees paid by you in the 12 months preceding a claim. We are not liable for indirect or consequential damages.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">11. Termination</h2>
|
||||
<p>You may delete your account anytime via settings or by emailing <strong>contact@pragativeer.com</strong>. We may terminate your account for breach of these Terms. Fees paid are non-refundable upon termination.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">12. Changes to Terms</h2>
|
||||
<p>We may update these Terms with <strong>7 days’ notice</strong> via email. Continued use after changes take effect constitutes acceptance.</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">13. Grievance Officer</h2>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-secondary/50 text-left">
|
||||
<th className="p-3 border">Detail</th>
|
||||
<th className="p-3 border">Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td className="p-3 border font-bold">Name</td><td className="p-3 border">Himani Kachhawaha</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Email</td><td className="p-3 border">grievance@pragativeer.com</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Phone</td><td className="p-3 border">+91 9079337961</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Address</td><td className="p-3 border">OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045</td></tr>
|
||||
<tr><td className="p-3 border font-bold">Response Time</td><td className="p-3 border">Acknowledged within 24 hours; resolved within 30 days</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">14. Governing Law</h2>
|
||||
<p>These Terms are governed by the laws of India. Disputes shall be resolved through binding arbitration in Pune, Maharashtra. The courts in Pune have exclusive jurisdiction.</p>
|
||||
|
||||
<div className="mt-8 p-6 bg-secondary/20 rounded-xl">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>CIN: U72900PN2015PTC156021</p>
|
||||
<p>OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
<p>pragativeer.com</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
src/app/terms/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { PublicLayout } from "@/components/Landing/PublicLayout";
|
||||
|
||||
export default function TermsAndConditionsPage() {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="bg-background min-h-screen pt-24 pb-16">
|
||||
<div className="container max-w-4xl mx-auto px-4 md:px-8">
|
||||
<div className="bg-secondary/20 p-8 md:p-12 rounded-3xl border border-border shadow-sm">
|
||||
<div className="mb-10 border-b border-border pb-6">
|
||||
<h1 className="text-3xl md:text-5xl font-black mb-4">PragatiVeer Terms & Conditions</h1>
|
||||
<p className="text-muted-foreground font-medium">Gyannetra Pvt Ltd</p>
|
||||
<div className="mt-4 space-y-1 text-sm text-muted-foreground/80">
|
||||
<p><span className="font-semibold text-muted-foreground">Last Updated:</span> 27 July 2026</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Version:</span> 3.0</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Document Ref:</span> PV-LEG-TC-003</p>
|
||||
<p><span className="font-semibold text-muted-foreground">Applicable To:</span> PragatiVeer website (pragativeer.com) and associated services.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 prose prose-invert max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-li:text-muted-foreground">
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">1. Agreement to Terms</h2>
|
||||
<p>
|
||||
These Terms and Conditions constitute a legally binding agreement made between you, whether personally or on behalf of an entity ("you") and Gyannetra Pvt Ltd ("we," "us," or "our"), concerning your access to and use of the PragatiVeer website as well as any other media form, media channel, mobile website, or mobile application related, linked, or otherwise connected thereto (collectively, the "Site").
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
You agree that by accessing the Site, you have read, understood, and agree to be bound by all of these Terms and Conditions. IF YOU DO NOT AGREE WITH ALL OF THESE TERMS AND CONDITIONS, THEN YOU ARE EXPRESSLY PROHIBITED FROM USING THE SITE AND YOU MUST DISCONTINUE USE IMMEDIATELY.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">2. Intellectual Property Rights</h2>
|
||||
<p>
|
||||
Unless otherwise indicated, the Site is our proprietary property and all source code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics on the Site (collectively, the "Content") and the trademarks, service marks, and logos contained therein (the "Marks") are owned or controlled by us or licensed to us, and are protected by copyright and trademark laws and various other intellectual property rights and unfair competition laws of India, international copyright laws, and international conventions.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Provided that you are eligible to use the Site, you are granted a limited license to access and use the Site and to download or print a copy of any portion of the Content to which you have properly gained access solely for your personal, non-commercial, educational use.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">3. User Representations</h2>
|
||||
<p>
|
||||
By using the Site, you represent and warrant that:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>All registration information you submit will be true, accurate, current, and complete.</li>
|
||||
<li>You will maintain the accuracy of such information and promptly update such registration information as necessary.</li>
|
||||
<li>You have the legal capacity and you agree to comply with these Terms and Conditions (If you are a minor, you have received parental permission to use the Site).</li>
|
||||
<li>You will not access the Site through automated or non-human means, whether through a bot, script, or otherwise.</li>
|
||||
<li>You will not use the Site for any illegal or unauthorized purpose.</li>
|
||||
<li>Your use of the Site will not violate any applicable law or regulation.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">4. User Registration</h2>
|
||||
<p>
|
||||
You may be required to register with the Site to access certain educational features like CBT and DPPs. You agree to keep your password confidential and will be responsible for all use of your account and password. We reserve the right to remove, reclaim, or change a username you select if we determine, in our sole discretion, that such username is inappropriate, obscene, or otherwise objectionable.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">5. Subscriptions and Payments</h2>
|
||||
<p>
|
||||
We offer various subscription plans (e.g., Free, Pro, AI Coach) for access to premium educational content. By selecting a premium plan, you agree to pay the fees specified for that plan. All fees are inclusive of GST unless otherwise stated.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Payments are processed securely through our third-party payment gateways. We do not store your full credit card or banking details on our servers.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
For details regarding refunds and cancellations, please refer to our separate <strong>Refund Policy</strong> and <strong>Cancellation Policy</strong> documents.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">6. Prohibited Activities</h2>
|
||||
<p>
|
||||
You may not access or use the Site for any purpose other than that for which we make the Site available. The Site may not be used in connection with any commercial endeavors except those that are specifically endorsed or approved by us.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
As a user of the Site, you agree not to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2 mt-2">
|
||||
<li>Systematically retrieve data or other content from the Site to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us.</li>
|
||||
<li>Circumvent, disable, or otherwise interfere with security-related features of the Site.</li>
|
||||
<li>Engage in unauthorized framing of or linking to the Site.</li>
|
||||
<li>Make improper use of our support services or submit false reports of abuse or misconduct.</li>
|
||||
<li>Engage in any automated use of the system, such as using scripts to send comments or messages, or using any data mining, robots, or similar data gathering and extraction tools.</li>
|
||||
<li>Interfere with, disrupt, or create an undue burden on the Site or the networks or services connected to the Site.</li>
|
||||
<li>Attempt to bypass any measures of the Site designed to prevent or restrict access to the Site, or any portion of the Site.</li>
|
||||
<li>Copy or adapt the Site's software, including but not limited to Flash, PHP, HTML, JavaScript, or other code.</li>
|
||||
<li>Share your account credentials with multiple individuals to bypass subscription fees. Account sharing is strictly prohibited and may result in immediate termination of your account without refund.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">7. Privacy Policy</h2>
|
||||
<p>
|
||||
We care about data privacy and security. Please review our <strong>Privacy Policy</strong>. By using the Site, you agree to be bound by our Privacy Policy, which is incorporated into these Terms and Conditions. Please be advised the Site is hosted in India.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">8. Modifications and Interruptions</h2>
|
||||
<p>
|
||||
We reserve the right to change, modify, or remove the contents of the Site at any time or for any reason at our sole discretion without notice. However, we have no obligation to update any information on our Site. We also reserve the right to modify or discontinue all or part of the Site without notice at any time.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
We will not be liable to you or any third party for any modification, price change, suspension, or discontinuance of the Site. We cannot guarantee the Site will be available at all times. We may experience hardware, software, or other problems or need to perform maintenance related to the Site, resulting in interruptions, delays, or errors.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">9. Governing Law</h2>
|
||||
<p>
|
||||
These Terms shall be governed by and defined following the laws of India. Gyannetra Pvt Ltd and yourself irrevocably consent that the courts of Pune, Maharashtra, India shall have exclusive jurisdiction to resolve any dispute which may arise in connection with these terms.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-bold border-b border-border pb-2 mb-4">10. Contact Us</h2>
|
||||
<p>
|
||||
In order to resolve a complaint regarding the Site or to receive further information regarding use of the Site, please contact us at:
|
||||
</p>
|
||||
<div className="mt-4 p-4 bg-background rounded-xl border border-border text-sm">
|
||||
<p className="font-bold">Gyannetra Pvt Ltd</p>
|
||||
<p>Email: contact@pragativeer.com</p>
|
||||
<p>Phone: +91 9079337961</p>
|
||||
<p>Address: OFF NO 201, 2ND FLOOR, SAI EMPIRE, S NO 183/1/2, Baner Gaon, Haveli, Pune - 411045, Maharashtra, India</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
291
src/app/upgrade/page.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Check, X, ShieldCheck, Loader2, Minus, CheckCircle2 } from "lucide-react"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const WEBHOOK_SECRET = "local_webhook_secret"
|
||||
|
||||
export default function UpgradePage() {
|
||||
const { userId, loading: userLoading, refreshUser } = useActiveUser()
|
||||
const [loadingTier, setLoadingTier] = useState<string | null>(null)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [orderData, setOrderData] = useState<any>(null)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const handleUpgrade = async (tier: string) => {
|
||||
if (userLoading || !userId) return
|
||||
setLoadingTier(tier)
|
||||
try {
|
||||
const res = await fetch(`/api/billing/create-order?user_id=${userId}&plan_tier=${tier}`, {
|
||||
method: "POST"
|
||||
})
|
||||
if (!res.ok) throw new Error("Order creation failed")
|
||||
const order = await res.json()
|
||||
setOrderData({ ...order, tier })
|
||||
setShowModal(true)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert("Failed to create order")
|
||||
} finally {
|
||||
setLoadingTier(null)
|
||||
}
|
||||
}
|
||||
|
||||
const generateMockSignature = async (payloadStr: string) => {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(WEBHOOK_SECRET),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payloadStr));
|
||||
return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
const simulatePayment = async () => {
|
||||
if (!userId) return
|
||||
setProcessing(true)
|
||||
try {
|
||||
const payloadObj = {
|
||||
event: "payment.captured",
|
||||
payload: {
|
||||
payment: {
|
||||
entity: {
|
||||
id: `pay_mock_${Date.now()}`,
|
||||
amount: orderData.amount,
|
||||
notes: {
|
||||
user_id: userId,
|
||||
tier_requested: orderData.tier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payloadStr = JSON.stringify(payloadObj)
|
||||
const signature = await generateMockSignature(payloadStr)
|
||||
|
||||
const res = await fetch(`/api/billing/webhook/razorpay`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Razorpay-Signature": signature
|
||||
},
|
||||
body: payloadStr
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error("Webhook failed")
|
||||
|
||||
setSuccess(true)
|
||||
await refreshUser()
|
||||
setTimeout(() => {
|
||||
window.location.href = "/dashboard"
|
||||
}, 2000)
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert("Payment simulation failed")
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||
<div className="text-center max-w-2xl mx-auto mb-16">
|
||||
<h1 className="text-4xl font-extrabold tracking-tight mb-4">Upgrade Your Preparation</h1>
|
||||
<p className="text-xl text-muted-foreground">Unlock the full power of PragatiVeer to master JEE & NEET.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-4xl mx-auto items-stretch">
|
||||
{/* Free Tier
|
||||
<Card className="bg-background/80 backdrop-blur-md border border-border p-6 rounded-[2rem] flex flex-col justify-between h-full z-10 relative shadow-xl hover:border-border/80 transition-colors">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold mb-2 text-foreground">Free</h3>
|
||||
<p className="text-muted-foreground mb-6 text-base">Perfect to get started.</p>
|
||||
<div className="text-6xl font-bold tracking-tighter mb-6 text-foreground">
|
||||
₹0<span className="text-xl text-white/60 font-medium tracking-normal ml-1">/year</span>
|
||||
</div>
|
||||
<ul className="space-y-3 mb-6 font-medium text-lg">
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-foreground shrink-0" size={24}/> 1 Daily Practice Sheet</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-foreground shrink-0" size={24}/> Basic Performance Analytics</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-foreground shrink-0" size={24}/> Community Access</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full rounded-2xl h-12 border-border text-foreground font-bold text-lg hover:bg-secondary" disabled>Get Started Free</Button>
|
||||
</Card>
|
||||
*/}
|
||||
|
||||
{/* Pro Tier */}
|
||||
<Card className="bg-background/80 backdrop-blur-md border border-border p-6 rounded-[2rem] flex flex-col justify-between h-full z-10 relative shadow-xl hover:border-border/80 transition-colors">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold mb-2 text-yellow-500">Pro</h3>
|
||||
<p className="text-muted-foreground mb-6 text-base">Perfect for self-paced practice.</p>
|
||||
<div className="text-6xl font-bold tracking-tighter mb-6 text-yellow-500">
|
||||
₹3,499<span className="text-xl text-yellow-500/60 font-medium tracking-normal ml-1">/year</span>
|
||||
</div>
|
||||
<ul className="space-y-3 mb-6 font-medium text-lg">
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Unlimited Daily Practice Sheets</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Weekly Test Series & Mock Tests</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Detailed Performance Analytics</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-yellow-500 shrink-0" size={24}/> Parent Dashboard Access</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full bg-yellow-500 text-black hover:bg-yellow-400 font-bold text-lg rounded-2xl h-12"
|
||||
onClick={() => handleUpgrade("Pro")}
|
||||
disabled={!!loadingTier}
|
||||
>
|
||||
{loadingTier === "Pro" ? <Loader2 className="animate-spin" /> : "Get Pro"}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* AI Coach Tier */}
|
||||
<Card className="bg-background/80 backdrop-blur-md border border-border p-6 rounded-[2rem] flex flex-col justify-between h-full z-10 relative shadow-xl hover:border-border/80 transition-colors">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold mb-2 text-[#00F0FF]">AI Coach</h3>
|
||||
<p className="text-muted-foreground mb-6 text-base">The complete AI-driven system.</p>
|
||||
<div className="text-6xl font-bold tracking-tighter mb-6 text-[#00F0FF]">
|
||||
₹11,999<span className="text-xl text-[#00F0FF]/60 font-medium tracking-normal ml-1">/year</span>
|
||||
</div>
|
||||
<ul className="space-y-3 mb-6 font-medium text-lg">
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> Everything in Pro</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> AI Spaced Repetition (Mistakes)</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> 24/7 Streaming AI Tutor Chat</li>
|
||||
<li className="flex items-center gap-4"><CheckCircle2 className="text-[#00F0FF] shrink-0" size={24}/> Predictive Rank & College Predictor</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full font-bold text-lg rounded-2xl h-12 bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 transition-all"
|
||||
onClick={() => handleUpgrade("Al_Coach")}
|
||||
disabled={!!loadingTier}
|
||||
>
|
||||
{loadingTier === "Al_Coach" ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<p className="text-center mt-8 text-sm text-muted-foreground font-medium w-full">
|
||||
* All above prices are inclusive of GST.
|
||||
</p>
|
||||
|
||||
{/* ── Tier Comparison Matrix ── */}
|
||||
<section className="mt-20 py-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight mb-3">Compare All Features</h2>
|
||||
<p className="text-muted-foreground text-base md:text-lg max-w-2xl mx-auto">
|
||||
Detailed breakdown of capabilities across Free, Pro, and AI Coach tiers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-2xl border border-border/50 bg-secondary/10 backdrop-blur-sm p-3 md:p-6 shadow-2xl">
|
||||
<table className="w-full text-left border-collapse min-w-[700px]">
|
||||
<thead>
|
||||
<tr className="border-b border-border/50 bg-background/50">
|
||||
<th className="p-4 md:p-6 font-bold text-lg w-1/2 border-l border-t border-border/50">Feature</th>
|
||||
{/* <th className="p-4 md:p-6 font-bold text-lg text-center w-1/5 border-x border-t border-white">Free</th> */}
|
||||
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/4 text-yellow-500 border-x border-t border-yellow-500">Pro</th>
|
||||
<th className="p-4 md:p-6 font-bold text-lg text-center w-1/4 text-[#00F0FF] bg-[#00F0FF]/5 border-x border-t border-[#00F0FF]">AI Coach</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border/30 text-sm md:text-base">
|
||||
{[
|
||||
{ name: "Daily Practice Sheets", free: "1/day", pro: "Unlimited", ai: "Unlimited" },
|
||||
{ name: "Answer Keys", free: true, pro: true, ai: true },
|
||||
{ name: "Basic Progress Dashboard", free: true, pro: true, ai: true },
|
||||
{ name: "Streak Tracking", free: true, pro: true, ai: true },
|
||||
{ name: "Leaderboard (City/School)", free: true, pro: true, ai: true },
|
||||
{ name: "Topic-wise Micro-tests", free: "5/day", pro: "Unlimited", ai: "Unlimited" },
|
||||
{ name: "Weekly Test Series", free: false, pro: true, ai: true },
|
||||
{ name: "Full-Length Mock Tests", free: false, pro: "4/month", ai: "Unlimited" },
|
||||
{ name: "Previous Year Papers", free: false, pro: true, ai: true },
|
||||
{ name: "Detailed Performance Analytics", free: false, pro: true, ai: true },
|
||||
{ name: "Chapter-wise Progress", free: false, pro: true, ai: true },
|
||||
{ name: "Doubt Resolution (Community)", free: false, pro: true, ai: true },
|
||||
{ name: "Parent Dashboard", free: false, pro: true, ai: true },
|
||||
{ name: "Offline Mode (Download DPS)", free: false, pro: true, ai: true },
|
||||
{ name: "AI Mistake Tracking", free: false, pro: false, ai: true },
|
||||
{ name: "Spaced Repetition (SM-2)", free: false, pro: false, ai: true },
|
||||
{ name: "Personalized Daily Study Plan", free: false, pro: false, ai: true },
|
||||
{ name: "AI Tutor Chat", free: false, pro: false, ai: true },
|
||||
{ name: "Exam Readiness Score", free: false, pro: false, ai: true },
|
||||
{ name: "Exam Day Strategy", free: false, pro: false, ai: true },
|
||||
{ name: "College Predictor", free: false, pro: false, ai: true },
|
||||
{ name: "Study Time Optimization", free: false, pro: false, ai: true },
|
||||
].map((feature, idx) => (
|
||||
<tr key={idx} className="hover:bg-background/40 transition-colors">
|
||||
<td className="p-4 md:p-6 font-medium border-y border-l border-border/30">{feature.name}</td>
|
||||
{/* <td className="p-4 md:p-6 text-center text-muted-foreground border border-white">
|
||||
{typeof feature.free === 'boolean' ? (feature.free ? <CheckCircle2 className="mx-auto text-white" size={20} /> : <Minus className="mx-auto text-white/30" size={20} />) : <span className="font-semibold text-white">{feature.free}</span>}
|
||||
</td> */}
|
||||
<td className="p-4 md:p-6 text-center border border-yellow-500">
|
||||
{typeof feature.pro === 'boolean' ? (feature.pro ? <CheckCircle2 className="mx-auto text-yellow-500" size={20} /> : <Minus className="mx-auto text-yellow-500/30" size={20} />) : <span className="font-semibold text-yellow-500">{feature.pro}</span>}
|
||||
</td>
|
||||
<td className="p-4 md:p-6 text-center bg-[#00F0FF]/5 border border-[#00F0FF]">
|
||||
{typeof feature.ai === 'boolean' ? (feature.ai ? <CheckCircle2 className="mx-auto text-[#00F0FF]" size={20} /> : <Minus className="mx-auto text-[#00F0FF]/30" size={20} />) : <span className="font-semibold text-[#00F0FF]">{feature.ai}</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mock Razorpay Modal */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<Card className="max-w-md w-full shadow-2xl border-primary">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<div className="mx-auto w-16 h-12 bg-blue-600 rounded-lg flex items-center justify-center mb-4">
|
||||
<span className="text-white font-black text-2xl">RZP</span>
|
||||
</div>
|
||||
<CardTitle>Mock Checkout Window</CardTitle>
|
||||
<CardDescription>Razorpay Test Sandbox Simulation</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 mt-4">
|
||||
<div className="bg-secondary p-4 rounded-xl space-y-2 text-sm font-mono">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Order ID:</span>
|
||||
<span className="font-bold">{orderData?.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Amount:</span>
|
||||
<span className="font-bold">₹{(orderData?.amount / 100).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Tier:</span>
|
||||
<span className="font-bold">{orderData?.tier}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{success ? (
|
||||
<div className="p-4 bg-green-500/10 text-green-500 rounded-xl text-center font-bold animate-in zoom-in">
|
||||
Payment Captured! Redirecting...
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Clicking confirm will securely sign and POST a payment.captured webhook to the backend.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex gap-3">
|
||||
<Button variant="outline" className="w-full" onClick={() => setShowModal(false)} disabled={processing || success}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={simulatePayment} disabled={processing || success}>
|
||||
{processing ? <Loader2 className="animate-spin" /> : "Confirm Payment"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
138
src/components/AI/AITutorChat.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Send, Bot, User, Minimize2, Maximize2, X } from "lucide-react"
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import 'katex/dist/katex.min.css' // Import Katex CSS for math rendering
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'ai';
|
||||
content: string;
|
||||
}
|
||||
|
||||
const INITIAL_MESSAGES: Message[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'ai',
|
||||
content: "Hi! I noticed you struggled with **Rotational Dynamics** in the last test. Let's look at a similar problem. \n\nA solid sphere rolls without slipping. The kinetic energy is $E = \\frac{1}{2}mv^2 + \\frac{1}{2}I\\omega^2$. \n\nWhat fraction of total kinetic energy is rotational?"
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'user',
|
||||
content: "I think it is $\\frac{2}{7}$?"
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
role: 'ai',
|
||||
content: "Excellent! Yes, because $I = \\frac{2}{5}mr^2$, the rotational kinetic energy is:\n\n$$ K_{rot} = \\frac{1}{2}(\\frac{2}{5}mr^2)(\\frac{v}{r})^2 = \\frac{1}{5}mv^2 $$\n\nTotal $K = \\frac{1}{2}mv^2 + \\frac{1}{5}mv^2 = \\frac{7}{10}mv^2$.\n\nSo, $\\frac{K_{rot}}{K_{total}} = \\frac{2/10}{7/10} = \\frac{2}{7}$. You nailed it!"
|
||||
}
|
||||
];
|
||||
|
||||
export function AITutorChat() {
|
||||
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
|
||||
const [input, setInput] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input };
|
||||
setMessages([...messages, newMessage]);
|
||||
setInput('');
|
||||
|
||||
// Mock AI response
|
||||
setTimeout(() => {
|
||||
setMessages(prev => [...prev, {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'ai',
|
||||
content: "That's an interesting point! Let's explore the math behind it: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$"
|
||||
}]);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed bottom-6 right-6 h-14 w-14 rounded-full shadow-[0_0_20px_-5px_var(--color-primary)] flex items-center justify-center z-50 bg-[#00F0FF] text-background hover:bg-[#00F0FF]/90 transition-transform hover:scale-105"
|
||||
>
|
||||
<Bot size={28} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={`fixed right-0 md:right-6 bottom-0 md:bottom-6 border-border bg-background/95 backdrop-blur-xl shadow-2xl z-50 flex flex-col overflow-hidden transition-all duration-300 ease-in-out
|
||||
${isExpanded ? 'w-full md:w-[600px] h-full md:h-[80vh] rounded-none md:rounded-2xl' : 'w-full md:w-[400px] h-[600px] rounded-t-2xl md:rounded-2xl'}
|
||||
`}>
|
||||
<CardHeader className="p-4 border-b border-border bg-secondary/30 flex flex-row items-center justify-between space-y-0 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/20 text-primary rounded-lg border border-primary/30">
|
||||
<Bot size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-bold text-foreground">AI Coach</CardTitle>
|
||||
<p className="text-xs font-medium text-green-500 flex items-center gap-1">
|
||||
<span className="size-2 bg-green-500 rounded-full animate-pulse"></span> Online
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
{isExpanded ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive" onClick={() => setIsOpen(false)}>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 p-4 overflow-y-auto space-y-4">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex gap-3 max-w-[90%] ${msg.role === 'user' ? 'ml-auto flex-row-reverse' : 'mr-auto'}`}>
|
||||
<div className={`shrink-0 h-8 w-8 rounded-full flex items-center justify-center border
|
||||
${msg.role === 'user' ? 'bg-background text-foreground border-border' : 'bg-primary text-primary-foreground border-primary'}
|
||||
`}>
|
||||
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
||||
</div>
|
||||
|
||||
{/* Markdown Container with Overflow Handling */}
|
||||
<div className={`px-4 py-3 rounded-2xl overflow-x-auto
|
||||
${msg.role === 'user' ? 'bg-secondary/50 border border-border text-foreground rounded-tr-sm' : 'bg-primary/10 border border-primary/20 text-foreground rounded-tl-sm'}
|
||||
`}>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none
|
||||
prose-p:leading-relaxed prose-pre:bg-secondary prose-pre:border prose-pre:border-border
|
||||
[&_.katex-display]:overflow-x-auto [&_.katex-display]:overflow-y-hidden [&_.katex-display]:pb-2 [&_.katex-display]:scrollbar-thin
|
||||
[&_.katex]:text-[1.1em]
|
||||
">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
{msg.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="p-4 border-t border-border bg-background shrink-0">
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleSend(); }} className="flex w-full items-center gap-2">
|
||||
<Input
|
||||
placeholder="Ask your AI Coach..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
className="flex-1 bg-secondary/30 border-border focus-visible:ring-primary h-12 rounded-xl"
|
||||
/>
|
||||
<Button type="submit" size="icon" className="h-12 w-12 rounded-xl shrink-0 shadow-sm" disabled={!input.trim()}>
|
||||
<Send size={18} className="ml-1" />
|
||||
</Button>
|
||||
</form>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
105
src/components/AI/AdaptiveStudyPlan.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react"
|
||||
|
||||
type PlanState = 'PENDING' | 'DONE' | 'SKIPPED';
|
||||
|
||||
interface PlanItem {
|
||||
id: string;
|
||||
title: string;
|
||||
duration: string;
|
||||
type: string;
|
||||
status: PlanState;
|
||||
}
|
||||
|
||||
const INITIAL_PLAN: PlanItem[] = [
|
||||
{ id: '1', title: 'Revise Thermodynamics Concepts', duration: '45 min', type: 'Theory', status: 'PENDING' },
|
||||
{ id: '2', title: 'Solve 15 Rotational Dynamics DPPs', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
||||
{ id: '3', title: 'Review Mistakes in Optics (T-3)', duration: '20 min', type: 'Review', status: 'PENDING' },
|
||||
];
|
||||
|
||||
export function AdaptiveStudyPlan() {
|
||||
const [items, setItems] = useState<PlanItem[]>(INITIAL_PLAN);
|
||||
|
||||
const updateStatus = (id: string, newStatus: PlanState) => {
|
||||
setItems(items.map(item => item.id === id ? { ...item, status: newStatus } : item));
|
||||
};
|
||||
|
||||
const totalTime = items.reduce((acc, curr) => {
|
||||
if (curr.status === 'DONE' || curr.status === 'PENDING') {
|
||||
return acc + parseInt(curr.duration);
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Card className="border-border bg-background shadow-sm h-full flex flex-col">
|
||||
<CardHeader className="pb-4 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<BrainCircuit className="text-primary" /> Adaptive Study Plan
|
||||
<Badge variant="outline" className="border-[#00F0FF]/30 text-[#00F0FF] bg-[#00F0FF]/5 rounded-md text-[10px] uppercase font-bold ml-2 hidden sm:flex">AI Coach</Badge>
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2 bg-secondary/50 px-3 py-1 rounded-full text-xs font-bold text-foreground">
|
||||
<Clock size={14} className="text-primary" /> Today: {totalTime} min
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Dynamically generated based on your weak areas.</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-6">
|
||||
<div className="relative space-y-6 before:absolute before:inset-0 before:ml-5 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-border before:to-transparent">
|
||||
{items.map((item, index) => {
|
||||
const isDone = item.status === 'DONE';
|
||||
const isSkipped = item.status === 'SKIPPED';
|
||||
|
||||
return (
|
||||
<div key={item.id} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group is-active">
|
||||
{/* Timeline Icon */}
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-4 border-background shrink-0 md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 shadow-sm z-10 transition-colors
|
||||
${isDone ? 'bg-green-500 text-white' : isSkipped ? 'bg-muted border-muted text-muted-foreground' : 'bg-primary/20 text-primary border-primary/30'}
|
||||
`}>
|
||||
{isDone ? <CheckCircle2 size={18} /> : isSkipped ? <SkipForward size={18} /> : <div className="size-2.5 rounded-full bg-primary animate-pulse" />}
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] p-4 rounded-xl border border-border bg-background shadow-sm transition-all hover:shadow-md">
|
||||
<div className={`flex items-center justify-between mb-1 ${isSkipped ? 'opacity-50' : ''}`}>
|
||||
<span className="text-[10px] uppercase font-bold tracking-wider text-muted-foreground bg-secondary/50 px-2 py-0.5 rounded-md">
|
||||
{item.type}
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-primary">{item.duration}</span>
|
||||
</div>
|
||||
<h4 className={`font-bold text-sm md:text-base leading-tight mb-3 transition-colors ${isDone ? 'line-through text-muted-foreground' : isSkipped ? 'text-muted-foreground' : 'text-foreground'}`}>
|
||||
{item.title}
|
||||
</h4>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
{item.status === 'PENDING' ? (
|
||||
<>
|
||||
<Button size="sm" onClick={() => updateStatus(item.id, 'DONE')} className="h-8 text-xs font-bold flex-1 bg-green-500/10 text-green-500 hover:bg-green-500 hover:text-white border border-green-500/20">
|
||||
Mark Done
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => updateStatus(item.id, 'SKIPPED')} className="h-8 text-xs font-bold px-3 hover:bg-destructive/10 hover:text-destructive">
|
||||
Skip
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full flex items-center justify-center py-1 bg-secondary/50 rounded-lg text-xs font-bold text-muted-foreground">
|
||||
{isDone ? 'Completed' : 'Rescheduled'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
114
src/components/AI/CollegePredictorCard.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export function CollegePredictorCard() {
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState("Target")
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
|
||||
async function fetchReadiness() {
|
||||
try {
|
||||
const res = await fetch(`/api/ai-coach/readiness/${userId}`)
|
||||
if (!res.ok) throw new Error("Failed to fetch")
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
} catch (err) {
|
||||
console.warn("Using mock fallback data for College Predictor.")
|
||||
setData({
|
||||
college_prediction: "NIT Trichy / NIT Surathkal on track.",
|
||||
prediction_category: "Target"
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchReadiness()
|
||||
}, [userId, userLoading])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex items-center justify-center min-h-[150px]">
|
||||
<RefreshCw className="animate-spin text-muted-foreground" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const prediction = data?.college_prediction || "Keep Practicing"
|
||||
const category = data?.prediction_category || "State Level"
|
||||
|
||||
const getCategoryColor = (cat: string) => {
|
||||
switch(cat.toUpperCase()) {
|
||||
case "REACH": return "bg-purple-500/10 text-purple-600 border-purple-500/20"
|
||||
case "TARGET": return "bg-blue-500/10 text-blue-600 border-blue-500/20"
|
||||
case "SAFETY": return "bg-green-500/10 text-green-600 border-green-500/20"
|
||||
default: return "bg-secondary text-muted-foreground border-transparent"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 min-h-[440px] overflow-hidden group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-5 flex flex-col h-full justify-between">
|
||||
<div className="flex gap-1.5 mb-3.5">
|
||||
{["Reach", "Target", "Safety"].map(tab => (
|
||||
<div
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`flex-1 p-2.5 text-center text-xs font-bold rounded-lg cursor-pointer transition-colors border ${
|
||||
activeTab === tab
|
||||
? "bg-[#00F0FF]/10 border-[#00F0FF]/20 text-[#00F0FF]"
|
||||
: "bg-white/5 border-transparent text-muted-foreground hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-white/5 border border-border hover:bg-white/10 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-black shrink-0 bg-[#00F0FF]/10 text-[#00F0FF]">
|
||||
#1
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-black text-primary truncate">NIT Trichy</div>
|
||||
<div className="text-xs font-semibold text-muted-foreground truncate">Computer Science</div>
|
||||
</div>
|
||||
<div className="text-sm font-black px-2.5 py-1 rounded-md shrink-0 text-green-500 bg-green-500/10">
|
||||
85%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-white/5 border border-border hover:bg-white/10 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-black shrink-0 bg-[#00F0FF]/10 text-[#00F0FF]">
|
||||
#2
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-black text-primary truncate">NIT Surathkal</div>
|
||||
<div className="text-xs font-semibold text-muted-foreground truncate">Information Tech</div>
|
||||
</div>
|
||||
<div className="text-sm font-black px-2.5 py-1 rounded-md shrink-0 text-yellow-500 bg-yellow-500/10">
|
||||
72%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg text-xs md:text-sm text-yellow-500 font-bold text-center">
|
||||
Just 15 more marks in Maths can upgrade you to IIT Roorkee!
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
87
src/components/AI/DailyChallengeBanner.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Flame, ArrowRight, Timer, BookOpen } from "lucide-react"
|
||||
|
||||
const FOCUS_TOPICS = [
|
||||
{ subject: "Physics", topic: "Rotational Dynamics", color: "from-blue-600/30 to-[#00F0FF]/20", accent: "#00F0FF", icon: "⚛️" },
|
||||
{ subject: "Chemistry", topic: "Organic Reactions — Mechanism", color: "from-amber-600/30 to-yellow-500/20", accent: "#F59E0B", icon: "🧪" },
|
||||
{ subject: "Biology", topic: "Human Nervous System", color: "from-green-600/30 to-emerald-400/20", accent: "#10B981", icon: "🧬" },
|
||||
{ subject: "Maths", topic: "Integral Calculus", color: "from-purple-600/30 to-violet-400/20", accent: "#A78BFA", icon: "∫" },
|
||||
]
|
||||
|
||||
export function DailyChallengeBanner() {
|
||||
const [todayFocus] = useState(() => FOCUS_TOPICS[new Date().getDay() % FOCUS_TOPICS.length])
|
||||
const [timeLeft, setTimeLeft] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => {
|
||||
const now = new Date()
|
||||
const end = new Date()
|
||||
end.setHours(23, 59, 59, 0)
|
||||
const diff = Math.max(0, end.getTime() - now.getTime())
|
||||
const h = Math.floor(diff / 3600000)
|
||||
const m = Math.floor((diff % 3600000) / 60000)
|
||||
const s = Math.floor((diff % 60000) / 1000)
|
||||
setTimeLeft(`${h}h ${m}m ${s}s`)
|
||||
}
|
||||
tick()
|
||||
const id = setInterval(tick, 1000)
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-2xl border border-white/10 bg-gradient-to-r ${todayFocus.color} p-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 shadow-[0_4px_30px_rgba(0,0,0,0.3)]`}
|
||||
>
|
||||
<div
|
||||
className="absolute -right-10 -top-10 w-48 h-48 rounded-full blur-3xl opacity-20 pointer-events-none"
|
||||
style={{ background: todayFocus.accent }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-4 relative z-10">
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl font-bold border border-white/10 shrink-0"
|
||||
style={{ background: `${todayFocus.accent}20` }}
|
||||
>
|
||||
{todayFocus.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-white/50">
|
||||
AI-Recommended Focus · Today
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-0.5 rounded-full bg-white/10 text-white/70">
|
||||
<Flame size={9} />
|
||||
DAILY CHALLENGE
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-extrabold text-white tracking-tight leading-tight">
|
||||
{todayFocus.subject}:{" "}
|
||||
<span style={{ color: todayFocus.accent }}>{todayFocus.topic}</span>
|
||||
</h3>
|
||||
<p className="text-xs text-white/50 mt-0.5 flex items-center gap-1.5">
|
||||
<BookOpen size={11} />
|
||||
Mastering this will boost your rank by an estimated{" "}
|
||||
<span className="font-bold text-white/70">+120 positions</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 relative z-10 shrink-0">
|
||||
<div className="text-right hidden sm:block">
|
||||
<div className="text-[10px] text-white/40 font-semibold uppercase flex items-center gap-1 justify-end">
|
||||
<Timer size={10} /> Expires In
|
||||
</div>
|
||||
<div className="text-sm font-black text-white/80 font-mono tracking-wider">{timeLeft}</div>
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm text-background transition-all hover:scale-105 active:scale-95"
|
||||
style={{ background: todayFocus.accent }}
|
||||
>
|
||||
Start Session <ArrowRight size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
src/components/AI/ExamStrategyCard.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Printer } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export function ExamStrategyCard() {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 min-h-[440px] overflow-hidden group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-5 flex flex-col h-full justify-between">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3.5 rounded-lg bg-white/5 border border-border text-center flex flex-col justify-center">
|
||||
<div className="text-2xl mb-1.5">🧪</div>
|
||||
<div className="text-sm font-black text-primary">Chemistry First</div>
|
||||
<div className="text-xs font-semibold text-muted-foreground mt-1">Finish in 40 mins</div>
|
||||
</div>
|
||||
<div className="p-3.5 rounded-lg bg-white/5 border border-border text-center flex flex-col justify-center">
|
||||
<div className="text-2xl mb-1.5">🧲</div>
|
||||
<div className="text-sm font-black text-primary">Physics Next</div>
|
||||
<div className="text-xs font-semibold text-muted-foreground mt-1">Target 55 mins</div>
|
||||
</div>
|
||||
<div className="p-3.5 rounded-lg bg-white/5 border border-border text-center flex flex-col justify-center">
|
||||
<div className="text-2xl mb-1.5">📐</div>
|
||||
<div className="text-sm font-black text-primary">Maths Last</div>
|
||||
<div className="text-xs font-semibold text-muted-foreground mt-1">Reserve 75 mins</div>
|
||||
</div>
|
||||
<div className="p-3.5 rounded-lg bg-white/5 border border-border text-center flex flex-col justify-center">
|
||||
<div className="text-2xl mb-1.5">⏱️</div>
|
||||
<div className="text-sm font-black text-primary">Buffer Time</div>
|
||||
<div className="text-xs font-semibold text-muted-foreground mt-1">10 mins for review</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-center gap-2 p-3 rounded-lg bg-[#00F0FF]/10 border border-dashed border-[#00F0FF]/20 text-xs md:text-sm text-[#00F0FF] font-bold cursor-pointer hover:bg-[#00F0FF]/20 transition-colors">
|
||||
<Printer size={18} className="stroke-[#00F0FF]" />
|
||||
Print Exam Day Blueprint
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
src/components/AI/FeatureIndex.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Link from "next/link"
|
||||
|
||||
const features = [
|
||||
{ name: "Mistake Tracking", href: "/dashboard/ai-coach/mistakes" },
|
||||
{ name: "Spaced Repetition", href: "/dashboard/ai-coach/mistakes" },
|
||||
{ name: "Study Plan", href: "/dashboard/ai-coach/study-plan" },
|
||||
{ name: "AI Tutor", href: "/dashboard/ai-coach/tutor" },
|
||||
{ name: "Readiness Score", href: "/dashboard/ai-coach/readiness" },
|
||||
{ name: "Exam Strategy", href: "/dashboard/ai-coach/strategy" },
|
||||
{ name: "College Predictor", href: "/dashboard/ai-coach/college-predictor" },
|
||||
{ name: "Time Optimization", href: "/dashboard/ai-coach/strategy" },
|
||||
]
|
||||
|
||||
export function FeatureIndex() {
|
||||
return (
|
||||
<div className="py-2 mb-4">
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{features.map((f) => (
|
||||
<Link
|
||||
key={f.name}
|
||||
href={f.href}
|
||||
className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-extrabold bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-[#00F0FF] hover:bg-[#00F0FF] hover:text-black transition-all shadow-[0_0_12px_rgba(0,240,255,0.1)] group"
|
||||
>
|
||||
<span className="size-2 rounded-full bg-[#00F0FF] group-hover:bg-black transition-colors" />
|
||||
<span>{f.name}</span>
|
||||
<span className="text-[10px] opacity-75">→</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
80
src/components/AI/MistakeHeatmap.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Flame, Info } from "lucide-react"
|
||||
|
||||
const HEATMAP_DATA = [
|
||||
{ topic: "Rotational Dynamics", attempts: [80, 90, 40, 20, 10] }, // 10 is critical error rate
|
||||
{ topic: "Electromagnetism", attempts: [30, 25, 20, 15, 10] },
|
||||
{ topic: "Thermodynamics", attempts: [5, 10, 5, 2, 0] },
|
||||
{ topic: "Optics", attempts: [60, 50, 45, 60, 55] },
|
||||
{ topic: "Modern Physics", attempts: [10, 5, 2, 0, 0] }
|
||||
];
|
||||
|
||||
export function MistakeHeatmap() {
|
||||
const getColor = (errorRate: number) => {
|
||||
if (errorRate >= 50) return 'bg-red-500/80 border-red-500 shadow-[0_0_10px_-2px_rgba(239,68,68,0.5)]'; // Critical
|
||||
if (errorRate >= 30) return 'bg-amber-500/60 border-amber-500'; // Warning
|
||||
if (errorRate >= 10) return 'bg-yellow-500/30 border-yellow-500/50'; // Moderate
|
||||
return 'bg-green-500/20 border-green-500/30'; // Good
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border-border bg-background shadow-sm h-full min-h-[540px] flex flex-col justify-between">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Flame className="text-orange-500" /> Mistake Heatmap
|
||||
<Badge variant="outline" className="border-[#00F0FF]/30 text-[#00F0FF] bg-[#00F0FF]/5 rounded-md text-[10px] uppercase font-bold ml-2 hidden sm:flex">AI Coach</Badge>
|
||||
</CardTitle>
|
||||
<Info size={16} className="text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Topic-wise error rate across last 5 tests.</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-4 md:p-6 flex flex-col justify-between">
|
||||
<div className="w-full">
|
||||
{/* Header row */}
|
||||
<div className="flex items-center mb-3 text-xs sm:text-sm font-black text-muted-foreground uppercase tracking-wider">
|
||||
<div className="w-32 sm:w-36 md:w-40 shrink-0 pr-2">Topic</div>
|
||||
<div className="flex-1 grid grid-cols-5 gap-1 text-center">
|
||||
<span>T-4</span>
|
||||
<span>T-3</span>
|
||||
<span>T-2</span>
|
||||
<span>T-1</span>
|
||||
<span>Latest</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data rows */}
|
||||
<div className="space-y-3">
|
||||
{HEATMAP_DATA.map((row, idx) => (
|
||||
<div key={idx} className="flex items-center group">
|
||||
<div className="w-32 sm:w-36 md:w-40 shrink-0 text-xs sm:text-sm md:text-base font-bold truncate pr-2 group-hover:text-primary transition-colors" title={row.topic}>
|
||||
{row.topic}
|
||||
</div>
|
||||
<div className="flex-1 grid grid-cols-5 gap-1">
|
||||
{row.attempts.map((errRate, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-8 sm:h-9 rounded-md border transition-all duration-300 group-hover:scale-105 flex items-center justify-center cursor-help ${getColor(errRate)}`}
|
||||
title={`Error Rate: ${errRate}%`}
|
||||
>
|
||||
<span className="text-[10px] sm:text-xs font-black text-foreground/90">{errRate}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 pt-4 border-t border-border/50 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs font-bold text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-green-500/20 border border-green-500/30"></div> {"< 10%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-yellow-500/30 border border-yellow-500/50"></div> {"10-30%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-amber-500/60 border border-amber-500"></div> {"30-50%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-red-500/80 border border-red-500"></div> {"> 50%"}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
198
src/components/AI/MistakeReviewCard.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Flame } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
export interface MistakePayload {
|
||||
mistake: {
|
||||
id: string
|
||||
user_id: string
|
||||
question_id: string
|
||||
error_type: string
|
||||
topic: string
|
||||
subtopic: string
|
||||
review_count: number
|
||||
consecutive_correct: number
|
||||
is_mastered: boolean
|
||||
}
|
||||
question: {
|
||||
id: string
|
||||
subject: string
|
||||
question_text: string
|
||||
options: Record<string, string>
|
||||
correct_answer: { correct: string }
|
||||
solution_text: string
|
||||
}
|
||||
}
|
||||
|
||||
interface MistakeReviewCardProps {
|
||||
data: MistakePayload
|
||||
onNext: () => void
|
||||
onSubmitReview: (mistakeId: string, selectedAnswer: string) => Promise<{is_correct: boolean, correct_answer: string}>
|
||||
}
|
||||
|
||||
export function MistakeReviewCard({ data, onNext, onSubmitReview }: MistakeReviewCardProps) {
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
const [result, setResult] = useState<'success' | 'error' | null>(null)
|
||||
const [correctAnswer, setCorrectAnswer] = useState<string | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const handleCheck = async () => {
|
||||
if (!selected) return;
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const res = await onSubmitReview(data.mistake.id, selected)
|
||||
setResult(res.is_correct ? 'success' : 'error')
|
||||
setCorrectAnswer(res.correct_answer)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
setSelected(null)
|
||||
setResult(null)
|
||||
setCorrectAnswer(null)
|
||||
onNext()
|
||||
}
|
||||
|
||||
const { mistake, question } = data;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center pb-1">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
♻️ Spaced Repetition Queue
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Spaced Repetition Timeline */}
|
||||
<div className="flex justify-between items-center relative py-4 px-2">
|
||||
<div className="absolute top-1/2 left-0 right-0 h-0.5 bg-secondary -translate-y-1/2 z-0" />
|
||||
<div className="absolute top-1/2 left-0 w-1/2 h-0.5 bg-[#00F0FF] -translate-y-1/2 z-0" />
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 opacity-60">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-[#00F0FF] border-2 border-background" />
|
||||
<span className="text-xs font-bold text-[#00F0FF]">Attempt 1</span>
|
||||
<span className="text-[11px] text-muted-foreground font-semibold">10 Jul</span>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 opacity-60">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-[#00F0FF] border-2 border-background" />
|
||||
<span className="text-xs font-bold text-[#00F0FF]">Attempt 2</span>
|
||||
<span className="text-[11px] text-muted-foreground font-semibold">14 Jul</span>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 scale-110">
|
||||
<div className="w-4 h-4 rounded-full border-[3px] border-[#00F0FF] bg-background shadow-[0_0_10px_rgba(0,240,255,0.5)]" />
|
||||
<span className="text-sm font-bold text-[#00F0FF]">Today</span>
|
||||
<span className="text-xs text-muted-foreground font-semibold">19 Jul</span>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 opacity-50">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-secondary border-2 border-background" />
|
||||
<span className="text-xs font-bold text-muted-foreground">Mastery</span>
|
||||
<span className="text-[11px] text-muted-foreground font-semibold">26 Jul</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mistake Card */}
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] min-h-[540px] h-full relative overflow-hidden flex flex-col justify-between group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-6 flex-1 flex flex-col justify-between">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-xs font-bold px-3 py-1 rounded-md bg-white/5 border border-border text-muted-foreground uppercase tracking-widest">
|
||||
{mistake.topic}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-red-500 bg-red-500/10 px-2.5 py-1 rounded-md">
|
||||
{mistake.error_type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-base md:text-lg mb-6 font-medium text-foreground leading-relaxed">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
{question.question_text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-auto">
|
||||
{Object.entries(question.options).map(([letter, text]) => {
|
||||
const isSelected = selected === letter
|
||||
|
||||
let btnClass = "border-border bg-white/5 hover:bg-white/10 hover:border-border text-muted-foreground"
|
||||
|
||||
if (result) {
|
||||
if (letter === correctAnswer) {
|
||||
btnClass = "border-green-500 bg-green-500/10 text-green-500 shadow-[0_0_15px_rgba(34,197,94,0.15)]"
|
||||
} else if (isSelected && letter !== correctAnswer) {
|
||||
btnClass = "border-red-500 bg-red-500/10 text-red-500"
|
||||
}
|
||||
} else if (isSelected) {
|
||||
btnClass = "border-[#00F0FF] bg-[#00F0FF]/10 text-[#00F0FF] shadow-[0_0_15px_rgba(0,240,255,0.15)]"
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={letter}
|
||||
onClick={() => !result && setSelected(letter)}
|
||||
disabled={!!result || isSubmitting}
|
||||
className={`text-left p-4 rounded-xl border-[1.5px] transition-all text-base md:text-lg font-bold flex gap-3.5 items-center ${btnClass}`}
|
||||
>
|
||||
<span className="font-bold opacity-70">{letter})</span>
|
||||
<span className="flex-1">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>{text}</ReactMarkdown>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{!result ? (
|
||||
<div className="mt-5 flex justify-end">
|
||||
<button
|
||||
onClick={handleCheck}
|
||||
disabled={!selected || isSubmitting}
|
||||
className="px-6 py-2.5 rounded-md font-bold text-sm bg-gradient-to-r from-[#00F0FF] to-teal-400 text-background hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? "Checking..." : "Submit Answer"}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Solution Block */}
|
||||
<div className="mt-5 p-4 rounded-xl border border-[#00F0FF]/20 bg-[#00F0FF]/5">
|
||||
<h4 className="text-[13px] font-bold text-[#00F0FF] mb-2 flex items-center gap-2">
|
||||
<Flame size={16} className="fill-[#00F0FF]/20" />
|
||||
AI Coach Explanation
|
||||
</h4>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-[13px] text-muted-foreground leading-relaxed [&>p]:m-0">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
{question.solution_text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="px-6 py-2.5 rounded-md font-bold text-sm bg-gradient-to-r from-[#00F0FF] to-teal-400 text-background hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Got It, Next Question →
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
src/components/AI/ProgressShare.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Download, Image as ImageIcon, Loader2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
export function ProgressShare() {
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
|
||||
// Example dynamic params
|
||||
const streak = "12"
|
||||
const accuracy = "85"
|
||||
const name = "Student"
|
||||
const imageUrl = `/api/og/progress?streak=${streak}&accuracy=${accuracy}&name=${name}`
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
setDownloading(true)
|
||||
const res = await fetch(imageUrl)
|
||||
const blob = await res.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.style.display = "none"
|
||||
a.href = url
|
||||
a.download = "PragatiVeer_Progress.png"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (err) {
|
||||
console.error("Failed to download image", err)
|
||||
} finally {
|
||||
setDownloading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<ImageIcon className="text-primary" size={24} />
|
||||
Share Progress
|
||||
</CardTitle>
|
||||
<CardDescription>Auto-generated dynamic scorecard.</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 flex flex-col items-center justify-center gap-6">
|
||||
<div className="w-full max-w-sm rounded-xl overflow-hidden border border-border shadow-sm bg-secondary/20 aspect-[1200/630] relative">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Progress Scorecard"
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleDownload} variant="outline" className="w-full" disabled={downloading}>
|
||||
{downloading ? <Loader2 size={18} className="mr-2 animate-spin" /> : <Download size={18} className="mr-2" />}
|
||||
Download as PNG
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
161
src/components/AI/ReadinessGauge.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { TrendingUp, Flame, RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export function ReadinessGauge() {
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
|
||||
async function fetchReadiness() {
|
||||
try {
|
||||
const res = await fetch(`/api/ai-coach/readiness/${userId}`)
|
||||
if (!res.ok) throw new Error("Failed to fetch")
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
} catch (err) {
|
||||
console.warn("Using mock fallback data for Readiness Gauge.")
|
||||
setData({
|
||||
score: 72,
|
||||
breakdown: {
|
||||
accuracy: 30,
|
||||
speed: 15,
|
||||
consistency: 8,
|
||||
recovery: 5,
|
||||
coverage: 14
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchReadiness()
|
||||
}, [userId, userLoading])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex items-center justify-center min-h-[300px]">
|
||||
<RefreshCw className="animate-spin text-muted-foreground" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const score = data?.score || 0
|
||||
const breakdown = data?.breakdown || {}
|
||||
const circumference = 2 * Math.PI * 45 // r=45
|
||||
const strokeDashoffset = circumference - (score / 100) * circumference
|
||||
|
||||
const getColor = (s: number) => {
|
||||
if (s >= 75) return "text-green-500"
|
||||
if (s >= 50) return "text-yellow-500"
|
||||
return "text-red-500"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] relative overflow-hidden flex-1 flex flex-col justify-between p-5 min-h-[440px] group hover:border-[#00F0FF]/30 transition-colors">
|
||||
{/* Background glow decoration */}
|
||||
<div className="absolute -top-1/2 -right-1/4 w-48 h-48 bg-[radial-gradient(circle,rgba(94,196,184,0.06)_0%,transparent_70%)] rounded-full pointer-events-none" />
|
||||
|
||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">Updated daily • Today</span>
|
||||
<span className="flex items-center gap-1 text-xs font-bold text-green-500 bg-green-500/10 px-2.5 py-0.5 rounded-full">
|
||||
<TrendingUp size={14} className="stroke-[2.5px]" />
|
||||
+5 this week
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center gap-6 mb-4 relative z-10">
|
||||
{/* Circular Score */}
|
||||
<div className="relative w-[110px] h-[110px] shrink-0">
|
||||
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="scoreGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#00F0FF" />
|
||||
<stop offset="100%" stopColor="#3B82F6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="45" stroke="currentColor" strokeWidth="6" fill="transparent" className="text-[#00F0FF]/10" />
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
stroke="url(#scoreGradient)"
|
||||
strokeWidth="6"
|
||||
fill="transparent"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1500 ease-out"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center mt-1">
|
||||
<span className="text-[32px] font-extrabold leading-none">{score}</span>
|
||||
<span className="text-[9px] text-muted-foreground font-semibold mt-0.5">SCORE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick metric grid beside ring */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-2.5 w-full">
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">88% <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Accuracy</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">45s <span className="text-xs text-red-500">↓</span></div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Speed/Q</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">High <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Consistency</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">64% <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Syllabus</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subject Bars */}
|
||||
<div className="space-y-2 mb-4 relative z-10">
|
||||
{[
|
||||
{ name: "Physics", val: 68, color: "#00F0FF" },
|
||||
{ name: "Chemistry", val: 74, color: "#F59E0B" },
|
||||
{ name: "Biology", val: 82, color: "#10B981" },
|
||||
].map(sub => (
|
||||
<div key={sub.name} className="flex items-center gap-3">
|
||||
<span className="text-xs font-bold text-muted-foreground w-20 shrink-0">{sub.name}</span>
|
||||
<div className="flex-1 h-2 rounded-full bg-white/10 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-1000"
|
||||
style={{ width: `${sub.val}%`, background: sub.color }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-black w-10 text-right" style={{ color: sub.color }}>{sub.val}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* National Avg */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-white/5 rounded-lg mb-4 relative z-10 text-xs font-bold">
|
||||
<span className="text-muted-foreground">National Avg</span>
|
||||
<span className="text-muted-foreground">58 / 100</span>
|
||||
<span className="text-green-500">You: +{score - 58} above avg</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto px-3.5 py-3 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-lg text-xs md:text-sm text-[#00F0FF] font-bold flex items-center gap-2 relative z-10">
|
||||
<Flame size={18} className="fill-[#00F0FF]/20 shrink-0" />
|
||||
Your speed has dropped slightly. Focus on time-bound practice today.
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
74
src/components/AI/ReferralCard.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Link as LinkIcon, Share2, Check, Gift } from "lucide-react"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export function ReferralCard() {
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
async function fetchReferral() {
|
||||
try {
|
||||
const res = await fetch(`/api/growth/referral/${userId}`)
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Using mock fallback data for Referral Card.")
|
||||
}
|
||||
}
|
||||
fetchReferral()
|
||||
}, [userId, userLoading])
|
||||
|
||||
const link = data?.referral_link || `https://pragativeer.com/join/${userId || "demo"}`
|
||||
const message = data?.share_message || "Join me on PragatiVeer! Use my link to get 7 days of the AI Coach for free."
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(link)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const shareToWhatsApp = () => {
|
||||
const waUrl = `https://wa.me/?text=${encodeURIComponent(message)}`
|
||||
window.open(waUrl, "_blank")
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex flex-col relative overflow-hidden bg-gradient-to-br from-background to-primary/5">
|
||||
<div className="absolute -right-6 -bottom-6 opacity-10 pointer-events-none">
|
||||
<Gift size={120} />
|
||||
</div>
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Gift className="text-primary" size={24} />
|
||||
Invite & Earn
|
||||
</CardTitle>
|
||||
<CardDescription>Give 7 days of AI Coach, get 7 days free.</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 flex flex-col justify-end gap-4 z-10">
|
||||
<div className="bg-secondary/50 border border-border p-3 rounded-lg flex items-center justify-between">
|
||||
<code className="text-sm font-semibold truncate mr-4">{link}</code>
|
||||
<Button variant="secondary" size="sm" onClick={copyToClipboard} className="shrink-0 rounded-md">
|
||||
{copied ? <Check size={16} className="text-green-500" /> : <LinkIcon size={16} />}
|
||||
<span className="ml-2 hidden sm:inline">{copied ? "Copied" : "Copy"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={shareToWhatsApp} className="w-full bg-[#25D366] hover:bg-[#1ebd5a] text-white">
|
||||
<Share2 size={18} className="mr-2" /> Share via WhatsApp
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
156
src/components/AI/StudyPlanCard.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Check, RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export function StudyPlanCard() {
|
||||
const [tasks, setTasks] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
|
||||
async function fetchPlan() {
|
||||
try {
|
||||
const res = await fetch(`/api/ai-coach/study-plan/today?user_id=${userId}`)
|
||||
if (!res.ok) {
|
||||
// If no plan is generated yet, fallback to mock silently
|
||||
throw new Error("silent_fallback")
|
||||
}
|
||||
const data = await res.json()
|
||||
setTasks(data.tasks || [])
|
||||
} catch (err: any) {
|
||||
if (err.message !== "silent_fallback") {
|
||||
console.error("Study plan fetch error:", err)
|
||||
}
|
||||
// Mock fallback if celery hasn't seeded yet
|
||||
setTasks([
|
||||
{ type: "DPS", description: "DPS — Electrostatics + Kinematics", duration_minutes: 20, status: "Done" },
|
||||
{ type: "Mistake_Review", description: "Mistake Review — Thermodynamics", duration_minutes: 10, status: "Done" },
|
||||
{ type: "Micro_test", description: "Micro-test — GOC (General Organic Chemistry)", duration_minutes: 5, status: "Pending" },
|
||||
{ type: "Test", description: "Weekly Test — Full Syllabus", duration_minutes: 180, status: "Pending" },
|
||||
{ type: "Revision", description: "Revise — Circles + Straight Lines", duration_minutes: 15, status: "Pending" }
|
||||
])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchPlan()
|
||||
}, [userId, userLoading])
|
||||
|
||||
const toggleTask = (idx: number, newStatus: string) => {
|
||||
setTasks(prev => {
|
||||
const copy = [...prev]
|
||||
copy[idx] = { ...copy[idx], status: newStatus }
|
||||
return copy
|
||||
})
|
||||
}
|
||||
|
||||
const completedTasks = tasks.filter(t => t.status === "Done").length
|
||||
const totalTasks = tasks.length
|
||||
const progressPercent = totalTasks === 0 ? 0 : Math.round((completedTasks / totalTasks) * 100)
|
||||
|
||||
const getTagStyle = (type: string) => {
|
||||
switch(type) {
|
||||
case "Revision": return "bg-yellow-500/10 text-yellow-500"
|
||||
case "Mistake_Review": return "bg-red-500/10 text-red-500"
|
||||
case "DPS": return "bg-[#00F0FF]/10 text-[#00F0FF]"
|
||||
case "Micro_test": return "bg-green-500/10 text-green-500"
|
||||
case "Test": return "bg-purple-500/10 text-purple-500"
|
||||
default: return "bg-secondary text-muted-foreground"
|
||||
}
|
||||
}
|
||||
|
||||
const getDisplayType = (type: string) => {
|
||||
switch(type) {
|
||||
case "Revision": return "Revision"
|
||||
case "Mistake_Review": return "Review"
|
||||
case "DPS": return "Practice"
|
||||
case "Micro_test": return "Micro-test"
|
||||
case "Test": return "Test"
|
||||
default: return "Task"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 overflow-hidden group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<div className="px-5 py-4 flex justify-between items-center border-b border-border">
|
||||
<div className="text-sm font-bold text-muted-foreground">
|
||||
{new Date().toLocaleDateString('en-US', { weekday: 'long', day: 'numeric', month: 'short' })}
|
||||
</div>
|
||||
<div className="text-xs text-[#00F0FF] font-bold">{progressPercent}% Completed</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="h-[3px] bg-[#00F0FF]/10 w-full">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#00F0FF] to-teal-400 rounded-r-[2px] transition-all duration-500"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0 flex-1 flex flex-col">
|
||||
{loading ? (
|
||||
<div className="flex-1 flex items-center justify-center p-8 min-h-[200px]">
|
||||
<RefreshCw className="animate-spin text-muted-foreground mr-3" /> Loading schedule...
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
{tasks.map((task, idx) => {
|
||||
const isDone = task.status === "Done"
|
||||
const isCurrent = idx === completedTasks && !isDone // First pending task is current
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => toggleTask(idx, isDone ? "Pending" : "Done")}
|
||||
className={`flex items-center gap-3 px-4 py-2.5 transition-colors cursor-pointer border-l-[3px] ${
|
||||
isDone ? "opacity-60 border-green-500 hover:bg-white/5" :
|
||||
isCurrent ? "border-[#00F0FF] bg-[#00F0FF]/5 hover:bg-[#00F0FF]/10" :
|
||||
"border-transparent hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{/* Custom Checkbox */}
|
||||
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-all ${
|
||||
isDone ? "bg-green-500 border-green-500" :
|
||||
isCurrent ? "border-[#00F0FF] shadow-[0_0_0_0_rgba(0,240,255,0.3)] animate-[pulse-border_2s_infinite]" :
|
||||
"border-muted-foreground"
|
||||
}`}>
|
||||
{isDone && <Check size={12} className="stroke-white stroke-[3px]" />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-bold text-primary truncate">{task.description}</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5 font-medium">{task.type.replace('_', ' ')} • Active Session</div>
|
||||
</div>
|
||||
|
||||
<div className={`text-xs font-bold px-2.5 py-1 rounded-md shrink-0 ${getTagStyle(task.type)}`}>
|
||||
{getDisplayType(task.type)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-bold text-muted-foreground bg-white/5 px-2.5 py-1 rounded-md shrink-0">
|
||||
{task.duration_minutes}m
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(0, 240, 255, 0.3); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(0, 240, 255, 0); }
|
||||
}
|
||||
`}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
src/components/AI/StudyTimeOptimizationCard.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Flame, Star } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
const SLOTS = [
|
||||
{ label: "6 AM", h: 30, peak: false },
|
||||
{ label: "9 AM", h: 50, peak: false },
|
||||
{ label: "12 PM", h: 65, peak: false },
|
||||
{ label: "3 PM", h: 100, peak: true },
|
||||
{ label: "6 PM", h: 80, peak: false },
|
||||
{ label: "9 PM", h: 60, peak: false },
|
||||
{ label: "12 AM", h: 38, peak: false },
|
||||
]
|
||||
|
||||
export function StudyTimeOptimizationCard() {
|
||||
const [hovered, setHovered] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] min-h-[440px] flex flex-col justify-between group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-5 flex-1 flex flex-col justify-between">
|
||||
{/* Peak window badge */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Star size={16} className="fill-yellow-500 text-yellow-500" />
|
||||
<span className="text-sm font-bold text-yellow-500">Peak Productivity:</span>
|
||||
<span className="text-sm font-bold text-foreground bg-yellow-500/10 border border-yellow-500/20 px-2.5 py-0.5 rounded-full">
|
||||
2 PM – 5 PM
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar chart */}
|
||||
<div className="flex items-end gap-1.5 h-20 px-1 mb-6">
|
||||
{SLOTS.map((slot, i) => (
|
||||
<div
|
||||
key={slot.label}
|
||||
className="flex-1 flex flex-col items-center gap-0.5 cursor-pointer"
|
||||
onMouseEnter={() => setHovered(i)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-t-md transition-all duration-300 ${
|
||||
slot.peak
|
||||
? "bg-gradient-to-t from-yellow-600/60 to-yellow-400 shadow-[0_0_10px_rgba(234,179,8,0.4)]"
|
||||
: hovered === i
|
||||
? "bg-gradient-to-t from-[#00F0FF]/50 to-[#00F0FF]"
|
||||
: "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/60"
|
||||
}`}
|
||||
style={{ height: `${slot.h}%` }}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground font-bold whitespace-nowrap mt-2">
|
||||
{slot.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto px-3.5 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg text-xs md:text-sm text-yellow-500 font-bold flex items-center gap-2">
|
||||
<Flame size={18} className="fill-yellow-500 shrink-0" />
|
||||
You perform best 2 PM – 5 PM. Schedule your hardest mock tests in this window.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
172
src/components/AI/TutorChat.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Send, Loader2 } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkMath from "remark-math"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
import "katex/dist/katex.min.css"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
interface Message {
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
}
|
||||
|
||||
export function TutorChat() {
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{ role: "assistant", content: "Hello! I am your PragatiVeer AI Coach. What topic would you like to review today?" }
|
||||
])
|
||||
const [input, setInput] = useState("")
|
||||
const [streaming, setStreaming] = useState(false)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
const handleSend = async (e?: React.FormEvent) => {
|
||||
e?.preventDefault()
|
||||
if (!input.trim() || streaming || userLoading || !userId) return
|
||||
|
||||
const userMsg = input.trim()
|
||||
setInput("")
|
||||
setMessages(prev => [...prev, { role: "user", content: userMsg }])
|
||||
|
||||
// Add empty assistant message placeholder
|
||||
setMessages(prev => [...prev, { role: "assistant", content: "" }])
|
||||
setStreaming(true)
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/ai-coach/chat/stream", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: userId, message: userMsg })
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Backend streaming failed. Using mock fallback.")
|
||||
}
|
||||
|
||||
if (!res.body) throw new Error("No readable stream")
|
||||
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
setMessages(prev => {
|
||||
const newMessages = [...prev]
|
||||
newMessages[newMessages.length - 1].content += chunk
|
||||
return newMessages
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Falling back to mock stream due to error:", err)
|
||||
// Mock Fallback Streaming logic
|
||||
const mockResponse = "It looks like my backend connection is restricted right now. However, I can still render math for you! Here is the quadratic formula:\n\n$$ x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} $$\n\nAnd inline math: $E = mc^2$."
|
||||
const words = mockResponse.split(" ")
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 50)) // Simulate network delay
|
||||
setMessages(prev => {
|
||||
const newMessages = [...prev]
|
||||
newMessages[newMessages.length - 1].content += (i === 0 ? "" : " ") + words[i]
|
||||
return newMessages
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setStreaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-[500px]">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
🤖 Ask AI Tutor
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 overflow-hidden flex flex-col group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<div className="px-4 py-3 bg-[#00F0FF]/5 border-b border-border flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#00F0FF] to-blue-500 flex items-center justify-center text-sm font-bold text-background shrink-0">
|
||||
AI
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[13px] font-bold text-primary">PragatiVeer Tutor</div>
|
||||
<div className="text-[11px] text-green-500 flex items-center gap-1 font-medium">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500"></span> Online
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto text-xs font-bold text-muted-foreground">
|
||||
<span className="text-yellow-500 font-black">14</span> msgs left
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.map((msg, idx) => (
|
||||
<div key={idx} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div className="max-w-[85%] flex flex-col gap-1">
|
||||
<div className={`p-3.5 text-sm leading-relaxed ${
|
||||
msg.role === "user"
|
||||
? "bg-[#00F0FF] text-background rounded-[12px] rounded-br-[4px] font-bold"
|
||||
: "bg-[#00F0FF]/10 border border-[#00F0FF]/20 text-primary rounded-[12px] rounded-bl-[4px] font-medium"
|
||||
}`}>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none [&>p]:m-0">
|
||||
{msg.role === "assistant" ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{msg.content || "..."}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
msg.content
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-xs text-muted-foreground font-semibold ${msg.role === "user" ? "text-right" : "text-left"}`}>
|
||||
Now
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</CardContent>
|
||||
|
||||
<div className="p-2.5 border-t border-border bg-black/20 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSend();
|
||||
}}
|
||||
placeholder="Message AI Tutor..."
|
||||
className="flex-1 bg-white/5 border border-border rounded-full px-4 py-2.5 text-sm text-primary font-medium focus:outline-none focus:border-[#00F0FF] focus:bg-[#00F0FF]/5 transition-colors"
|
||||
disabled={streaming}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || streaming}
|
||||
className="w-9 h-9 rounded-full bg-[#00F0FF] flex items-center justify-center shrink-0 hover:bg-[#00F0FF]/90 transition-transform hover:scale-105 disabled:opacity-50 disabled:hover:scale-100"
|
||||
>
|
||||
{streaming ? <Loader2 className="animate-spin text-background" size={16} /> : <Send size={16} className="text-background" />}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
src/components/AI/WeeklyProgressReport.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp, TrendingDown, Brain, Zap } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import Link from "next/link"
|
||||
|
||||
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
const BAR_HEIGHTS = [55, 70, 45, 90, 80, 60, 75] // % heights
|
||||
const ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
||||
|
||||
export function WeeklyProgressReport() {
|
||||
const avgAccuracy = Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length)
|
||||
const trend = ACCURACY[ACCURACY.length - 1] - ACCURACY[0]
|
||||
const maxBar = Math.max(...BAR_HEIGHTS)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 flex flex-col group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-4 flex flex-col h-full gap-4">
|
||||
{/* Summary chips */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full bg-green-500/10 text-green-500 border border-green-500/20">
|
||||
<TrendingUp size={12} />
|
||||
{avgAccuracy}% avg accuracy
|
||||
</div>
|
||||
<div className={`flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border ${trend >= 0 ? "bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20" : "bg-red-500/10 text-red-500 border-red-500/20"}`}>
|
||||
{trend >= 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
|
||||
{trend >= 0 ? "+" : ""}{trend}% vs Mon
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full bg-yellow-500/10 text-yellow-500 border border-yellow-500/20">
|
||||
<Zap size={12} />
|
||||
7/7 days active
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar Chart */}
|
||||
<div className="flex items-end gap-2 h-24 flex-1">
|
||||
{DAYS.map((day, i) => (
|
||||
<div key={day} className="flex flex-col items-center gap-1 flex-1">
|
||||
<div className="relative w-full flex items-end justify-center" style={{ height: "80px" }}>
|
||||
<div
|
||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
||||
i === 6
|
||||
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
||||
: "bg-gradient-to-t from-white/10 to-white/20"
|
||||
}`}
|
||||
style={{ height: `${(BAR_HEIGHTS[i] / maxBar) * 80}px` }}
|
||||
title={`${ACCURACY[i]}% accuracy`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground font-bold">{day}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AI insight */}
|
||||
<div className="px-3 py-2.5 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-md text-xs text-[#00F0FF] font-medium flex items-start gap-3">
|
||||
<Brain size={16} className="shrink-0 mt-0.5" />
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span><strong className="text-white">You keep making sign errors in Thermodynamics.</strong> In your last 3 attempts, you've used the wrong sign for work done (W = -PΔV). Review the First Law of Thermodynamics — we've queued 3 practice questions for tomorrow.</span>
|
||||
<span className="text-[#00F0FF]/80">💡 <strong className="text-white">Tip:</strong> You perform best between 7-9 PM. Schedule difficult topics then.</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
111
src/components/CBT/Calculator.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X, Delete } from "lucide-react";
|
||||
|
||||
interface CalculatorProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function Calculator({ isOpen, onClose }: CalculatorProps) {
|
||||
const [display, setDisplay] = useState("");
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleInput = (val: string) => {
|
||||
setDisplay((prev) => prev + val);
|
||||
};
|
||||
|
||||
const handleCalculate = () => {
|
||||
try {
|
||||
// Map functions to JS Math
|
||||
let mappedExp = display
|
||||
.replace(/sin\(/g, 'Math.sin(')
|
||||
.replace(/cos\(/g, 'Math.cos(')
|
||||
.replace(/tan\(/g, 'Math.tan(')
|
||||
.replace(/log\(/g, 'Math.log10(')
|
||||
.replace(/ln\(/g, 'Math.log(')
|
||||
.replace(/sqrt\(/g, 'Math.sqrt(')
|
||||
.replace(/\^/g, '**');
|
||||
|
||||
const result = new Function('return ' + mappedExp)();
|
||||
|
||||
// Handle floating point precision issues simply
|
||||
if (typeof result === 'number' && !Number.isInteger(result)) {
|
||||
setDisplay(String(parseFloat(result.toFixed(6))));
|
||||
} else {
|
||||
setDisplay(String(result));
|
||||
}
|
||||
} catch {
|
||||
setDisplay("Error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => setDisplay("");
|
||||
const handleBackspace = () => setDisplay((prev) => prev.slice(0, -1));
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="bg-background border border-border shadow-2xl rounded-2xl w-[400px] overflow-hidden animate-in zoom-in-95 duration-200">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/50">
|
||||
<h3 className="font-bold">Scientific Calculator</h3>
|
||||
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 bg-background">
|
||||
<div className="bg-secondary p-4 rounded-xl text-right text-3xl font-mono mb-4 overflow-x-auto min-h-16 flex items-center justify-end break-all shadow-inner">
|
||||
{display || "0"}
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{/* Scientific Row 1 */}
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('sin(')}>sin</Button>
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('cos(')}>cos</Button>
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('tan(')}>tan</Button>
|
||||
<Button variant="outline" className="font-bold text-destructive hover:bg-destructive/10" onClick={handleClear}>AC</Button>
|
||||
<Button variant="outline" onClick={handleBackspace}><Delete size={18} /></Button>
|
||||
|
||||
{/* Scientific Row 2 */}
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('log(')}>log</Button>
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('ln(')}>ln</Button>
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('sqrt(')}>√</Button>
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('(')}>(</Button>
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput(')')}>)</Button>
|
||||
|
||||
{/* Number Row 1 */}
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('^')}>x^y</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('7')}>7</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('8')}>8</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('9')}>9</Button>
|
||||
<Button variant="outline" className="text-primary font-bold bg-primary/10" onClick={() => handleInput('/')}>÷</Button>
|
||||
|
||||
{/* Number Row 2 */}
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('3.14159')}>π</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('4')}>4</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('5')}>5</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('6')}>6</Button>
|
||||
<Button variant="outline" className="text-primary font-bold bg-primary/10" onClick={() => handleInput('*')}>×</Button>
|
||||
|
||||
{/* Number Row 3 */}
|
||||
<Button variant="secondary" className="font-medium text-xs px-0" onClick={() => handleInput('2.71828')}>e</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('1')}>1</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('2')}>2</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('3')}>3</Button>
|
||||
<Button variant="outline" className="text-primary font-bold bg-primary/10" onClick={() => handleInput('-')}>-</Button>
|
||||
|
||||
{/* Number Row 4 */}
|
||||
<div className="col-span-1"></div>
|
||||
<Button variant="outline" className="col-span-2 font-bold" onClick={() => handleInput('0')}>0</Button>
|
||||
<Button variant="outline" className="font-bold" onClick={() => handleInput('.')}>.</Button>
|
||||
<Button variant="outline" className="text-primary font-bold bg-primary/10" onClick={() => handleInput('+')}>+</Button>
|
||||
|
||||
{/* Equals */}
|
||||
<Button className="col-span-5 font-bold shadow-[0_0_15px_-5px_var(--color-primary)] mt-1" onClick={handleCalculate}>=</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
src/components/CBT/ExamResult.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { CheckCircle2, XCircle, AlertCircle, Clock, Award, ArrowRight, RotateCcw, Share2, Mail } from "lucide-react";
|
||||
|
||||
interface ExamAttemptPayload {
|
||||
score: number;
|
||||
total_questions: number;
|
||||
accuracy: number;
|
||||
time_taken: number;
|
||||
answers: Array<{
|
||||
question_id: string;
|
||||
selected_answer: string | null;
|
||||
is_correct: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ExamResultProps {
|
||||
payload: ExamAttemptPayload;
|
||||
attemptId: string | null;
|
||||
examType?: string; // 'CBT' or 'DPS'
|
||||
onRetake: () => void;
|
||||
}
|
||||
|
||||
export function ExamResult({ payload, attemptId, examType, onRetake }: ExamResultProps) {
|
||||
const router = useRouter();
|
||||
const isDPP = examType === 'DPS';
|
||||
const backRoute = isDPP ? '/dashboard/ai-coach/cbt-dpp' : '/dashboard/cbt';
|
||||
const backLabel = isDPP ? 'Back to PrepPilot AI' : 'Back to Mock Tests';
|
||||
const answered = payload.answers.filter(a => a.selected_answer !== null).length;
|
||||
const unanswered = payload.total_questions - answered;
|
||||
|
||||
// Calculate correct based on score formula (+4 for correct, -1 for wrong).
|
||||
// Wait, payload score is just correct count according to seed script right now, let's assume it.
|
||||
const correctCount = payload.answers.filter(a => a.is_correct).length;
|
||||
const incorrectCount = answered - correctCount;
|
||||
|
||||
// Real score calculation: +4 correct, -1 incorrect
|
||||
const finalScore = (correctCount * 4) - (incorrectCount * 1);
|
||||
const maxPossibleScore = payload.total_questions * 4;
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
if (h > 0) return `${h}h ${m}m ${s}s`;
|
||||
return `${m}m ${s}s`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col items-center py-12 px-4 sm:px-6 animate-in fade-in zoom-in duration-500">
|
||||
<div className="max-w-4xl w-full space-y-8">
|
||||
|
||||
<div className="text-center space-y-4">
|
||||
<div className="inline-flex items-center justify-center size-20 rounded-full bg-green-500/10 mb-4">
|
||||
<Award className="size-10 text-green-500" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-extrabold tracking-tight">Exam Successfully Submitted</h1>
|
||||
<p className="text-xl text-muted-foreground font-medium">Here is a quick snapshot of your performance.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-10">
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
|
||||
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Final Score</span>
|
||||
<div className="text-4xl font-black text-primary">
|
||||
{finalScore} <span className="text-lg text-muted-foreground font-medium">/ {maxPossibleScore}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
|
||||
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Accuracy</span>
|
||||
<div className="text-4xl font-black text-blue-500">
|
||||
{Math.round(payload.accuracy)}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
|
||||
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Time Taken</span>
|
||||
<div className="flex items-center gap-2 text-2xl font-black text-amber-500 mt-1">
|
||||
<Clock className="size-6" /> {formatTime(payload.time_taken)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
|
||||
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Attempted</span>
|
||||
<div className="text-4xl font-black text-purple-500">
|
||||
{answered} <span className="text-lg text-muted-foreground font-medium">/ {payload.total_questions}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 pt-4">
|
||||
<div className="bg-green-500/10 border border-green-500/20 rounded-2xl p-6 text-center">
|
||||
<CheckCircle2 className="size-8 text-green-500 mx-auto mb-2" />
|
||||
<div className="text-3xl font-black text-green-500">{correctCount}</div>
|
||||
<div className="text-sm font-bold text-green-600/70 uppercase tracking-wide mt-1">Correct</div>
|
||||
</div>
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl p-6 text-center">
|
||||
<XCircle className="size-8 text-red-500 mx-auto mb-2" />
|
||||
<div className="text-3xl font-black text-red-500">{incorrectCount}</div>
|
||||
<div className="text-sm font-bold text-red-600/70 uppercase tracking-wide mt-1">Incorrect</div>
|
||||
</div>
|
||||
<div className="bg-secondary border border-border rounded-2xl p-6 text-center">
|
||||
<AlertCircle className="size-8 text-muted-foreground mx-auto mb-2" />
|
||||
<div className="text-3xl font-black text-foreground">{unanswered}</div>
|
||||
<div className="text-sm font-bold text-muted-foreground uppercase tracking-wide mt-1">Unanswered</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center pt-8">
|
||||
<Button variant="outline" size="lg" className="rounded-xl h-14 px-8 font-bold text-lg gap-2" onClick={onRetake}>
|
||||
<RotateCcw className="size-5" /> {isDPP ? 'Retake DPP' : 'Retake Mock Test'}
|
||||
</Button>
|
||||
{attemptId && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="rounded-xl h-14 px-8 font-bold text-lg bg-indigo-600 hover:bg-indigo-700 text-white gap-2 w-full sm:w-auto"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error exiting fullscreen", err);
|
||||
}
|
||||
router.push(isDPP ? `/dashboard/ai-coach/cbt-dpp/history/${attemptId}` : `/dashboard/cbt/history/${attemptId}`);
|
||||
}}
|
||||
>
|
||||
View Solutions
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="lg"
|
||||
className="rounded-xl h-14 px-8 font-bold text-lg shadow-[0_0_20px_-5px_var(--color-primary)] gap-2 w-full sm:w-auto"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error exiting fullscreen", err);
|
||||
}
|
||||
router.push(backRoute);
|
||||
}}
|
||||
>
|
||||
{backLabel} <ArrowRight className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center pt-8 border-t border-border mt-8 space-y-4">
|
||||
<h3 className="text-xl font-bold tracking-tight">Challenge a Friend</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md">Share this test with your friends and see if they can beat your score of {finalScore}!</p>
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" className="gap-2 font-bold rounded-xl" onClick={() => {
|
||||
const subject = encodeURIComponent("Can you beat my PragatiVeer Mock Test Score?");
|
||||
const body = encodeURIComponent(`I just scored ${finalScore}/${maxPossibleScore} with ${Math.round(payload.accuracy)}% accuracy in a PragatiVeer Mock Test!\n\nTake the challenge here: https://pragativeer.com/challenge`);
|
||||
window.open(`mailto:?subject=${subject}&body=${body}`);
|
||||
}}>
|
||||
<Mail className="size-4" /> Share via Email
|
||||
</Button>
|
||||
<Button className="gap-2 font-bold rounded-xl bg-green-500 hover:bg-green-600 text-white shadow-lg" onClick={() => {
|
||||
const text = encodeURIComponent(`I just scored ${finalScore}/${maxPossibleScore} in a PragatiVeer Mock Test! Can you beat me? \nTake the challenge here: https://pragativeer.com/challenge`);
|
||||
window.open(`https://wa.me/?text=${text}`, '_blank');
|
||||
}}>
|
||||
<Share2 className="size-4" /> Share on WhatsApp
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/CBT/PeriodicTable.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { X, ExternalLink } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
|
||||
interface PeriodicTableProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function PeriodicTable({ isOpen, onClose }: PeriodicTableProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md animate-in fade-in duration-300">
|
||||
<div className="bg-background border border-border shadow-2xl rounded-2xl overflow-hidden animate-in zoom-in-95 duration-300 flex flex-col w-[95vw] max-w-[1400px] h-[95vh]">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/80">
|
||||
<h3 className="font-bold text-lg">Periodic Table of Elements</h3>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={onClose} className="text-muted-foreground hover:text-foreground ml-2 p-2">
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 bg-white p-4 flex items-center justify-center overflow-auto">
|
||||
<div className="relative min-w-[1000px] w-full h-full flex items-center justify-center">
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Periodic_table_large.svg/1920px-Periodic_table_large.svg.png"
|
||||
alt="Periodic Table"
|
||||
className="object-contain w-full h-full max-h-[90vh]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
178
src/components/CBT/Scratchpad.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X, Eraser, Pen } from "lucide-react";
|
||||
|
||||
interface ScratchpadProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function Scratchpad({ isOpen, onClose }: ScratchpadProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [mode, setMode] = useState<"pen" | "eraser">("pen");
|
||||
const [notes, setNotes] = useState("");
|
||||
|
||||
// Initialize and load saved session memory
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const savedNotes = localStorage.getItem("cbt_scratchpad_notes");
|
||||
if (savedNotes) setNotes(savedNotes);
|
||||
|
||||
if (canvasRef.current) {
|
||||
const canvas = canvasRef.current;
|
||||
canvas.width = 500;
|
||||
canvas.height = 500;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "white";
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
const savedDrawing = localStorage.getItem("cbt_scratchpad_drawing");
|
||||
if (savedDrawing) {
|
||||
const img = new window.Image();
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
};
|
||||
img.src = savedDrawing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const startDrawing = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
setIsDrawing(true);
|
||||
draw(e);
|
||||
};
|
||||
|
||||
const stopDrawing = () => {
|
||||
setIsDrawing(false);
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.beginPath();
|
||||
localStorage.setItem("cbt_scratchpad_drawing", canvas.toDataURL());
|
||||
}
|
||||
};
|
||||
|
||||
const draw = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
if (!isDrawing || !canvasRef.current) return;
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let clientX, clientY;
|
||||
if ('touches' in e) {
|
||||
clientX = e.touches[0].clientX;
|
||||
clientY = e.touches[0].clientY;
|
||||
} else {
|
||||
clientX = (e as React.MouseEvent).clientX;
|
||||
clientY = (e as React.MouseEvent).clientY;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
const y = clientY - rect.top;
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
};
|
||||
|
||||
const clearCanvas = () => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (ctx && canvas) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
localStorage.removeItem("cbt_scratchpad_drawing");
|
||||
}
|
||||
};
|
||||
|
||||
const clearNotes = () => {
|
||||
setNotes("");
|
||||
localStorage.removeItem("cbt_scratchpad_notes");
|
||||
};
|
||||
|
||||
const handleModeSwitch = (newMode: "pen" | "eraser") => {
|
||||
setMode(newMode);
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (ctx) {
|
||||
if (newMode === "eraser") {
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.lineWidth = 20;
|
||||
} else {
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.lineWidth = 2;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotesChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const val = e.target.value;
|
||||
setNotes(val);
|
||||
localStorage.setItem("cbt_scratchpad_notes", val);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200 p-8">
|
||||
<div className="bg-background border border-border shadow-2xl rounded-2xl overflow-hidden animate-in zoom-in-95 duration-200 flex flex-col w-full max-w-6xl h-full max-h-[800px]">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center bg-secondary/50">
|
||||
<h3 className="font-bold flex items-center gap-2"><Pen size={18}/> Split Workspace: Draw & Type</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant={mode === "pen" ? "default" : "outline"} size="sm" onClick={() => handleModeSwitch("pen")}><Pen size={14} className="mr-2"/> Pen</Button>
|
||||
<Button variant={mode === "eraser" ? "default" : "outline"} size="sm" onClick={() => handleModeSwitch("eraser")}><Eraser size={14} className="mr-2"/> Eraser</Button>
|
||||
<button onClick={onClose} className="text-muted-foreground hover:text-foreground ml-4 bg-secondary p-2 rounded-lg">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex overflow-hidden bg-zinc-950">
|
||||
{/* Drawing Column */}
|
||||
<div className="flex-1 flex flex-col border-r border-zinc-800 relative">
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<Button variant="outline" size="sm" onClick={clearCanvas} className="bg-zinc-900 border-zinc-700 text-zinc-300">Clear Drawing</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4 flex items-center justify-center">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onMouseDown={startDrawing}
|
||||
onMouseUp={stopDrawing}
|
||||
onMouseOut={stopDrawing}
|
||||
onMouseMove={draw}
|
||||
onTouchStart={startDrawing}
|
||||
onTouchEnd={stopDrawing}
|
||||
onTouchMove={draw}
|
||||
className="bg-zinc-900 border border-zinc-800 rounded-lg cursor-crosshair touch-none shadow-inner"
|
||||
style={{ width: '500px', height: '500px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Typing Column */}
|
||||
<div className="flex-1 flex flex-col relative">
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<Button variant="outline" size="sm" onClick={clearNotes} className="bg-zinc-900 border-zinc-700 text-zinc-300">Clear Notes</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4 flex items-center justify-center">
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={handleNotesChange}
|
||||
placeholder="Type your rough notes, formulas, and calculations here..."
|
||||
className="bg-zinc-900 border border-zinc-800 rounded-lg shadow-inner text-zinc-100 p-6 resize-none focus:outline-none focus:ring-1 focus:ring-primary text-lg"
|
||||
style={{ width: '500px', height: '500px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
src/components/Dashboard/StreakOnboardingModal.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Flame, Sparkles } from "lucide-react";
|
||||
import { useActiveUser } from "@/hooks/useActiveUser";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export function StreakOnboardingModal() {
|
||||
const { user, loading } = useActiveUser();
|
||||
const { resolvedTheme, theme } = useTheme();
|
||||
const activeTheme = resolvedTheme || theme || "dark";
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !user) return;
|
||||
|
||||
// Check sessionStorage
|
||||
const hasSeen = sessionStorage.getItem(`hasSeenStreakPopup_${user.id}`);
|
||||
if (!hasSeen) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}, [user, loading]);
|
||||
|
||||
if (!isOpen || !user) return null;
|
||||
|
||||
const streak = user.streak_count;
|
||||
const targetExam = user.target_exam?.replace("_", " ") || "JEE/NEET";
|
||||
|
||||
// Streak-based wishes and titles
|
||||
let wishTitle = "Welcome to PragatiVeer! 🌟";
|
||||
let wishMessage = `Prepare yourself to conquer ${targetExam}. Your daily practice starts here!`;
|
||||
|
||||
if (streak === 0) {
|
||||
wishTitle = "Start Your Streak Today! 🚀";
|
||||
wishMessage = "Today is Day 1 of your journey. Establish your streak now and build the consistency needed for IIT/AIIMS!";
|
||||
} else if (streak < 4) {
|
||||
wishTitle = "Keep the Fire Burning! 🔥";
|
||||
wishMessage = `Great job! You have maintained a ${streak}-day study streak. Keep practicing daily to build solid muscle memory!`;
|
||||
} else {
|
||||
wishTitle = "Unstoppable Dedication! ⚡";
|
||||
wishMessage = `Incredible! A ${streak}-day streak shows true championship discipline. You are outperforming the competition. Let's make today count!`;
|
||||
}
|
||||
|
||||
const handleEnter = () => {
|
||||
sessionStorage.setItem(`hasSeenStreakPopup_${user.id}`, "true");
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
// Calculate trial days left
|
||||
let trialDaysLeft = 0;
|
||||
if (user.trial_end) {
|
||||
const end = new Date(user.trial_end);
|
||||
const now = new Date();
|
||||
const diffTime = end.getTime() - now.getTime();
|
||||
trialDaysLeft = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/85 backdrop-blur-md animate-in fade-in duration-300">
|
||||
|
||||
{/* Centered Modal Card Container covering 80% screen size (w-[80vw] h-[80vh]) */}
|
||||
<div className="bg-background border-none shadow-[0_0_60px_-10px_rgba(0,240,255,0.25)] rounded-3xl w-[80vw] max-w-5xl h-[80vh] max-h-[800px] relative overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 z-20 pointer-events-auto mx-4 flex flex-col justify-start">
|
||||
|
||||
{/* Subtle background glow */}
|
||||
<div className="absolute -right-20 -top-20 size-48 bg-[#00F0FF]/5 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute -left-20 -bottom-20 size-48 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Top Celebration Animation Panel (Iframe with height 60%, matching bg) */}
|
||||
<div className="relative w-full h-[60%] z-10 pointer-events-none overflow-hidden bg-background">
|
||||
<iframe
|
||||
src={`/streak-animation.html?streak=${streak}&theme=${activeTheme === 'light' ? 'light' : 'dark'}`}
|
||||
className="w-full h-full border-none"
|
||||
title="Streak Celebration"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom Greetings & Action Panel (height 40%, matching bg) */}
|
||||
<div className="p-8 text-center flex-1 flex flex-col justify-between bg-background z-20">
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Badge Indicators */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{streak > 0 ? (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-orange-500/10 border border-orange-500/30 text-orange-500 text-sm md:text-base font-extrabold rounded-full tracking-wider uppercase">
|
||||
<Flame size={16} className="fill-current animate-bounce" /> {streak} Day Streak Active
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-blue-500/10 border border-blue-500/30 text-blue-400 text-sm md:text-base font-extrabold rounded-full tracking-wider uppercase">
|
||||
<Sparkles size={16} className="animate-spin" style={{ animationDuration: '4s' }} /> Day 1 Awaits
|
||||
</div>
|
||||
)}
|
||||
|
||||
{trialDaysLeft > 0 && (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-sm md:text-base font-semibold rounded-full tracking-wider">
|
||||
🎁 Free Trial: {trialDaysLeft} {trialDaysLeft === 1 ? "day" : "days"} remaining
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Greetings Text */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-2xl md:text-3xl font-black tracking-tight bg-gradient-to-r dark:from-white dark:via-white dark:to-gray-400 from-black via-black to-gray-600 bg-clip-text text-transparent">
|
||||
{wishTitle}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm md:text-base leading-relaxed max-w-lg mx-auto">
|
||||
{wishMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="pt-2 max-w-sm w-full mx-auto">
|
||||
<Button
|
||||
onClick={handleEnter}
|
||||
className="w-full h-12 rounded-xl text-base font-black bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 shadow-[0_0_15px_-3px_#00F0FF] hover:shadow-[0_0_25px_#00F0FF] hover:-translate-y-0.5 transition-all duration-300"
|
||||
>
|
||||
Enter Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
src/components/Landing/PublicLayout.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function PublicLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground overflow-x-hidden selection:bg-primary/20 relative">
|
||||
{/* Navigation */}
|
||||
<nav className="fixed top-0 w-full z-50 bg-background/50 backdrop-blur-xl border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<Link href="/" className="relative h-14 w-60 hover:opacity-90 transition-opacity block">
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left dark:hidden" priority
|
||||
/>
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left hidden dark:block" priority
|
||||
/>
|
||||
</Link>
|
||||
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-muted-foreground">
|
||||
<Link href="/#features" className="hover:text-foreground transition-colors">Features</Link>
|
||||
<Link href="/#pricing" className="hover:text-foreground transition-colors">Pricing</Link>
|
||||
<Link href="/about" className="hover:text-foreground transition-colors">About</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/login" className="text-sm font-bold hover:text-primary transition-colors hidden sm:block">Log in</Link>
|
||||
<Link href="/signup">
|
||||
<Button className="rounded-full font-bold px-6 shadow-[0_0_15px_-5px_var(--color-primary)] hover:shadow-[0_0_25px_-5px_var(--color-primary)] transition-all">Get Started</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="relative z-10 min-h-screen flex flex-col">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Global Footer (Static) */}
|
||||
<footer className="relative z-10 bg-background/90 backdrop-blur-md border-t border-border pt-16 pb-8">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
||||
<div className="md:col-span-1 space-y-4">
|
||||
<div className="relative h-12 w-64 mb-6">
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
|
||||
alt="PragatiVeer" fill className="object-contain object-left scale-135 origin-left hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-medium leading-relaxed">
|
||||
Re-engineering NEET & JEE preparation with adaptive AI models. Learn faster, rank higher.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-bold mb-6 tracking-tight text-foreground">Product</h4>
|
||||
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
|
||||
<li><Link href="/#features" className="hover:text-primary transition-colors">Features</Link></li>
|
||||
<li><Link href="/#pricing" className="hover:text-primary transition-colors">Pricing</Link></li>
|
||||
<li><Link href="/dashboard" className="hover:text-primary transition-colors">Student Dashboard</Link></li>
|
||||
<li><Link href="/admin" className="hover:text-[#00F0FF] transition-colors">Admin Panel</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-bold mb-6 tracking-tight text-foreground">Company</h4>
|
||||
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
|
||||
<li><Link href="/about" className="hover:text-primary transition-colors">About Us</Link></li>
|
||||
<li><Link href="/contact" className="hover:text-primary transition-colors">Contact</Link></li>
|
||||
<li><Link href="#" className="hover:text-primary transition-colors">Careers</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-bold mb-6 tracking-tight text-foreground">Legal</h4>
|
||||
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
|
||||
<li><Link href="/terms" className="hover:text-primary transition-colors">Terms & Conditions</Link></li>
|
||||
<li><Link href="/privacy" className="hover:text-primary transition-colors">Privacy Policy</Link></li>
|
||||
<li><Link href="/refund" className="hover:text-primary transition-colors">Refund Policy</Link></li>
|
||||
<li><Link href="/cancellation" className="hover:text-primary transition-colors">Cancellation Policy</Link></li>
|
||||
<li><Link href="/disclaimer" className="hover:text-primary transition-colors">Disclaimer</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-8 flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p className="text-sm text-muted-foreground font-medium">© {new Date().getFullYear()} PragatiVeer. All rights reserved.</p>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground font-medium">
|
||||
Made with <div className="size-2 rounded-full bg-primary animate-pulse"></div> by AI Agents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
37
src/components/LanguageToggle.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Languages } from "lucide-react"
|
||||
|
||||
export function LanguageToggle() {
|
||||
const [lang, setLang] = useState("EN")
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<button className="flex items-center gap-2 p-2 rounded-xl hover:bg-secondary/80 transition-colors border border-transparent hover:border-border font-medium text-sm text-muted-foreground hover:text-foreground">
|
||||
<Languages size={18} />
|
||||
<span className="hidden sm:inline">{lang === "EN" ? "English" : lang === "HI" ? "Hindi" : "Bilingual"}</span>
|
||||
</button>
|
||||
<div className="absolute top-full right-0 mt-2 w-36 bg-background border border-border rounded-xl shadow-lg opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all z-50 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setLang("EN")}
|
||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-secondary transition-colors ${lang === "EN" ? "font-bold text-primary" : "text-foreground"}`}
|
||||
>
|
||||
English
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLang("HI")}
|
||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-secondary transition-colors ${lang === "HI" ? "font-bold text-primary" : "text-foreground"}`}
|
||||
>
|
||||
Hindi
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLang("BI")}
|
||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-secondary transition-colors ${lang === "BI" ? "font-bold text-primary" : "text-foreground"}`}
|
||||
>
|
||||
Bilingual
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
37
src/components/Logo.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
interface LogoProps {
|
||||
href?: string
|
||||
className?: string
|
||||
imageClassName?: string
|
||||
height?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* PragatiVeer Logo - correctly switches between modes:
|
||||
* - Light Mode: Rectangular_light_background.svg (dark/gold text visible on white)
|
||||
* - Dark Mode: Rectangular_dark_background.svg (white text visible on dark)
|
||||
*/
|
||||
export function Logo({ href = "/", className = "", imageClassName = "object-contain object-left", height = 56 }: LogoProps) {
|
||||
return (
|
||||
<Link href={href} className={`hover:opacity-90 transition-opacity block relative ${className}`} style={{ height }}>
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
|
||||
alt="PragatiVeer"
|
||||
fill
|
||||
className={`${imageClassName} dark:hidden`}
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
|
||||
alt="PragatiVeer"
|
||||
fill
|
||||
className={`${imageClassName} hidden dark:block`}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
32
src/components/MaintenanceScreen.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { Wrench } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function MaintenanceScreen() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="max-w-md w-full bg-secondary/20 border border-border rounded-2xl p-8 text-center space-y-6"
|
||||
>
|
||||
<div className="mx-auto w-16 h-16 bg-yellow-500/10 text-yellow-500 rounded-full flex items-center justify-center mb-6">
|
||||
<Wrench size={32} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">We're Upgrading Your Learning Experience</h1>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
Our platform is currently undergoing maintenance to bring you faster performance and exciting new features.
|
||||
</p>
|
||||
|
||||
<div className="pt-4 border-t border-border/50">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
We'll be back shortly. Thank you for your patience!
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/components/StreakCounter.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import { Flame } from "lucide-react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
interface StreakCounterProps {
|
||||
streak: number
|
||||
}
|
||||
|
||||
export function StreakCounter({ streak }: StreakCounterProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0, y: -10 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 15 }}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-orange-500/10 border border-orange-500/20 text-orange-500 font-bold shadow-[0_0_10px_-2px_rgba(249,115,22,0.5)] cursor-default"
|
||||
>
|
||||
<Flame size={18} className="animate-pulse" />
|
||||
<span>{streak}</span>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
41
src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [isDark, setIsDark] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
setIsDark(true);
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
setIsDark(false);
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (isDark) {
|
||||
document.documentElement.classList.remove('dark')
|
||||
localStorage.setItem('theme', 'light');
|
||||
setIsDark(false)
|
||||
} else {
|
||||
document.documentElement.classList.add('dark')
|
||||
localStorage.setItem('theme', 'dark');
|
||||
setIsDark(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-xl hover:bg-secondary/80 transition-colors border border-transparent hover:border-border text-muted-foreground hover:text-foreground"
|
||||
aria-label="Toggle Dark Mode"
|
||||
>
|
||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
21
src/components/TierBadge.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
interface TierBadgeProps {
|
||||
tier: "Free" | "Pro" | "AI Coach"
|
||||
}
|
||||
|
||||
export function TierBadge({ tier }: TierBadgeProps) {
|
||||
if (tier === "Free") {
|
||||
return <Badge variant="outline" className="text-xs font-semibold uppercase tracking-wider text-muted-foreground bg-secondary/30">Free</Badge>
|
||||
}
|
||||
|
||||
if (tier === "Pro") {
|
||||
return <Badge className="text-xs font-semibold uppercase tracking-wider bg-primary/10 text-primary border-primary/20 hover:bg-primary/20 transition-colors">Pro</Badge>
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className="text-xs font-semibold uppercase tracking-wider bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20 hover:bg-[#00F0FF]/20 transition-colors shadow-[0_0_10px_-2px_#00F0FF]">
|
||||
AI Coach
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
26
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "default" | "secondary" | "outline" | "destructive";
|
||||
}
|
||||
|
||||
function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-medium uppercase tracking-wider transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80": variant === "default",
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80": variant === "destructive",
|
||||
"border border-border text-foreground": variant === "outline",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge }
|
||||
37
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "default" | "outline" | "ghost" | "secondary" | "link";
|
||||
size?: "default" | "sm" | "lg" | "icon";
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", asChild, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium ring-offset-background transition-all duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90 hover:scale-[1.02] active:scale-[0.98]": variant === "default",
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
|
||||
"border border-border bg-background hover:bg-accent hover:text-accent-foreground": variant === "outline",
|
||||
"hover:bg-accent hover:text-accent-foreground": variant === "ghost",
|
||||
"text-primary underline-offset-4 hover:underline": variant === "link",
|
||||
"h-10 px-4 py-2": size === "default",
|
||||
"h-9 rounded-xl px-3": size === "sm",
|
||||
"h-11 rounded-2xl px-8": size === "lg",
|
||||
"h-10 w-10": size === "icon",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button }
|
||||
72
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-2xl border border-border bg-background text-foreground shadow-sm transition-all duration-300 hover:shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"font-semibold leading-none tracking-tight text-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
23
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-xl border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
18
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Label.displayName = "Label"
|
||||
|
||||
export { Label }
|
||||
48
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface SwitchProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
||||
({ className, checked, onCheckedChange, disabled, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||
disabled ? "cursor-not-allowed opacity-50" : "",
|
||||
checked ? "bg-primary" : "bg-border",
|
||||
className
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!disabled && onCheckedChange) {
|
||||
onCheckedChange(!checked);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
ref={ref}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
if (onCheckedChange) onCheckedChange(e.target.checked);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform",
|
||||
checked ? "translate-x-5" : "translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Switch.displayName = "Switch"
|
||||
|
||||
export { Switch }
|
||||
118
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("bg-primary font-medium text-primary-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
210
src/hooks/useActiveUser.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
class: string;
|
||||
target_exam: "JEE_Main" | "JEE_Advanced" | "NEET";
|
||||
medium: "English" | "Hindi" | "Bilingual";
|
||||
school: string | null;
|
||||
city: string;
|
||||
state: string;
|
||||
tier: "Free" | "Pro" | "Al_Coach"; // Al_Coach in database schema
|
||||
streak_count: number;
|
||||
longest_streak: number;
|
||||
active_session_token?: string;
|
||||
created_at?: string;
|
||||
trial_end?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"; // AI Coach
|
||||
|
||||
export const DEMO_USERS = {
|
||||
FREE: { id: "11111111-1111-1111-1111-111111111111", label: "Amit Kumar (Free Tier)" },
|
||||
PRO: { id: "22222222-2222-2222-2222-222222222222", label: "Priya Singh (Pro Tier)" },
|
||||
NEHA: { id: "33333333-3333-3333-3333-333333333333", label: "Neha Verma (AI Coach)" },
|
||||
AI_COACH: { id: DEFAULT_USER_ID, label: "Rahul Sharma (AI Coach)" },
|
||||
};
|
||||
|
||||
export function useActiveUser() {
|
||||
const [userId, setUserIdState] = useState<string>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("activeUserId") || DEFAULT_USER_ID;
|
||||
}
|
||||
return DEFAULT_USER_ID;
|
||||
});
|
||||
|
||||
const [user, setUser] = useState<UserProfile | null>(null);
|
||||
const [globalSettings, setGlobalSettings] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const failCountRef = { current: 0 };
|
||||
|
||||
const fetchUserProfile = useCallback(async (id: string, isPoll: boolean = false) => {
|
||||
if (!isPoll) setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/users/${id}`);
|
||||
const settingsRes = await fetch(`/api/admin/settings`, { cache: "no-store" });
|
||||
|
||||
if (!res.ok) {
|
||||
if (!isPoll) {
|
||||
setError("Failed to fetch user profile");
|
||||
setUser(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (settingsRes.ok) {
|
||||
const settingsData = await settingsRes.json();
|
||||
setGlobalSettings(settingsData);
|
||||
}
|
||||
|
||||
failCountRef.current = 0;
|
||||
|
||||
// Single Active Session Check
|
||||
if (typeof window !== "undefined") {
|
||||
const localToken = localStorage.getItem("active_session_token");
|
||||
if (data.active_session_token && localToken && data.active_session_token !== localToken) {
|
||||
localStorage.removeItem("activeUserId");
|
||||
localStorage.removeItem("active_session_token");
|
||||
alert("Your account was logged in from another device. You have been logged out.");
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setUser(data);
|
||||
} catch (err: any) {
|
||||
// Network error (backend down / restarting) — only surface on initial load
|
||||
failCountRef.current = (failCountRef.current || 0) + 1;
|
||||
if (!isPoll) {
|
||||
console.warn("Backend unreachable, will retry:", err.message);
|
||||
setError("Backend unreachable — retrying...");
|
||||
}
|
||||
// Silently swallow polling errors so the UI doesn't crash
|
||||
} finally {
|
||||
if (!isPoll) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initial fetch
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const storedId = localStorage.getItem("activeUserId") || DEFAULT_USER_ID;
|
||||
setUserIdState(storedId);
|
||||
fetchUserProfile(storedId);
|
||||
}
|
||||
}, [fetchUserProfile]);
|
||||
|
||||
// Session validity polling — only runs when we already have a user
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const handleUpdate = () => fetchUserProfile(userId);
|
||||
window.addEventListener("pragativeer-user-updated", handleUpdate);
|
||||
|
||||
// Poll every 30s (was 10s — less aggressive, kinder on backend restarts)
|
||||
const interval = setInterval(() => {
|
||||
fetchUserProfile(userId, true);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pragativeer-user-updated", handleUpdate);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [userId, fetchUserProfile]);
|
||||
|
||||
|
||||
const switchUser = useCallback(async (newId: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const sessionToken = crypto.randomUUID();
|
||||
localStorage.setItem("active_session_token", sessionToken);
|
||||
|
||||
try {
|
||||
await fetch(`/api/users/${newId}/session`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session_token: sessionToken })
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to set session", err);
|
||||
}
|
||||
|
||||
localStorage.setItem("activeUserId", newId);
|
||||
setUserIdState(newId);
|
||||
window.location.reload();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hasAccessTo = useCallback((feature: string): boolean => {
|
||||
if (!user) return false;
|
||||
let tier = user.tier;
|
||||
|
||||
// Trial logic interception
|
||||
if (globalSettings && (user.tier === 'Free' || user.id === DEMO_USERS.NEHA.id)) {
|
||||
const trialDays = globalSettings.trial_duration_days !== undefined ? globalSettings.trial_duration_days : 15;
|
||||
const trialEnabled = globalSettings.enable_free_trial !== undefined ? globalSettings.enable_free_trial : true;
|
||||
let isTrialActive = false;
|
||||
|
||||
if (trialEnabled && trialDays > 0) {
|
||||
// For demo purposes, we ignore created_at and just assume trial is active if days > 0
|
||||
isTrialActive = true;
|
||||
}
|
||||
|
||||
if (isTrialActive) {
|
||||
tier = (globalSettings.trial_tier as "Pro" | "Al_Coach") || "Al_Coach";
|
||||
} else {
|
||||
tier = "Free";
|
||||
}
|
||||
}
|
||||
|
||||
// Core restrictions mapping based on PRD feature matrix
|
||||
switch (feature) {
|
||||
case "topic-wise-microtests-unlimited":
|
||||
case "weekly-test-series":
|
||||
case "full-length-mock-tests":
|
||||
case "previous-year-papers":
|
||||
case "detailed-performance-analytics":
|
||||
case "chapter-wise-progress-tracking":
|
||||
case "doubt-resolution-community":
|
||||
case "parent-dashboard":
|
||||
case "offline-mode":
|
||||
case "pdf-export":
|
||||
return tier === "Pro" || tier === "Al_Coach";
|
||||
|
||||
case "ai-mistake-tracking":
|
||||
case "spaced-repetition":
|
||||
case "personalized-study-plan":
|
||||
case "ai-tutor-chat":
|
||||
case "exam-readiness-score":
|
||||
case "exam-day-strategy":
|
||||
case "college-predictor":
|
||||
case "study-time-optimization":
|
||||
return tier === "Al_Coach";
|
||||
|
||||
default:
|
||||
// Free tier features: DPS (1/day), basic dashboard, basic streak tracking, leaderboard
|
||||
return true;
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return {
|
||||
userId,
|
||||
user,
|
||||
globalSettings,
|
||||
loading,
|
||||
error,
|
||||
switchUser,
|
||||
hasAccessTo,
|
||||
refreshUser: async () => {
|
||||
await fetchUserProfile(userId);
|
||||
window.dispatchEvent(new CustomEvent('pragativeer-user-updated'));
|
||||
}
|
||||
};
|
||||
}
|
||||
49
src/hooks/useExamTimer.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useCBTStore } from '@/store/cbtStore';
|
||||
|
||||
export function useExamTimer() {
|
||||
const { startTime, totalDuration, status, submitExam } = useCBTStore();
|
||||
const [timeRemaining, setTimeRemaining] = useState<number>(totalDuration);
|
||||
const frameRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'IN_PROGRESS' || !startTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const calculateTime = () => {
|
||||
const now = Date.now();
|
||||
const elapsedSeconds = Math.floor((now - startTime) / 1000);
|
||||
const remaining = Math.max(0, totalDuration - elapsedSeconds);
|
||||
|
||||
setTimeRemaining(remaining);
|
||||
|
||||
if (remaining === 0) {
|
||||
submitExam('TIMED_OUT');
|
||||
} else {
|
||||
// Use requestAnimationFrame for a high-performance loop instead of setInterval
|
||||
// We throttle updates to when the second actually changes
|
||||
frameRef.current = requestAnimationFrame(calculateTime);
|
||||
}
|
||||
};
|
||||
|
||||
frameRef.current = requestAnimationFrame(calculateTime);
|
||||
|
||||
return () => {
|
||||
if (frameRef.current) cancelAnimationFrame(frameRef.current);
|
||||
};
|
||||
}, [startTime, totalDuration, status, submitExam]);
|
||||
|
||||
// Format HH:MM:SS
|
||||
const hours = Math.floor(timeRemaining / 3600);
|
||||
const minutes = Math.floor((timeRemaining % 3600) / 60);
|
||||
const seconds = timeRemaining % 60;
|
||||
|
||||
const formattedTime = [
|
||||
hours.toString().padStart(2, '0'),
|
||||
minutes.toString().padStart(2, '0'),
|
||||
seconds.toString().padStart(2, '0'),
|
||||
].join(':');
|
||||
|
||||
return { timeRemaining, formattedTime, isTimedOut: timeRemaining === 0 };
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
299
src/store/cbtStore.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist, StateStorage, createJSONStorage } from 'zustand/middleware'
|
||||
import { get, set, del } from 'idb-keyval'
|
||||
|
||||
// Custom IndexedDB storage engine for Zustand
|
||||
const idbStorage: StateStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
return (await get(name)) || null
|
||||
},
|
||||
setItem: async (name: string, value: string): Promise<void> => {
|
||||
await set(name, value)
|
||||
},
|
||||
removeItem: async (name: string): Promise<void> => {
|
||||
await del(name)
|
||||
},
|
||||
}
|
||||
|
||||
export type QuestionStatus = 'UNVISITED' | 'NOT_ANSWERED' | 'ANSWERED' | 'MARKED' | 'ANSWERED_MARKED'
|
||||
|
||||
export interface AnswerState {
|
||||
question_id: string
|
||||
selected_answer: string | null
|
||||
status: QuestionStatus
|
||||
time_spent: number // in seconds
|
||||
}
|
||||
|
||||
export interface ExamAttemptPayload {
|
||||
id: string
|
||||
user_id: string
|
||||
source_type: 'DPS' | 'Test' | 'MicroTest'
|
||||
source_id: string
|
||||
started_at: string
|
||||
submitted_at: string
|
||||
time_taken: number
|
||||
answers: Array<{
|
||||
question_id: string
|
||||
selected_answer: string | null
|
||||
is_correct: boolean // Typically evaluated on server, but requested in payload structure
|
||||
is_subjective: boolean
|
||||
time_spent: number
|
||||
}>
|
||||
score: number
|
||||
total_questions: number
|
||||
accuracy: number
|
||||
status: 'In_Progress' | 'Submitted' | 'Timed_Out'
|
||||
}
|
||||
|
||||
interface CBTState {
|
||||
examId: string | null
|
||||
attemptId: string | null
|
||||
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'SUBMITTED' | 'TIMED_OUT'
|
||||
startTime: number | null
|
||||
totalDuration: number // in seconds
|
||||
questions: any[]
|
||||
answers: Record<string, AnswerState>
|
||||
currentQuestionIndex: number
|
||||
|
||||
// Actions
|
||||
initExam: (examId: string, questions: any[], duration: number) => void
|
||||
startExam: () => void
|
||||
selectAnswer: (questionId: string, answer: string) => void
|
||||
clearResponse: (questionId: string) => void
|
||||
markForReview: (questionId: string) => void
|
||||
goToQuestion: (index: number) => void
|
||||
submitExam: (reason?: 'SUBMITTED' | 'TIMED_OUT') => void
|
||||
addTimeSpent: (questionId: string, seconds: number) => void
|
||||
getAttemptPayload: (userId: string) => ExamAttemptPayload | null
|
||||
submitAttemptToServer: (userId: string) => Promise<string | null>
|
||||
resetExamState: () => void
|
||||
}
|
||||
|
||||
export const useCBTStore = create<CBTState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
examId: null,
|
||||
attemptId: null,
|
||||
status: 'NOT_STARTED',
|
||||
startTime: null,
|
||||
totalDuration: 0,
|
||||
questions: [],
|
||||
answers: {},
|
||||
currentQuestionIndex: 0,
|
||||
|
||||
initExam: (examId, questions, duration) => {
|
||||
const currentExamId = get().examId;
|
||||
const currentQuestions = get().questions;
|
||||
|
||||
// Force reset if it's a new exam or the number of questions has changed
|
||||
const needsReset = currentExamId !== examId || currentQuestions.length !== questions.length;
|
||||
|
||||
if (needsReset) {
|
||||
const initialAnswers: Record<string, AnswerState> = {};
|
||||
questions.forEach((q, idx) => {
|
||||
initialAnswers[q.id] = {
|
||||
question_id: q.id,
|
||||
selected_answer: null,
|
||||
status: idx === 0 ? 'NOT_ANSWERED' : 'UNVISITED',
|
||||
time_spent: 0
|
||||
}
|
||||
})
|
||||
|
||||
set({
|
||||
examId,
|
||||
attemptId: null,
|
||||
status: 'NOT_STARTED',
|
||||
startTime: null,
|
||||
totalDuration: duration,
|
||||
questions,
|
||||
answers: initialAnswers,
|
||||
currentQuestionIndex: 0
|
||||
})
|
||||
} else {
|
||||
// Just update questions array in case text/options changed
|
||||
set({ questions, totalDuration: duration })
|
||||
}
|
||||
},
|
||||
|
||||
startExam: () => {
|
||||
set({ status: 'IN_PROGRESS', startTime: Date.now() })
|
||||
},
|
||||
|
||||
selectAnswer: (questionId, answer) => set((state) => {
|
||||
const current = state.answers[questionId]
|
||||
if (!current) return state;
|
||||
|
||||
let newStatus: QuestionStatus = 'ANSWERED';
|
||||
if (current.status === 'MARKED' || current.status === 'ANSWERED_MARKED') {
|
||||
newStatus = 'ANSWERED_MARKED';
|
||||
}
|
||||
|
||||
return {
|
||||
answers: {
|
||||
...state.answers,
|
||||
[questionId]: { ...current, selected_answer: answer, status: newStatus }
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
clearResponse: (questionId) => set((state) => {
|
||||
const current = state.answers[questionId]
|
||||
if (!current) return state;
|
||||
|
||||
let newStatus: QuestionStatus = 'NOT_ANSWERED';
|
||||
if (current.status === 'ANSWERED_MARKED' || current.status === 'MARKED') {
|
||||
newStatus = 'MARKED';
|
||||
}
|
||||
|
||||
return {
|
||||
answers: {
|
||||
...state.answers,
|
||||
[questionId]: { ...current, selected_answer: null, status: newStatus }
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
markForReview: (questionId) => set((state) => {
|
||||
const current = state.answers[questionId]
|
||||
if (!current) return state;
|
||||
|
||||
let newStatus: QuestionStatus = current.selected_answer ? 'ANSWERED_MARKED' : 'MARKED';
|
||||
|
||||
// If already marked, unmark it
|
||||
if (current.status === 'MARKED') newStatus = 'NOT_ANSWERED';
|
||||
if (current.status === 'ANSWERED_MARKED') newStatus = 'ANSWERED';
|
||||
|
||||
return {
|
||||
answers: {
|
||||
...state.answers,
|
||||
[questionId]: { ...current, status: newStatus }
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
goToQuestion: (index) => set((state) => {
|
||||
if (index < 0 || index >= state.questions.length) return state;
|
||||
|
||||
const targetQId = state.questions[index].id;
|
||||
const targetState = state.answers[targetQId];
|
||||
|
||||
// Update status of the target question to NOT_ANSWERED if it was UNVISITED
|
||||
const newAnswers = { ...state.answers };
|
||||
if (targetState.status === 'UNVISITED') {
|
||||
newAnswers[targetQId] = { ...targetState, status: 'NOT_ANSWERED' };
|
||||
}
|
||||
|
||||
return {
|
||||
currentQuestionIndex: index,
|
||||
answers: newAnswers
|
||||
}
|
||||
}),
|
||||
|
||||
addTimeSpent: (questionId, seconds) => set((state) => {
|
||||
const current = state.answers[questionId]
|
||||
if (!current) return state;
|
||||
return {
|
||||
answers: {
|
||||
...state.answers,
|
||||
[questionId]: { ...current, time_spent: current.time_spent + seconds }
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
submitExam: (reason = 'SUBMITTED') => set({ status: reason }),
|
||||
|
||||
getAttemptPayload: (userId: string) => {
|
||||
const state = get();
|
||||
if (!state.examId || !state.startTime) return null;
|
||||
|
||||
const timeTaken = Math.floor((Date.now() - state.startTime) / 1000);
|
||||
const answerList = Object.values(state.answers);
|
||||
|
||||
// Backend will perform secure evaluation
|
||||
let correctCount = 0; // Removing mock count
|
||||
const finalAnswers = answerList.map(a => {
|
||||
const q = state.questions.find(quest => quest.id === a.question_id);
|
||||
const isSubjective = q?.type === 'Subjective' || q?.type === 'Numerical';
|
||||
|
||||
return {
|
||||
question_id: a.question_id,
|
||||
selected_answer: a.selected_answer,
|
||||
is_correct: false,
|
||||
is_subjective: isSubjective,
|
||||
time_spent: a.time_spent
|
||||
}
|
||||
});
|
||||
|
||||
const totalQ = state.questions.length;
|
||||
const payload: ExamAttemptPayload = {
|
||||
id: crypto.randomUUID(), // Mock Attempt ID
|
||||
user_id: userId,
|
||||
source_type: 'Test', // Or DPS based on exam metadata
|
||||
source_id: state.examId,
|
||||
started_at: new Date(state.startTime).toISOString(),
|
||||
submitted_at: new Date().toISOString(),
|
||||
time_taken: timeTaken,
|
||||
answers: finalAnswers,
|
||||
score: correctCount * 4, // Assuming +4 logic
|
||||
total_questions: totalQ,
|
||||
accuracy: totalQ > 0 ? (correctCount / totalQ) * 100 : 0,
|
||||
status: state.status === 'IN_PROGRESS' ? 'Submitted' : (state.status === 'TIMED_OUT' ? 'Timed_Out' : 'Submitted')
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
submitAttemptToServer: async (userId: string) => {
|
||||
const payload = get().getAttemptPayload(userId);
|
||||
if (!payload) return null;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/attempts/submit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("Failed to submit attempt to server", await res.text());
|
||||
return null;
|
||||
} else {
|
||||
const data = await res.json();
|
||||
console.log("Successfully submitted attempt to server", data.id);
|
||||
set({ attemptId: data.id });
|
||||
return data.id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Network error during submission:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
resetExamState: () => {
|
||||
set({
|
||||
examId: null,
|
||||
attemptId: null,
|
||||
status: 'NOT_STARTED',
|
||||
startTime: null,
|
||||
totalDuration: 0,
|
||||
questions: [],
|
||||
answers: {},
|
||||
currentQuestionIndex: 0
|
||||
});
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'cbt-storage', // unique name
|
||||
storage: createJSONStorage(() => idbStorage), // use custom IndexedDB storage
|
||||
partialize: (state) => ({
|
||||
examId: state.examId,
|
||||
status: state.status,
|
||||
startTime: state.startTime,
|
||||
totalDuration: state.totalDuration,
|
||||
questions: state.questions,
|
||||
answers: state.answers,
|
||||
currentQuestionIndex: state.currentQuestionIndex,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||