diff --git a/CHANGELOG.md b/CHANGELOG.md index 9538be6..16ab1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/); versioning: [SemVer](ht ## [Unreleased] +### Fixed +- Phone header: Studio, data saver and the theme switch were off-screen below ~560 px. Header, Studio and /try now fit 360 px (Phase 23) + ### Added - Weak spots on Saved: wrong quiz answers become a personal revision reel of the explanations you missed (Phase 22) - Study notes `/notes/[id]` and `GET /api/lectures/[id]/notes`: a timestamp-linked study sheet per session, as Markdown or PDF (Phase 22) - Data saver: a header switch (automatic on Save-Data and 2G/3G) that serves lighter Cloudinary renditions and the `sd` streaming ladder; 33–76% fewer bytes measured (Phase 21) +- `/judges`: the submission requirements mapped to 14 Cloudinary capabilities, live URLs and a 90-second test; revision reel on `/saved` (Phase 20) - Concept Map `/concepts` and Compare view `/concepts/[key]`: the same concept from every teacher as one Compare Reel (Phase 19) - Cloudinary asset index: tags and contextual metadata on each session's asset, synced from the Studio and read back with the Search API (Phase 19) diff --git a/README.md b/README.md index 33eb937..d0070b4 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Colleges, clubs and coaching institutes record hundreds of hours of lectures and | **Data saver** | One switch (and automatic on Save-Data or 2G/3G) serves smaller, lower-quality Cloudinary renditions: thumbnails 76% lighter, clips and reels 38–41%, and a streaming ladder that tops out at 0.9 Mbps instead of 3.4. Built for India’s mobile data. | | **Weak spots** | Quiz mistakes are remembered on your device and come back as **one Cloudinary reel of exactly the explanations you missed**, with the answer and the second to jump to. Get a question right and it leaves the list. | | **Study notes** | Every session has a study sheet at `/notes/[id]`: summary, key concepts, chapters and the quiz with answers, each point linked to its timestamp. Copy it as Markdown, download it, or save it as a PDF. | +| **Revision reel** | On `/saved`, the latest saved moments from any sessions play as one labelled video: a revision reel for the night before an exam. | | **Study Packs** | Every session gets a summary, key concepts, a quiz whose explanations **play the moment** the teacher explains it, and a "Session in 60 seconds" highlight reel. Pravaha generates them automatically from the transcript. | | **Moments** | One tap turns any cited clip into a vertical, AI-cropped, subtitled short for WhatsApp or Instagram, with its own share page and preview card. The short is just a Cloudinary URL; nothing is rendered. | | **Insights** | Organizers see what learners ask, the **knowledge gaps** the library can't answer yet (what to record next), and which Moments get shared. Every report downloads as CSV. Visitors can explore a read-only demo of the Studio. | @@ -164,6 +165,8 @@ Diagrams and reasoning: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) · [`docs ## How to test it +> **Judges:** [`/judges`](https://pravaha-cyan.vercel.app/judges) maps the submission requirements to the Cloudinary features, with live URLs and a 90-second test. + Open the live app at **[pravaha-cyan.vercel.app](https://pravaha-cyan.vercel.app)**. Learners need no login. 1. On the home page, type a question or tap a suggestion. Try "Why should the learning rate decrease during training, and how do adaptive optimizers handle it?": live progress appears, then one answer citing two professors from two institutes, plus an Answer Reel. Or ask in Hindi: "ओवरफिटिंग क्या है?" diff --git a/docs/DEMO.md b/docs/DEMO.md index 4be5b80..ccf6f79 100644 --- a/docs/DEMO.md +++ b/docs/DEMO.md @@ -29,6 +29,15 @@ Never show setup, code, dashboards or waiting. Every shot is the problem, the pr **Study shots (if there is time):** answer a quiz question wrong → `/saved` → **Play my weak-spot reel** ("What you get wrong comes back as video.") → a session’s **Study notes** → **Download .md** ("Every point links to the second the teacher said it.") **Bonus shot (if there is time):** header → **Data saver** on → the same answer reel and thumbnails reload visibly lighter. Voice-over: "Built for India’s mobile data: one switch, and Cloudinary serves everything at a fraction of the bytes." +## Phase 18–20 scenes (Oct 3) — swap in for the 2:06–2:35 shots if time is tight + +| Shot | Voice-over | +|---|---| +| Ask "What is the bias-variance trade-off?" → the answer says honestly the lecturer never uses that phrase, then explains the related ideas with clips | "It doesn’t bluff. If the lecturer never said it, Pravaha says so and teaches the closest ideas." | +| Ask the same in Hindi: "ओवरफिटिंग क्या है?" → Hindi answer citing English lectures | "Ask in Hindi, answer from English lectures." | +| `/concepts/overfitting` → play the Compare Reel: three professors, labelled | "Same idea, three teachers, one video, until one explanation clicks." | +| `/learn` → "Choosing a learning rate" → 4-step course video | "Any topic becomes a short course, ordered basics first, edited by Cloudinary." | +| `/judges` → capability list → Studio → Cloudinary asset index (Search API) | "Every capability is listed with live URLs. Even our tags are read back from Cloudinary’s Search API." | ## Pre-Recording Checklist diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 7fcd947..a6b5a5a 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -96,3 +96,5 @@ The core loop (Phases 01–09) is built and verified on real Cloudinary + Neon. | 19 | Concept Map & Cloudinary asset index | Same concept, every teacher, as one Compare Reel; tags + contextual metadata on Cloudinary assets, read back with the Search API | ✅ done (Oct 3) | | 21 | Data saver | Light Cloudinary renditions (35–76% fewer bytes, sd streaming ladder) for slow or metered connections; follows Save-Data and 2G/3G automatically | ✅ done (Oct 3) | | 22 | Weak spots & study notes | Quiz mistakes become a personal revision reel; every session exports a timestamp-linked study sheet (Markdown / print) | ✅ done (Oct 3) | +| 20 | Judges page & revision reel | `/judges` maps the brief to Cloudinary capabilities with live URLs; saved moments replay as one reel | ✅ done (Oct 3) | +| 23 | Phone layout | Header, Studio and /try fit a 360 px phone; e2e guard | ✅ done (Oct 3) | diff --git a/docs/SUBMISSION_CHECKLIST.md b/docs/SUBMISSION_CHECKLIST.md index 677c249..1581d54 100644 --- a/docs/SUBMISSION_CHECKLIST.md +++ b/docs/SUBMISSION_CHECKLIST.md @@ -15,9 +15,11 @@ Deadline: submit by **Oct 3, 2026**; code freeze **Oct 4, 00:15 IST**. Submit vi - [x] Learner flows (Watch, Find, Ask, Moments) work without login (Oct 2: Playwright smoke tests 12/12 against production, desktop and mobile) - [x] Demo library: 6 public sessions (NPTEL excerpts, CC BY-NC-SA, credited in the README), uploaded through the production pipeline on Oct 2 - [x] Organizer and enterprise features merged and checked on Vercel previews (Oct 2–3): demo Studio, Insights charts and CSV export, LMS embed, Privacy and Terms, health endpoint -- [ ] **Cloudinary credits:** 24.3 of 25 used on Oct 3 (free plan). Upgrade, or confirm the reset date is before judging, so new answer clips and Moments keep generating +- [ ] **Cloudinary credits:** the Admin API reported 14.1 of 25 used (56%) at its last update (Oct 2). Every new reel, Moment and Compare Reel spends some, so avoid regenerating them in bulk before judging, and re-check `api.usage()` on submission day - [ ] Trials decision after that: keep `/try` paused (`TRIALS_PER_DAY=0`) or re-enable (`TRIALS_PER_DAY=5`, then redeploy) +- [x] Phone check at 360 px on every key page (Oct 3, Phase 23): header controls reachable, no sideways scroll + ## Repository - [x] Public repo: `Monolithic-Dev/Pravaha` (own repo is allowed by HackIndia's FAQ) - [ ] README explains track, problem, Cloudinary usage, how to test — with live URL, screenshots, demo link (all done except the demo link; screenshots retaken with the real library on Oct 2) @@ -25,6 +27,9 @@ Deadline: submit by **Oct 3, 2026**; code freeze **Oct 4, 00:15 IST**. Submit vi - [ ] Setup instructions a stranger can follow cold (`SETUP.md`) - [x] No API keys or credentials anywhere in history — `git log -p | grep -iE "api_secret|sk-ant|postgres://|passcode"` returns only env-variable references (checked Oct 2) +- [ ] Studio → **Cloudinary asset index** → Sync tags run once on production, so the Search API shows tagged assets +- [x] `/judges` page maps the brief to Cloudinary capabilities and the 90-second test (Phase 20) + ## Demo - [ ] 2–4 minute video recorded per `DEMO.md` (target 2:45), publicly viewable link (check credits first: see its pre-recording checklist) - [ ] Demo video link added to the README diff --git a/docs/TESTING.md b/docs/TESTING.md index 9b1e062..1130c97 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -22,6 +22,12 @@ Scope: the things that would embarrass the product in front of a judge or corrup Against the **deployed** `APP_URL`: home → search a known phrase → result opens `/watch/id?t=` at the right second → Ask a known question → ≥1 citation card → open Moment sheet. Run before recording the demo and before submitting. +### More end-to-end specs (all run against the deployed `APP_URL`, `--project=desktop --project=mobile`) +- `judges.spec.ts`: the judges page, the concept map with its compare reel, `/learn` +- `data-saver.spec.ts`: the header switch makes Cloudinary thumbnails `w_320,q_auto:low`, is remembered, and a session streams the `sp_sd` ladder +- `revision.spec.ts`: a wrong quiz answer appears under Weak spots with a revision reel and clears; study notes carry timestamp links and download as Markdown +- `mobile.spec.ts`: at 360 px the header's Studio, data saver and theme controls are on screen and no page scrolls sideways + ## AI Eval (`pnpm eval:ask`) 10 questions (7 answerable, 3 not) against the demo library; results recorded in `AI_EVALUATION.md`. diff --git a/docs/UX_UI.md b/docs/UX_UI.md index 5572e12..425d990 100644 --- a/docs/UX_UI.md +++ b/docs/UX_UI.md @@ -70,7 +70,7 @@ Skeletons, never bare spinners. 404 session → "This session isn't available." ### Motion & shell (v3) - `rise` (staggered entrance), `word-in` (answer reveal), `lift` (hover), `spinner`; all disabled under `prefers-reduced-motion` -- Sticky blurred header with the Pravaha mark (also the favicon, `app/icon.svg`), Library and Studio links, a skip link, and a footer +- Sticky blurred header with the Pravaha mark (also the favicon, `app/icon.svg`), the page links (Library, Learn, Concepts, Saved, Studio) and the data saver and theme switches. On a phone the links drop to a second, sideways-scrolling row so nothing is pushed off-screen (Phase 23); a skip link, and a footer - Home: "How it works" in three steps under the Ask bar - `/` or Ctrl/⌘K focuses the Ask bar from anywhere diff --git a/docs/phases/PHASE-20-judges-page-revision-reel.md b/docs/phases/PHASE-20-judges-page-revision-reel.md new file mode 100644 index 0000000..145724a --- /dev/null +++ b/docs/phases/PHASE-20-judges-page-revision-reel.md @@ -0,0 +1,15 @@ +# Phase 20 — Judges page and revision reel + +**Status: DONE (Oct 3)** · branch `feature/judges-page-revision-reel` + +## Why +A hackathon is judged in minutes. The product was strong but a judge had to discover how Cloudinary is used by reading code and docs. This phase puts the evidence in the product. + +## What shipped +1. **`/judges`** (`src/app/judges/page.tsx`, `src/lib/capabilities.ts`): the submission requirements mapped to where each is met, a 90-second test with deep links, 14 Cloudinary capabilities (what each does, where to see it, which file), and the live Cloudinary URLs generated from the real library. `tests/unit/capabilities.test.ts` fails if any listed file doesn't exist, so the page can't drift from the code. +2. **Revision reel** on `/saved`: the latest saved moments, from any sessions, as one labelled `fl_splice` video. Saved moments stay on the device; only the Cloudinary URL is built from them. +3. Playwright smoke tests for `/judges`, `/concepts` and `/learn`; the demo script and submission checklist updated. + +## Findings from the Oct 3 architecture review +- Cloudinary credits were 14.1 of 25 (56%) at the Admin API's last update (Oct 2): enough for judging if reels aren't regenerated in bulk. The biggest consumers are `sd_video_second` and AI hover previews. +- The Gemini free quota remains the main production risk; Groq is the working fallback. diff --git a/docs/phases/PHASE-23-mobile-layout.md b/docs/phases/PHASE-23-mobile-layout.md new file mode 100644 index 0000000..840acf4 --- /dev/null +++ b/docs/phases/PHASE-23-mobile-layout.md @@ -0,0 +1,17 @@ +# Phase 23 — Phone layout + +**Status: DONE (Oct 3)** · branch `fix/mobile-layout-audit` + +## Why +A 360 px audit of every key page (Playwright, `isMobile`, scanning for elements past the viewport) found one real bug that appeared on **every page**: the header's single row needed ~560 px, so on a phone **Studio, the data saver switch and the theme switch were off-screen and unreachable** (the page clips horizontal overflow, so there was no scrollbar to hint at them). Judges often open a link on a phone. + +## What changed +- **Header:** on phones the logo and the two switches share row one; the page links sit on a second row that scrolls sideways if it must. From `sm` up it is the same single row as before (checked at 1280 px). +- **Studio:** the page grid had no explicit column, so its content's minimum width pushed it 19 px past the screen. `grid-cols-1` fixes it. +- **/try:** the three-step progress bar wraps instead of overflowing. +- `tests/e2e/mobile.spec.ts` asserts, for 8 pages at 360 px, that Studio, data saver and the theme switch are on screen and the page doesn't scroll sideways. + +## Verified +Before: 11 of 11 audited pages had off-screen header controls. After: 0 offenders on all 11 pages (home's remaining hits are decorative clipped backgrounds). + +Bundled in this PR: the judges page and revision reel (Phase 20), which touch the same header and footer. diff --git a/src/app/globals.css b/src/app/globals.css index 444cfe4..75d9ff8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -180,3 +180,11 @@ body { .playhead { width: 100%; } .lift, .lift:hover { transition: none; transform: none; } } + +/* A row that scrolls sideways without showing a scrollbar (the phone header's page links). */ +.no-scrollbar { + scrollbar-width: none; +} +.no-scrollbar::-webkit-scrollbar { + display: none; +} diff --git a/src/app/judges/page.tsx b/src/app/judges/page.tsx new file mode 100644 index 0000000..e5e0132 --- /dev/null +++ b/src/app/judges/page.tsx @@ -0,0 +1,109 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +import { UnderTheHood } from "@/components/UnderTheHood"; +import { CAPABILITIES } from "@/lib/capabilities"; +import { getConcept, listConcepts } from "@/lib/concepts"; +import { conceptHoodItems, sessionHoodItems, type HoodItem } from "@/lib/hood-items"; +import { getLecture, getSegments, listLectures } from "@/lib/lectures"; +import { getStudyPack } from "@/lib/study-packs"; + +export const dynamic = "force-dynamic"; + +export const metadata: Metadata = { + title: "For judges — Pravaha", + description: "How Pravaha uses Cloudinary, mapped to the hackathon's requirements, with live URLs and a 90-second test.", +}; + +const REQUIREMENTS: [string, string][] = [ + ["Cloudinary is an active part of the product", "Media in is raw video; transcripts, chapters, streams, clips, reels, cards and tags all come from Cloudinary. Without it there is no product."], + ["Upload, manage, transform, optimize, search, deliver", "Signed upload · tags and context · Search API · splice, crop, caption, preview transformations · f_auto/q_auto · HLS delivery."], + ["A live working demo", "This site. Six real NPTEL sessions, no login for learners."], + ["Public repo with setup instructions", "github.com/Monolithic-Dev/Pravaha, SETUP.md."], + ["README: track, problem, Cloudinary usage, how to test", "Track 3 (Your Media-Savvy Startup); see the README."], +]; + +const STEPS: [string, string, string][] = [ + ["Ask", "Type: “What is the bias-variance trade-off?” The answer cites clips; open one. Try it in Hindi: “ओवरफिटिंग क्या है?”", "/"], + ["Watch the answer", "Tap “Watch the answer”: moments from different lecturers as one Cloudinary-edited video.", "/"], + ["Compare teachers", "Open the Overfitting concept: three lecturers, one video.", "/concepts/overfitting"], + ["Build a course", "Ask for “Choosing a learning rate”: a 4-step course, one video.", "/learn?topic=Choosing%20a%20learning%20rate"], + ["Share a Moment", "On any clip tap “Share as Moment”: vertical, speaker-tracked, captioned.", "/search?q=overfitting"], + ["See the proof", "Open “Cloudinary under the hood” at the bottom of a session, answer or concept.", "/#library"], +]; + +export default async function JudgesPage() { + const [lectures, concepts] = await Promise.all([listLectures({ includeAll: false }), listConcepts()]); + const lecture = lectures[0] ? await getLecture(lectures[0].id) : null; + const shared = concepts.find((c) => c.entries.length > 1); + const [segments, pack, concept] = await Promise.all([ + lecture ? getSegments(lecture.id) : [], + lecture ? getStudyPack(lecture.id) : null, + shared ? getConcept(shared.key) : null, + ]); + const items: HoodItem[] = [ + ...(lecture && segments.length ? sessionHoodItems(lecture, segments, pack?.highlights) : []), + ...(concept ? conceptHoodItems(concept.key, concept.moments) : []), + ]; + + return ( +
+

Track 3 · Your Media-Savvy Startup

+

Pravaha, for judges

+

+ Pravaha turns lecture recordings into knowledge you can ask. Every answer is a clip of the teacher saying it, and the clip, + the reel, the share card and the Hindi subtitles are all Cloudinary. This page maps that to the submission requirements. +

+ +
+

The requirements, and where they are met

+
+ {REQUIREMENTS.map(([need, met]) => ( +
+
{need}
+
{met}
+
+ ))} +
+
+ +
+

Test it in 90 seconds

+
    + {STEPS.map(([name, text, href], i) => ( +
  1. + {i + 1} + + {name} + · {text} + +
  2. + ))} +
+
+ +
+

{CAPABILITIES.length} Cloudinary capabilities in use

+ +
+ + {items.length > 0 && ( +
+

Live Cloudinary URLs

+

Generated from the real library right now. Open any of them.

+ +
+ )} +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index de83d1a..6f7479c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -46,35 +46,40 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
@@ -95,6 +100,10 @@ export default function RootLayout({ children }: { children: React.ReactNode }) Privacy {" "} ·{" "} + + For judges + {" "} + ·{" "} Terms {" "} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 76c4726..e858ce7 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -17,6 +17,7 @@ export default async function sitemap(): Promise { { url: base, changeFrequency: "daily", priority: 1 }, { url: `${base}/learn`, changeFrequency: "monthly", priority: 0.6 }, { url: `${base}/concepts`, changeFrequency: "weekly", priority: 0.7 }, + { url: `${base}/judges`, changeFrequency: "monthly", priority: 0.5 }, { url: `${base}/try`, changeFrequency: "monthly", priority: 0.6 }, { url: `${base}/privacy`, changeFrequency: "yearly", priority: 0.2 }, { url: `${base}/terms`, changeFrequency: "yearly", priority: 0.2 }, diff --git a/src/components/SavedView.tsx b/src/components/SavedView.tsx index b9ac122..acc6f3c 100644 --- a/src/components/SavedView.tsx +++ b/src/components/SavedView.tsx @@ -3,10 +3,11 @@ import Image from "next/image"; import Link from "next/link"; +import { ReelPlayer } from "@/components/ReelPlayer"; import { WeakSpots } from "@/components/WeakSpots"; import { useLiteUrl } from "@/lib/data-saver"; import { formatTime } from "@/lib/format"; -import { thumbUrl } from "@/lib/media"; +import { reelUrl, thumbUrl } from "@/lib/media"; import { clearSaved, toggleSavedMoment, useRecentQuestions, useSavedMoments, useWatchProgress, useWeakSpots } from "@/lib/saved"; // The learner's Saved page: everything here lives on this device (src/lib/saved.ts). @@ -16,6 +17,8 @@ export function SavedView() { const questions = useRecentQuestions(); const progress = useWatchProgress(); const weak = useWeakSpots(); + // Revise before an exam: the latest saved moments (any sessions) as one labelled video. + const revision = moments.length > 1 ? reelUrl(moments.slice(0, 5).map((m, i) => ({ publicId: m.publicId, startS: m.startS, endS: m.endS, label: `${i + 1} · ${m.title.slice(0, 40)}` }))) : null; return (
@@ -35,7 +38,16 @@ export function SavedView() { {moments.length === 0 ? ( Tap Save on any answer clip or search result to keep the moment here. ) : ( -
    + <> + {revision && ( + + )} +
      {moments.map((m, i) => (
    • @@ -53,6 +65,7 @@ export function SavedView() {
    • ))}
    + )} diff --git a/src/components/Studio.tsx b/src/components/Studio.tsx index fe05325..5115b7f 100644 --- a/src/components/Studio.tsx +++ b/src/components/Studio.tsx @@ -74,7 +74,7 @@ export function Studio({ demo = false }: { demo?: boolean }) { } return ( -
    +
    {demo ? ( diff --git a/src/components/TryFlow.tsx b/src/components/TryFlow.tsx index d738ac5..b1586d8 100644 --- a/src/components/TryFlow.tsx +++ b/src/components/TryFlow.tsx @@ -58,7 +58,7 @@ export function TryFlow({ seconds, ttlHours }: { seconds: number; ttlHours: numb function Progress({ step }: { step: 1 | 2 | 3 }) { const labels = ["Details", "Upload", "Processing"]; return ( -
      +
        {labels.map((label, i) => { const n = i + 1; const state = n < step ? "done" : n === step ? "current" : "todo"; @@ -72,7 +72,7 @@ function Progress({ step }: { step: 1 | 2 | 3 }) { {state === "done" ? "✓" : n} {label} - {n < 3 && } + {n < 3 && } ); })} diff --git a/src/lib/capabilities.ts b/src/lib/capabilities.ts new file mode 100644 index 0000000..faf5056 --- /dev/null +++ b/src/lib/capabilities.ts @@ -0,0 +1,23 @@ +// Every Cloudinary capability Pravaha uses, where a visitor can see it working, and where it lives in the code. +// The /judges page renders this list, so it must stay true: tests/unit/capabilities.test.ts checks every +// `code` path exists in the repository. + +export type Capability = { name: string; does: string; see: string; code: string }; + +export const CAPABILITIES: Capability[] = [ + { name: "Signed upload preset + Upload Widget", does: "Browser-to-Cloudinary video upload with real progress; the server only signs.", see: "Studio, /try", code: "src/app/api/upload-signature/route.ts" }, + { name: "auto_transcription", does: "Word-timed transcript at upload: the corpus for Find, Ask and subtitles.", see: "Watch → Transcript", code: "src/lib/ingest.ts" }, + { name: "auto_chaptering", does: "AI chapters on the seek bar and as search context.", see: "Watch → Chapters", code: "src/lib/chapters.ts" }, + { name: "Translated transcripts (hi-IN)", does: "Hindi subtitles generated by Cloudinary, plus asking in Hindi.", see: "Watch → subtitles menu", code: "src/lib/subtitles.ts" }, + { name: "Video Player + HLS streaming profile", does: "Adaptive 720p/360p/180p ladder that survives slow mobile data.", see: "Any session", code: "src/components/Player.tsx" }, + { name: "e_preview + fl_getinfo + fl_sprite", does: "AI highlights graph, seek-bar thumbnails and 6-second hover previews.", see: "Library cards, Watch", code: "src/lib/media.ts" }, + { name: "so_/eo_ trims + c_fill,ar_9:16,g_auto", does: "Moments: any sentence becomes a vertical clip that tracks the speaker.", see: "Share as Moment", code: "src/lib/media.ts" }, + { name: "Timed l_text layers", does: "Word-accurate burned-in captions and speaker labels, exact on trimmed clips.", see: "Moments, reels", code: "src/lib/media.ts" }, + { name: "l_video + fl_splice", does: "Answer Reels, Study Pack reels, Learning Paths and Compare Reels: moments from many sessions as one video.", see: "Ask, /learn, /concepts", code: "src/lib/media.ts" }, + { name: "g_auto thumbnails + gradient/text share cards", does: "Open Graph images for every moment, answer, course and concept: one URL, no image service.", see: "/m, /a, /p, /concepts", code: "src/lib/media.ts" }, + { name: "f_auto, q_auto", does: "Best format and quality for each device on every derived asset.", see: "Everywhere", code: "src/lib/media.ts" }, + { name: "Notification webhooks (signed)", does: "Upload to ready with no polling; signature and replay window verified.", see: "Studio status", code: "src/lib/webhook-signature.ts" }, + { name: "Incoming transformation eo_60", does: "Public trial uploads keep only 60 seconds, so abuse cannot cost more.", see: "/try", code: "src/lib/trial-policy.ts" }, + { name: "Tags + contextual metadata", does: "Each asset carries its concepts, speaker and language inside Cloudinary.", see: "Studio → Cloudinary asset index", code: "src/lib/cloudinary-index.ts" }, + { name: "Search API", does: "Reads the tagged assets back from Cloudinary itself.", see: "Studio → Cloudinary asset index", code: "src/lib/cloudinary-index.ts" }, +]; diff --git a/tests/e2e/judges.spec.ts b/tests/e2e/judges.spec.ts new file mode 100644 index 0000000..b7afc46 --- /dev/null +++ b/tests/e2e/judges.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from "@playwright/test"; + +// Smoke tests for the pages a judge is most likely to open (run against the deployed app, like smoke.spec.ts). + +test("the judges page maps the brief to Cloudinary capabilities", async ({ page }) => { + await page.goto("/judges"); + await expect(page.getByRole("heading", { name: /Pravaha, for judges/i })).toBeVisible(); + await expect(page.getByRole("heading", { name: /Cloudinary capabilities in use/i })).toBeVisible(); +}); + +test("the concept map opens a concept with a compare reel", async ({ page }) => { + await page.goto("/concepts"); + await page.getByRole("link", { name: /Overfitting/ }).first().click(); + await expect(page.getByRole("heading", { name: "Overfitting" })).toBeVisible(); + await expect(page.getByRole("button", { name: /back to back/i })).toBeVisible(); +}); + +test("Learn explains itself and takes a topic", async ({ page }) => { + await page.goto("/learn?topic=learning%20rate"); + await expect(page.getByRole("heading", { name: /Learn a topic/i })).toBeVisible(); +}); diff --git a/tests/e2e/mobile.spec.ts b/tests/e2e/mobile.spec.ts new file mode 100644 index 0000000..2265364 --- /dev/null +++ b/tests/e2e/mobile.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from "@playwright/test"; + +// A 360 px phone: every control in the header must be on screen, and no page may scroll sideways. +// (At 360 px the old single-row header needed ~560 px, which pushed Studio, data saver and the theme switch off-screen.) +test.use({ viewport: { width: 360, height: 740 }, hasTouch: true, isMobile: true }); + +const PAGES = ["/", "/concepts", "/learn", "/saved", "/studio", "/try", "/judges", "/privacy"]; + +for (const path of PAGES) { + test(`${path} fits a 360 px phone`, async ({ page }) => { + await page.goto(path); + const nav = page.getByRole("navigation", { name: "Main" }); + for (const name of [/Studio/, /Data saver|data saver/, /theme/i]) { + const el = nav.getByRole(/Studio/.test(String(name)) ? "link" : "button", { name }).first(); + await expect(el).toBeVisible(); + const box = (await el.boundingBox())!; + expect(box.x + box.width, `${name} is on screen`).toBeLessThanOrEqual(361); + } + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); + expect(scrollWidth).toBeLessThanOrEqual(361); + }); +} diff --git a/tests/unit/capabilities.test.ts b/tests/unit/capabilities.test.ts new file mode 100644 index 0000000..cd719bc --- /dev/null +++ b/tests/unit/capabilities.test.ts @@ -0,0 +1,16 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { CAPABILITIES } from "@/lib/capabilities"; + +describe("CAPABILITIES (the /judges page)", () => { + it("points every capability at code that exists", () => { + for (const c of CAPABILITIES) expect(existsSync(join(process.cwd(), c.code)), `${c.name}: ${c.code}`).toBe(true); + }); + + it("has unique names and complete entries", () => { + expect(new Set(CAPABILITIES.map((c) => c.name)).size).toBe(CAPABILITIES.length); + for (const c of CAPABILITIES) expect([c.name, c.does, c.see, c.code].every((s) => s.trim().length > 3)).toBe(true); + }); +});