diff --git a/.env.example b/.env.example index 0c8ed45..2a7a410 100644 --- a/.env.example +++ b/.env.example @@ -1,47 +1,66 @@ -# Copy to .env.local. Never commit real values. All variables are validated in config/env.ts at boot. +# Copy to .env.local. Never commit real values. Every variable is validated in config/env.ts at boot. # Free-tier sign-up notes are in docs/TECH_STACK.md. +# +# Sections are grouped by when they become required. A variable is added to config/env.ts by the +# PR that introduces the service, never earlier — so an unused key here is documentation only. -# ── Firebase (server: service account; Spark plan) ───────────────────────────── +# ══════════════════════════════════════════════════════════════════════════════ +# REQUIRED NOW (config/env.ts validates these) +# ══════════════════════════════════════════════════════════════════════════════ + +# ── Firebase Admin (server; service account; Spark plan) ────────────────────── FIREBASE_PROJECT_ID= FIREBASE_CLIENT_EMAIL= # Single line, keep the \n escapes exactly as exported from the JSON key. FIREBASE_PRIVATE_KEY= -# ── Firebase (client; public by design, restricted by Firestore rules + Auth domains) +# ── Firebase web config (client; public by design, governed by rules + Auth domains) NEXT_PUBLIC_FIREBASE_API_KEY= NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= NEXT_PUBLIC_FIREBASE_PROJECT_ID= NEXT_PUBLIC_FIREBASE_APP_ID= -# ── LLM providers (free tiers) ───────────────────────────────────────────────── -GEMINI_API_KEY= # Google AI Studio -GROQ_API_KEY= # Groq console (fallback) -OLLAMA_BASE_URL=http://localhost:11434 # local dev only, optional +# ── LLM: Google AI Studio (free tier) ───────────────────────────────────────── +GEMINI_API_KEY= + +# ── Vapi (legacy voice pipeline; removed in P4.6 — RISKS R12) ──────────────── +NEXT_PUBLIC_VAPI_WEB_TOKEN= +NEXT_PUBLIC_VAPI_WORKFLOW_ID= + +# ── App ──────────────────────────────────────────────────────────────────────── +NEXT_PUBLIC_APP_URL=http://localhost:3000 +# Set to 1 only in CI build steps that have no secrets. +SKIP_ENV_VALIDATION= + +# ══════════════════════════════════════════════════════════════════════════════ +# ADDED IN LATER PHASES (documented ahead; not yet read by config/env.ts) +# ══════════════════════════════════════════════════════════════════════════════ -# ── Rate limiting / quotas (Upstash Redis free tier) ─────────────────────────── +# P1.5 Rate limiting / quotas — Upstash Redis free tier UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= -# ── Object storage (Cloudflare R2, S3-compatible; or Supabase Storage) ───────── -STORAGE_PROVIDER=r2 # r2 | supabase -R2_ACCOUNT_ID= -R2_ACCESS_KEY_ID= -R2_SECRET_ACCESS_KEY= -R2_BUCKET= -NEXT_PUBLIC_STORAGE_PUBLIC_URL= # public bucket domain for images/audio (also added to CSP) - -# ── Observability ────────────────────────────────────────────────────────────── +# P1.6 Observability — Sentry developer tier, PostHog free tier SENTRY_DSN= NEXT_PUBLIC_SENTRY_DSN= NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -# ── Email (Resend free tier) ─────────────────────────────────────────────────── +# P2.3 LLM fallback — Groq free tier; Ollama for local dev (optional) +GROQ_API_KEY= +OLLAMA_BASE_URL=http://localhost:11434 + +# P2.6 Object storage — Cloudflare R2 (S3-compatible) or Supabase Storage (ADR-0004) +STORAGE_PROVIDER=r2 +R2_ACCOUNT_ID= +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= +R2_BUCKET= +NEXT_PUBLIC_STORAGE_PUBLIC_URL= + +# P3.4 Email — Resend free tier RESEND_API_KEY= EMAIL_FROM=JustPrep -# ── App ──────────────────────────────────────────────────────────────────────── -NEXT_PUBLIC_APP_URL=http://localhost:3000 -CRON_SECRET= # random 32+ bytes; used by GitHub Actions cron → /api/cron/* -SESSION_COOKIE_NAME=session # becomes __Host-session in production automatically -SKIP_ENV_VALIDATION= # set to 1 only in CI build steps that lack secrets +# P5.7 Signed cron — random 32+ bytes; used by GitHub Actions → /api/cron/* +CRON_SECRET= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6e80775 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Normalise line endings (RISKS R19). Text files are LF in the repo and in working trees. +* text=auto eol=lf + +# Lockfiles and generated files +pnpm-lock.yaml -diff + +# Binary assets +*.png binary +*.jpg binary +*.jpeg binary +*.ico binary +*.woff binary +*.woff2 binary +*.webp binary +*.mp3 binary +*.wav binary diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ce4ea79..9b6bbbc 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Security vulnerability - url: https://github.com/Jeston10/JustPrep_Ai_interview/blob/main/SECURITY.md + url: https://github.com/Jeston10/JustPrep/blob/main/SECURITY.md about: Please report security issues privately as described in SECURITY.md. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ebb3dc..f4493de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,15 @@ # Until then this workflow is expected to fail on the legacy codebase; do not weaken it to make it pass. name: CI +# Branching model (PROCESS.md §Branching): PR branch → milestone/* → dev → main. +# The pipeline runs on every PR (any base) and on pushes to the three integration tiers. on: pull_request: push: - branches: [main] + branches: + - main + - dev + - "milestone/**" concurrency: group: ci-${{ github.ref }} @@ -16,7 +21,7 @@ permissions: env: NODE_VERSION: 22 - PNPM_VERSION: 10 + # pnpm version comes from package.json#packageManager (pnpm/action-setup reads it). jobs: static: @@ -24,15 +29,17 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: { fetch-depth: 0 } + with: + fetch-depth: 0 - uses: pnpm/action-setup@v4 - with: { version: ${{ env.PNPM_VERSION }} } - uses: actions/setup-node@v4 - with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm } + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm lint - run: pnpm typecheck - - run: pnpm dlx knip + - run: pnpm exec knip - name: Secret scan uses: gitleaks/gitleaks-action@v2 env: @@ -45,15 +52,18 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: { version: ${{ env.PNPM_VERSION }} } - uses: actions/setup-node@v4 - with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm } + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - run: pnpm install --frozen-lockfile + - uses: actions/setup-java@v4 # Firebase emulators require JDK 21+ + with: + distribution: temurin + java-version: "21" - run: pnpm test -- --coverage - name: Integration (Firestore emulator) - run: pnpm dlx firebase-tools emulators:exec --only auth,firestore "pnpm test:integration" - env: - FIREBASE_PROJECT_ID: demo-justprep + run: pnpm dlx firebase-tools emulators:exec --only auth,firestore --project demo-justprep "pnpm test:integration" build: name: Build · Audit · Size @@ -62,15 +72,16 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: { version: ${{ env.PNPM_VERSION }} } - uses: actions/setup-node@v4 - with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm } + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm audit --audit-level=high - run: pnpm build env: SKIP_ENV_VALIDATION: "1" - - run: pnpm dlx size-limit + - run: pnpm exec size-limit e2e: name: E2E · Responsive · A11y @@ -80,15 +91,39 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: { version: ${{ env.PNPM_VERSION }} } - uses: actions/setup-node@v4 - with: { node-version: ${{ env.NODE_VERSION }}, cache: pnpm } + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - run: pnpm install --frozen-lockfile - - run: pnpm dlx playwright install --with-deps chromium webkit - - run: pnpm dlx firebase-tools emulators:exec --only auth,firestore "pnpm test:e2e" + - uses: actions/setup-java@v4 # Firebase emulators require JDK 21+ + with: + distribution: temurin + java-version: "21" + - run: pnpm exec playwright install --with-deps chromium + # Demo values: the Firebase web config is public by design and the emulator accepts any project id. + # Server secrets are absent, so SKIP_ENV_VALIDATION=1; nothing in the e2e suite needs them yet. + - run: pnpm build + env: + SKIP_ENV_VALIDATION: "1" + NEXT_PUBLIC_FIREBASE_API_KEY: demo-api-key + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: demo-justprep.firebaseapp.com + NEXT_PUBLIC_FIREBASE_PROJECT_ID: demo-justprep + NEXT_PUBLIC_FIREBASE_APP_ID: "1:0:web:demo" + NEXT_PUBLIC_VAPI_WEB_TOKEN: demo + NEXT_PUBLIC_VAPI_WORKFLOW_ID: demo + NEXT_PUBLIC_APP_URL: http://localhost:3000 + - run: pnpm dlx firebase-tools emulators:exec --only auth,firestore --project demo-justprep "pnpm test:e2e" env: - FIREBASE_PROJECT_ID: demo-justprep SKIP_ENV_VALIDATION: "1" + NEXT_PUBLIC_FIREBASE_API_KEY: demo-api-key + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: demo-justprep.firebaseapp.com + NEXT_PUBLIC_FIREBASE_PROJECT_ID: demo-justprep + NEXT_PUBLIC_FIREBASE_APP_ID: "1:0:web:demo" + NEXT_PUBLIC_VAPI_WEB_TOKEN: demo + NEXT_PUBLIC_VAPI_WORKFLOW_ID: demo + NEXT_PUBLIC_APP_URL: http://localhost:3000 + CI: "true" - uses: actions/upload-artifact@v4 if: failure() with: diff --git a/.github/workflows/promote-guard.yml b/.github/workflows/promote-guard.yml new file mode 100644 index 0000000..ecabc24 --- /dev/null +++ b/.github/workflows/promote-guard.yml @@ -0,0 +1,28 @@ +# Enforces the promotion path: only `dev` may open a PR into `main`, and only +# `milestone/*` or hotfix branches may open a PR into `dev`. Pair with branch +# protection (required checks: CI + this job) so the rule cannot be bypassed. +name: Promotion guard + +on: + pull_request: + branches: [main, dev] + +permissions: + contents: read + +jobs: + guard: + name: Check source branch + runs-on: ubuntu-latest + steps: + - name: main accepts only dev + if: github.base_ref == 'main' && github.head_ref != 'dev' + run: | + echo "::error::PRs into main must come from dev (got '${{ github.head_ref }}')." + exit 1 + - name: dev accepts milestone/* and hotfix/* + if: github.base_ref == 'dev' && !startsWith(github.head_ref, 'milestone/') && !startsWith(github.head_ref, 'hotfix/') + run: | + echo "::error::PRs into dev must come from milestone/* or hotfix/* (got '${{ github.head_ref }}')." + exit 1 + - run: echo "Promotion path OK" diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..bb52773 --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +# pnpm settings +# eslint-config-next references its plugins by name; pnpm 10 stopped hoisting them by default. +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* +# Fail on peer-dependency conflicts instead of silently resolving them. +strict-peer-dependencies=false +auto-install-peers=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e2b3c2e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# Prettier ignore — generated, vendored, and binary paths +node_modules +.next +out +build +coverage +playwright-report +test-results +pnpm-lock.yaml +public +*.md diff --git a/.size-limit.json b/.size-limit.json new file mode 100644 index 0000000..d942e57 --- /dev/null +++ b/.size-limit.json @@ -0,0 +1,8 @@ +[ + { + "name": "client JS, all static chunks, gzip — ratchet: lower this as legacy code is removed (P3/P4); per-route budgets arrive with Lighthouse CI (P8.7)", + "path": ".next/static/chunks/**/*.js", + "limit": "560 kB", + "gzip": true + } +] diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..974ee23 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "vitest.explorer", + "ms-playwright.playwright" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cd174ae --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "eslint.useFlatConfig": true, + "eslint.validate": ["javascript", "typescript", "typescriptreact"], + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.importModuleSpecifier": "non-relative", + "tailwindCSS.experimental.classRegex": [ + ["cn\(([^)]*)\)", "[\"'`]([^\"'`]*)[\"'`]"], + ["cva\(([^)]*)\)", "[\"'`]([^\"'`]*)[\"'`]"] + ], + "search.exclude": { "**/node_modules": true, "**/.next": true, "pnpm-lock.yaml": true } +} diff --git a/AGENTS.md b/AGENTS.md index a1aa737..2c474cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,13 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + + # AGENTS.md — Operating manual for JustPrep This file is the single entry point for any engineer or AI agent working in this repository. Read it fully before touching code. It is intentionally short; it links to the authoritative documents in `docs/`. @@ -36,7 +46,7 @@ docs/ Handbook: architecture, standards, security, design, decisions ( ## 4. How to work - **Before starting a task**: read the relevant `docs/` page and the ADRs it references. Check `docs/MILESTONES.md` for where the task sits and what it depends on, and **read the `docs/RISKS.md` rows tagged for that phase** (MILESTONES §13). A PR that closes a risk row says so (`Closes R7`) and updates the row's status. -- **Branching**: `main` is protected. Branch as `type/short-description` (`feat/text-interview-mode`, `fix/profile-cookie-await`). One concern per branch. +- **Branching**: `main` and `dev` are protected. Cut work branches from the current `milestone/p-` branch as `type/short-description` (`feat/text-interview-mode`, `fix/profile-cookie-await`) and PR back into it. Promotion is `milestone/* → dev → main`, each by PR with green CI (`PROCESS.md`). One concern per branch. - **Commits**: Conventional Commits (`feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`, `perf:`, `security:`). Imperative mood, ≤ 72 chars subject, body explains *why*. - **Pull requests**: use the template. Include screenshots for UI at 375 px and 1440 px minimum. Link the plan item or issue. Self-review the diff before requesting review. - **Definition of done** (all required): diff --git a/CHANGELOG.md b/CHANGELOG.md index 757072f..13471ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,27 @@ All notable changes to this project are documented here. Format follows [Keep a - Engineering handbook: `AGENTS.md`, `CONTRIBUTING.md`, `PROCESS.md`, `SECURITY.md`, and `docs/` (guardrails, architecture, tech stack, security, design system, component library, responsive rules, coding standards, quality, implementation plan, milestones execution plan with legacy → target migration map, risk register/watch-list, ADRs 0001–0010). - GitHub templates (issues, PR), `CODEOWNERS`, CI workflow, `lefthook.yml`, `.env.example`. +- Toolchain (PR 0.1): pnpm 10, strict type-aware ESLint with layer rules, Prettier, lefthook hooks, commitlint, `.gitattributes`, `.nvmrc`, VS Code settings. +- Strict TypeScript flags; production build fails on type or lint errors (PR 0.2). +- `config/env.ts` — validated environment, the only `process.env` reader, lint-enforced (PR 0.3). +- Test pipeline (PR 0.5): vitest with coverage thresholds, Playwright at 375/768/1280 on a production build with horizontal-overflow assertions, size-limit ratchet, Firebase emulator config, first unit/e2e tests. CI fully green. +- Branching model: `milestone/* → dev → main` with a promotion-guard workflow and branch protection (`scripts/github/protect-branches.sh`). + ### Changed - README rewritten to describe the product, stack, and how to work in the repo. +- Legacy code brought to lint-clean under the strict config (356 violations → 0). +- Dependencies (PR 0.4): Next 15.3 → 16.3 (Turbopack default, native flat ESLint config), React 19.3, AI SDK 4 → 7 with `@ai-sdk/google` 4 (`generateText` + `Output.object`, `createGoogle`), zod 3 → 4, firebase 12, firebase-admin 14. `pnpm audit`: 27 findings → 0. ESLint 10 and TypeScript 7 deferred until plugin peer ranges allow. +- Dead dependencies and files removed (`pdfjs-dist`, `next-themes`, `lucide-react`, `@radix-ui/react-label`, `tailwindcss-animate`, unused shadcn primitives) — `knip` is clean. +- Firebase Admin initialised lazily so secret-less CI builds succeed. +- `/api/news` reduced to an empty stub (it only ever served fabricated articles); widget removed in P3.5. + +### Fixed +- Profile update always returned 500 (`cookies()` not awaited). +- Duplicate object keys in `DisplayTechIcons`; stateful `/g` regex in `InterviewCard`; `signIn` returned nothing on success; conflicting `AgentProps` definitions. + +### Security +- Removed the server log that printed the raw session cookie and decoded claims (RISKS R1). +- `/api/vapi/generate` no longer echoes internal errors to the client and rejects malformed bodies (full auth + zod lands in P1.4). ## [0.1.0] — 2025-09 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ed7084..ac75866 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,9 +13,13 @@ pnpm dev `lefthook install` runs automatically on `pnpm install` and enables the git hooks. +Optional but recommended: install the [gitleaks](https://github.com/gitleaks/gitleaks) binary (`winget install gitleaks` / `scoop install gitleaks` / `brew install gitleaks`) so secrets are caught at commit time; CI runs it on every push either way. + +If a globally installed older `pnpm` shadows the pinned version, run commands through `corepack pnpm …` or upgrade the global install. + ## Branches and commits -- Branch from `main`: `feat/…`, `fix/…`, `refactor/…`, `docs/…`, `chore/…`, `security/…`. +- Branch from the **current milestone branch** (`milestone/p-`, see `docs/MILESTONES.md`), never from `main`: `feat/…`, `fix/…`, `refactor/…`, `docs/…`, `chore/…`, `security/…`. PRs target the milestone branch. Promotion is milestone → `dev` → `main` (see `PROCESS.md`). - Conventional Commits. Examples: - `feat(attempt): stream interviewer turns in text mode` - `fix(auth): await cookies() in profile update` @@ -30,7 +34,7 @@ pnpm dev 4. Server changes: tick the checklist in `docs/SECURITY.md` §3. 5. Architecture, vendor, data-model, or token changes: include an ADR in `docs/decisions/`. 6. Update docs in the same PR. Add a `CHANGELOG.md` entry under *Unreleased*. -7. Squash-merge once approved. +7. Squash-merge into the milestone branch once approved. Milestone → `dev` and `dev` → `main` promotions are merge commits so history per phase stays traceable. ## Code review standards diff --git a/PROCESS.md b/PROCESS.md index fbe1bda..c3c5621 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -2,11 +2,34 @@ How work flows through this repository, modelled on GitLab's engineering process at small scale. +## Branching and promotion + +``` +chore/… feat/… fix/… security/… one PR each + │ PR (CI green, template complete) + ▼ +milestone/p- one per phase in docs/MILESTONES.md; created from dev + │ PR when the phase exit criteria are met (CI green on the milestone branch) + ▼ +dev integration; deployed to the preview environment; soak-tested + │ PR only from dev (promotion guard) with CI green; merge commit, tagged + ▼ +main production; protected; never receives direct pushes +``` + +Rules: +- `main` and `dev` are protected: PRs only, required checks (`CI` jobs + `Promotion guard`), no force-push, no deletions. Promotions are **merge commits** (not squash/rebase) so `dev` and `main` share history and each phase stays traceable. +- Milestone branches are `milestone/p0-toolchain`, `milestone/p1-security`, … (names in `docs/MILESTONES.md`). They are deleted after their PR into `dev` merges; the next milestone branch is cut from the updated `dev`. +- Work branches are cut from the current milestone branch and merged back into it by PR. They never target `dev` or `main`. +- `hotfix/*` may target `dev` directly (and is then promoted to `main` through `dev`); it is the only exception. +- Every push to `dev`, `milestone/**`, and `main` runs the full pipeline; PRs run it against the merge result. +- A phase is "achieved" when its milestone branch has merged into `dev`, CI is green on `dev`, and the manual soak (release checklist in `docs/QUALITY.md` §5) passes; promotion to `main` then tags the release. + ## Cadence - **Milestones** = phases in `docs/IMPLEMENTATION_PLAN.md`. Each has a GitHub milestone and a tracking issue with the exit criteria as a checklist. - **Iterations** are weekly. Monday: pick issues for the week from the active phase; Friday: release if the exit criteria for the phase are met, otherwise tag a pre-release. -- **Releases**: SemVer tags (`v0.x.y` until Phase 5 completes, then `v1.0.0`). `CHANGELOG.md` is updated per release from Conventional Commits. +- **Releases**: SemVer tags (`v0.x.y` until Phase 5 completes, then `v1.0.0`) applied on `main` when `dev` is promoted. `CHANGELOG.md` is updated per release from Conventional Commits. ## Issue lifecycle diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index f0d350d..0066e9a 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,16 +1,13 @@ -import {ReactNode} from 'react' -import { isAuthenticated } from '@/lib/actions/auth.action' -import { redirect } from 'next/navigation'; +import { redirect } from "next/navigation"; +import { type ReactNode } from "react"; -const Authlayout = async ({children}:{children:ReactNode}) => { +import { isAuthenticated } from "@/lib/actions/auth.action"; + +const Authlayout = async ({ children }: { children: ReactNode }) => { const isUserAuthenticated = await isAuthenticated(); - - if(isUserAuthenticated) redirect ('/'); - return ( -
{children} -
- ) -} -export default Authlayout + if (isUserAuthenticated) redirect("/"); + return
{children}
; +}; + +export default Authlayout; diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx index a3a13ef..98cca39 100644 --- a/app/(auth)/sign-in/page.tsx +++ b/app/(auth)/sign-in/page.tsx @@ -4,4 +4,4 @@ const Page = () => { return ; }; -export default Page; \ No newline at end of file +export default Page; diff --git a/app/(root)/interview/[id]/feedback/page.tsx b/app/(root)/interview/[id]/feedback/page.tsx index 6bd78ff..69237f8 100644 --- a/app/(root)/interview/[id]/feedback/page.tsx +++ b/app/(root)/interview/[id]/feedback/page.tsx @@ -1,46 +1,42 @@ import dayjs from "dayjs"; -import Link from "next/link"; import Image from "next/image"; +import Link from "next/link"; import { redirect } from "next/navigation"; -import { - getFeedbackByInterviewId, - getInterviewById, -} from "@/lib/actions/general.action"; import { Button } from "@/components/ui/button"; + import { getCurrentUser } from "@/lib/actions/auth.action"; +import { getFeedbackByInterviewId, getInterviewById } from "@/lib/actions/general.action"; -const Feedback = async ({ params }: RouteParams) => { +const Feedback = async ({ params }: RouteParams<{ id: string }>) => { const { id } = await params; const user = await getCurrentUser(); + if (!user) redirect("/sign-in"); const interview = await getInterviewById(id); if (!interview) redirect("/"); const feedback = await getFeedbackByInterviewId({ interviewId: id, - userId: user?.id!, + userId: user.id, }); return (

- Feedback on the Interview -{" "} - {interview.role} Interview + Feedback on the Interview - {interview.role} Interview

-
+
{/* Overall Impression */} -
+
star

Overall Impression:{" "} - - {feedback?.totalScore} - + {feedback?.totalScore} /100

@@ -49,9 +45,7 @@ const Feedback = async ({ params }: RouteParams) => {
calendar

- {feedback?.createdAt - ? dayjs(feedback.createdAt).format("MMM D, YYYY h:mm A") - : "N/A"} + {feedback?.createdAt ? dayjs(feedback.createdAt).format("MMM D, YYYY h:mm A") : "N/A"}

@@ -64,7 +58,7 @@ const Feedback = async ({ params }: RouteParams) => { {/* Interview Breakdown */}

Breakdown of the Interview:

- {feedback?.categoryScores?.map((category, index) => ( + {feedback?.categoryScores.map((category, index) => (

{index + 1}. {category.name} ({category.score}/100) @@ -77,7 +71,7 @@ const Feedback = async ({ params }: RouteParams) => {

Strengths

    - {feedback?.strengths?.map((strength, index) => ( + {feedback?.strengths.map((strength, index) => (
  • {strength}
  • ))}
@@ -86,30 +80,65 @@ const Feedback = async ({ params }: RouteParams) => {

Areas for Improvement

    - {feedback?.areasForImprovement?.map((area, index) => ( + {feedback?.areasForImprovement.map((area, index) => (
  • {area}
  • ))}
{/* Links to Practice Section */} -
-

Links to Practice

-
    + @@ -117,20 +146,13 @@ const Feedback = async ({ params }: RouteParams) => {
    @@ -138,4 +160,4 @@ const Feedback = async ({ params }: RouteParams) => { ); }; -export default Feedback; \ No newline at end of file +export default Feedback; diff --git a/app/(root)/interview/[id]/page.tsx b/app/(root)/interview/[id]/page.tsx index b487f6c..7d20cc0 100644 --- a/app/(root)/interview/[id]/page.tsx +++ b/app/(root)/interview/[id]/page.tsx @@ -2,39 +2,37 @@ import Image from "next/image"; import { redirect } from "next/navigation"; import Agent from "@/components/Agent"; -import { getRandomInterviewCover } from "@/lib/utils"; +import DisplayTechIcons from "@/components/DisplayTechIcons"; -import { - getFeedbackByInterviewId, - getInterviewById, -} from "@/lib/actions/general.action"; import { getCurrentUser } from "@/lib/actions/auth.action"; -import DisplayTechIcons from "@/components/DisplayTechIcons"; +import { getFeedbackByInterviewId, getInterviewById } from "@/lib/actions/general.action"; +import { getRandomInterviewCover } from "@/lib/utils"; -const InterviewDetails = async ({ params }: RouteParams) => { +const InterviewDetails = async ({ params }: RouteParams<{ id: string }>) => { const { id } = await params; const user = await getCurrentUser(); + if (!user) redirect("/sign-in"); const interview = await getInterviewById(id); if (!interview) redirect("/"); const feedback = await getFeedbackByInterviewId({ interviewId: id, - userId: user?.id!, + userId: user.id, }); return ( <> -
    -
    -
    +
    +
    +
    cover-image

    {interview.role} Interview

    @@ -42,14 +40,12 @@ const InterviewDetails = async ({ params }: RouteParams) => {
    -

    - {interview.type} -

    +

    {interview.type}

    { ); }; -export default InterviewDetails; \ No newline at end of file +export default InterviewDetails; diff --git a/app/(root)/interview/page.tsx b/app/(root)/interview/page.tsx index b5d4ffb..1abf260 100644 --- a/app/(root)/interview/page.tsx +++ b/app/(root)/interview/page.tsx @@ -1,21 +1,20 @@ +import { redirect } from "next/navigation"; + import Agent from "@/components/Agent"; + import { getCurrentUser } from "@/lib/actions/auth.action"; const Page = async () => { const user = await getCurrentUser(); + if (!user) redirect("/sign-in"); return ( <>

    Interview generation

    - + ); }; -export default Page; \ No newline at end of file +export default Page; diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx index 64570b8..53c7567 100644 --- a/app/(root)/layout.tsx +++ b/app/(root)/layout.tsx @@ -1,29 +1,36 @@ -import { ReactNode } from 'react' -import Image from 'next/image' -import Link from 'next/link' -import { isAuthenticated, getCurrentUser, hasLoggedInToday, getUserLoginStreak } from '@/lib/actions/auth.action' -import { redirect } from 'next/navigation'; -import { headers } from 'next/headers'; -import SettingsMenu from '@/components/SettingsMenu'; -import DailyLoginStar from '@/components/DailyLoginStar'; -import LiveDateTime from '@/components/LiveDateTime'; -import { FiHome, FiUser } from 'react-icons/fi'; -import AIChatbot from '@/components/AIChatbot'; -import DynamicCareerQuote from '@/components/DynamicCareerQuote'; -import { FaLinkedin, FaInstagram, FaGithub } from 'react-icons/fa'; +import { headers } from "next/headers"; +import Image from "next/image"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { type ReactNode } from "react"; +import { FaLinkedin, FaInstagram, FaGithub } from "react-icons/fa"; +import { FiHome, FiUser } from "react-icons/fi"; -const PUBLIC_ROUTES = ['/sign-in', '/sign-up']; +import AIChatbot from "@/components/AIChatbot"; +import DailyLoginStar from "@/components/DailyLoginStar"; +import DynamicCareerQuote from "@/components/DynamicCareerQuote"; +import LiveDateTime from "@/components/LiveDateTime"; +import SettingsMenu from "@/components/SettingsMenu"; -const Rootlayout = async ({children}: {children:ReactNode}) => { - const headersList =await headers(); - const currentPath = headersList.get("x-next-url") || "/"; +import { + isAuthenticated, + getCurrentUser, + hasLoggedInToday, + getUserLoginStreak, +} from "@/lib/actions/auth.action"; + +const PUBLIC_ROUTES = ["/sign-in", "/sign-up"]; + +const Rootlayout = async ({ children }: { children: ReactNode }) => { + const headersList = await headers(); + const currentPath = headersList.get("x-next-url") ?? "/"; // Skip auth check for public pages if (!PUBLIC_ROUTES.includes(currentPath)) { const isUserAuthenticated = await isAuthenticated(); - if(!isUserAuthenticated) { - redirect('/sign-in'); + if (!isUserAuthenticated) { + redirect("/sign-in"); } } @@ -31,46 +38,46 @@ const Rootlayout = async ({children}: {children:ReactNode}) => { let user = null; let initialHasLoggedInToday = false; let initialStreak = 0; - + if (!PUBLIC_ROUTES.includes(currentPath)) { try { user = await getCurrentUser(); if (user?.id) { [initialHasLoggedInToday, initialStreak] = await Promise.all([ hasLoggedInToday(user.id), - getUserLoginStreak(user.id) + getUserLoginStreak(user.id), ]); } - } catch (error) { - console.error("Error getting user data for daily login star:", error); + } catch { + // Streak widget is non-essential; render without it. } } // Restore to simple layout return (
    -