relevo de glm
This commit is contained in:
@@ -1,3 +1,65 @@
|
||||
# Behavioral Guidelines (Karpathy)
|
||||
|
||||
Behavioral guidelines to reduce common LLM coding mistakes. These must be followed for **all** work in this project.
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them — don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it — don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
|
||||
@@ -1,66 +1,52 @@
|
||||
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).
|
||||
# Film Intel
|
||||
|
||||
## Getting Started
|
||||
Advanced movie insight you won't find in one click. Search any film and generate
|
||||
ratings explained, skip guides, sensitivity warnings, international ratings,
|
||||
production facts, and historical-accuracy breakdowns.
|
||||
|
||||
First, run the development server:
|
||||
## Requirements
|
||||
|
||||
- Node.js 22
|
||||
- A TMDB API key (https://www.themoviedb.org/settings/api)
|
||||
- An LLM API key (OpenRouter or any OpenAI-compatible endpoint)
|
||||
- PostgreSQL for caching generated insight cards (optional but recommended)
|
||||
|
||||
Copy `.env.example` to `.env` and fill in the keys.
|
||||
|
||||
## Development with Docker (recommended)
|
||||
|
||||
Starts the app (with live reload) and the database, and runs migrations
|
||||
automatically on first start:
|
||||
|
||||
```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.
|
||||
|
||||
## Development with Docker (recommended for full features)
|
||||
|
||||
The project uses Postgres for caching generated movie insight cards. For the best experience (including persistent cache and automatic migrations):
|
||||
|
||||
```bash
|
||||
# Start everything (app + database)
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
- Open http://localhost:3000
|
||||
- The first time it will run DB migrations automatically.
|
||||
- Source code is mounted for live reload (`npm run dev` inside the container).
|
||||
- Source code is mounted for live reload (`npm run dev` inside the container)
|
||||
- Stop with `docker compose -f docker-compose.dev.yml down`
|
||||
|
||||
Environment variables (TMDB_API_KEY, LLM_API_KEY, etc.) are read from your `.env` file.
|
||||
|
||||
If you prefer running without Docker:
|
||||
## Development without Docker
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
> Note: Without the database the insight cards will still generate (they just won't be cached), thanks to graceful cache handling.
|
||||
> Without a database the insight cards will still generate (they just won't be
|
||||
> cached), thanks to graceful cache handling. Run `npm run db:migrate` against a
|
||||
> reachable Postgres to enable caching.
|
||||
|
||||
## Production
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml up --build
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
- `npm run dev` — start the Next.js dev server
|
||||
- `npm run build` — production build
|
||||
- `npm run start` — serve the production build
|
||||
- `npm run lint` — run ESLint
|
||||
- `npm run db:migrate` — apply database migrations
|
||||
|
||||
+8
-4
@@ -1,8 +1,12 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #020617;
|
||||
--foreground: #f8fafc;
|
||||
--background: #f9f6f0;
|
||||
--foreground: #1c1917;
|
||||
--card: #ffffff;
|
||||
--card-border: #e7e5e4;
|
||||
--muted: #57534e;
|
||||
--accent: #b45309;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -13,8 +17,8 @@
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
background: #f9f6f0;
|
||||
color: #1c1917;
|
||||
font-family: var(--font-geist-sans), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export default function RootLayout({
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col bg-slate-950 text-slate-100">
|
||||
<body className="min-h-full flex flex-col bg-[#f9f6f0] text-[#1c1917]">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default async function MoviePage({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer className="mt-16 border-t border-white/10 pt-6 text-center text-xs text-slate-500">
|
||||
<footer className="mt-16 border-t border-stone-200 pt-6 text-center text-xs text-stone-500">
|
||||
AI-generated content for informational purposes. Verify timestamps on
|
||||
your copy.
|
||||
</footer>
|
||||
|
||||
+40
-20
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SearchBar } from "@/components/SearchBar";
|
||||
import { MovieHeader } from "@/components/MovieHeader";
|
||||
import { MovieInsightPanel } from "@/components/MovieInsightPanel";
|
||||
@@ -16,6 +16,33 @@ export default function HomePage() {
|
||||
const [isLoadingMovie, setIsLoadingMovie] = useState(false);
|
||||
const [movieError, setMovieError] = useState<string | null>(null);
|
||||
|
||||
// Preload a random interesting movie on initial page load to demonstrate the app
|
||||
useEffect(() => {
|
||||
const DEMO_IDS = [27205, 157336, 155, 550, 496243, 424]; // Inception, Interstellar, Dark Knight, Fight Club, Parasite, Schindler's List
|
||||
const loadDemo = async () => {
|
||||
const id = DEMO_IDS[Math.floor(Math.random() * DEMO_IDS.length)];
|
||||
try {
|
||||
const res = await fetch(`/api/movie/${id}`);
|
||||
if (!res.ok) return;
|
||||
const details: TmdbMovieDetails = await res.json();
|
||||
const minimalSearch: TmdbSearchResult = {
|
||||
id: details.id,
|
||||
title: details.title,
|
||||
release_date: details.release_date || "",
|
||||
poster_path: details.poster_path,
|
||||
overview: details.overview || "",
|
||||
};
|
||||
setSelectedMovie(minimalSearch);
|
||||
setMovieDetails(details);
|
||||
} catch {
|
||||
// silently fall back to empty state if demo preload fails
|
||||
}
|
||||
};
|
||||
if (!selectedMovie) {
|
||||
void loadDemo();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelect = async (movie: TmdbSearchResult) => {
|
||||
setSelectedMovie(movie);
|
||||
setMovieDetails(null);
|
||||
@@ -38,25 +65,18 @@ export default function HomePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedMovie(null);
|
||||
setMovieDetails(null);
|
||||
setIsLoadingMovie(false);
|
||||
setMovieError(null);
|
||||
};
|
||||
|
||||
const hasFullMovie = movieDetails != null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Compact navbar: title + slogan on left, search on right */}
|
||||
<nav className="sticky top-0 z-50 border-b border-white/10 bg-slate-950/95 backdrop-blur supports-[backdrop-filter]:bg-slate-950/80">
|
||||
<nav className="sticky top-0 z-50 border-b border-stone-200 bg-white">
|
||||
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between gap-4 px-6">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="font-semibold tracking-tight text-white">
|
||||
<span className="font-semibold tracking-tight text-stone-950">
|
||||
Film Intel
|
||||
</span>
|
||||
<span className="hidden truncate text-xs text-slate-400 sm:inline">
|
||||
<span className="hidden truncate text-xs text-stone-500 sm:inline">
|
||||
Movie insight you won't find in one click
|
||||
</span>
|
||||
</div>
|
||||
@@ -74,7 +94,7 @@ export default function HomePage() {
|
||||
<main className="mx-auto min-h-[calc(100vh-3.5rem)] w-full max-w-6xl px-6">
|
||||
{!selectedMovie && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<p className="max-w-md text-sm text-slate-400">
|
||||
<p className="max-w-md text-sm text-stone-600">
|
||||
Search for any film using the bar above. We'll generate
|
||||
ratings explained, skip guides, sensitivity warnings, production
|
||||
facts, and more.
|
||||
@@ -85,30 +105,30 @@ export default function HomePage() {
|
||||
{selectedMovie && (
|
||||
<div className="space-y-8 py-8">
|
||||
{hasFullMovie ? (
|
||||
<MovieHeader movie={movieDetails} onClear={clearSelection} />
|
||||
<MovieHeader movie={movieDetails} showBack={false} />
|
||||
) : (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold text-white">
|
||||
<h2 className="text-2xl font-semibold text-stone-950">
|
||||
{selectedMovie.title}
|
||||
{selectedMovie.release_date ? (
|
||||
<span className="ml-2 font-normal text-slate-400">
|
||||
<span className="ml-2 font-normal text-stone-500">
|
||||
({selectedMovie.release_date.slice(0, 4)})
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
{selectedMovie.overview && (
|
||||
<p className="text-sm leading-relaxed text-slate-400">
|
||||
<p className="text-sm leading-relaxed text-stone-600">
|
||||
{selectedMovie.overview}
|
||||
</p>
|
||||
)}
|
||||
{isLoadingMovie && (
|
||||
<p className="text-sm text-amber-300/80">
|
||||
<p className="text-sm text-amber-600">
|
||||
Loading extended details...
|
||||
</p>
|
||||
)}
|
||||
{movieError && (
|
||||
<p className="text-sm text-red-400">
|
||||
<p className="text-sm text-red-600">
|
||||
{movieError}. Card content will still load.
|
||||
</p>
|
||||
)}
|
||||
@@ -124,7 +144,7 @@ export default function HomePage() {
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-white/10 py-6 text-center text-xs text-slate-500">
|
||||
<footer className="border-t border-stone-200 py-6 text-center text-xs text-stone-500">
|
||||
AI-generated content for informational purposes. Verify timestamps on
|
||||
your copy.
|
||||
</footer>
|
||||
|
||||
@@ -17,19 +17,19 @@ export function InfoCard({ meta, selected, onClick }: InfoCardProps) {
|
||||
className={cn(
|
||||
"group rounded-2xl border p-5 text-left transition",
|
||||
selected
|
||||
? "border-amber-400/40 bg-amber-400/10 shadow-lg shadow-amber-500/10"
|
||||
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]",
|
||||
? "border-amber-400 bg-amber-50 shadow-sm"
|
||||
: "border-stone-200 bg-white hover:border-stone-300 hover:bg-stone-50",
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<h3 className="font-medium text-white">{meta.title}</h3>
|
||||
<h3 className="font-medium text-stone-950">{meta.title}</h3>
|
||||
{meta.badge ? (
|
||||
<span className="rounded-full bg-emerald-400/15 px-2 py-0.5 text-[11px] font-medium uppercase tracking-wide text-emerald-300">
|
||||
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-medium uppercase tracking-wide text-emerald-700">
|
||||
{meta.badge}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-sm text-slate-400">{meta.subtitle}</p>
|
||||
<p className="text-sm text-stone-600">{meta.subtitle}</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+11
-20
@@ -5,10 +5,10 @@ import { getPosterUrl, getUsCertification, type TmdbMovieDetails } from "@/lib/t
|
||||
|
||||
export function MovieHeader({
|
||||
movie,
|
||||
onClear,
|
||||
showBack = true,
|
||||
}: {
|
||||
movie: TmdbMovieDetails;
|
||||
onClear?: () => void;
|
||||
showBack?: boolean;
|
||||
}) {
|
||||
const poster = getPosterUrl(movie.poster_path, "w500");
|
||||
const year = movie.release_date?.slice(0, 4);
|
||||
@@ -16,19 +16,10 @@ export function MovieHeader({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{onClear ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClear}
|
||||
className="inline-flex w-fit items-center gap-2 text-sm text-slate-400 transition hover:text-amber-200"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Search another movie
|
||||
</button>
|
||||
) : (
|
||||
{showBack && (
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex w-fit items-center gap-2 text-sm text-slate-400 transition hover:text-amber-200"
|
||||
className="inline-flex w-fit items-center gap-2 text-sm text-stone-500 transition hover:text-amber-700"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to search
|
||||
@@ -36,7 +27,7 @@ export function MovieHeader({
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-end">
|
||||
<div className="relative mx-auto h-56 w-36 shrink-0 overflow-hidden rounded-2xl border border-white/10 bg-slate-900 shadow-2xl sm:mx-0">
|
||||
<div className="relative mx-auto h-56 w-36 shrink-0 overflow-hidden rounded-2xl border border-stone-200 bg-stone-100 shadow-sm sm:mx-0">
|
||||
{poster ? (
|
||||
<Image
|
||||
src={poster}
|
||||
@@ -47,7 +38,7 @@ export function MovieHeader({
|
||||
priority
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-slate-500">
|
||||
<div className="flex h-full items-center justify-center text-sm text-stone-500">
|
||||
No poster
|
||||
</div>
|
||||
)}
|
||||
@@ -55,10 +46,10 @@ export function MovieHeader({
|
||||
|
||||
<div className="space-y-3 text-center sm:text-left">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-white md:text-4xl">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-stone-950 md:text-4xl">
|
||||
{movie.title}
|
||||
</h1>
|
||||
<p className="mt-1 text-slate-400">
|
||||
<p className="mt-1 text-stone-600">
|
||||
{[year, movie.runtime ? `${movie.runtime} min` : null]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
@@ -67,14 +58,14 @@ export function MovieHeader({
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-2 sm:justify-start">
|
||||
{certification ? (
|
||||
<span className="rounded-full border border-amber-400/30 bg-amber-400/10 px-3 py-1 text-sm font-medium text-amber-200">
|
||||
<span className="rounded-full border border-amber-300 bg-amber-100 px-3 py-1 text-sm font-medium text-amber-700">
|
||||
{certification}
|
||||
</span>
|
||||
) : null}
|
||||
{movie.genres.map((genre) => (
|
||||
<span
|
||||
key={genre.id}
|
||||
className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-sm text-slate-300"
|
||||
className="rounded-full border border-stone-200 bg-stone-100 px-3 py-1 text-sm text-stone-700"
|
||||
>
|
||||
{genre.name}
|
||||
</span>
|
||||
@@ -82,7 +73,7 @@ export function MovieHeader({
|
||||
</div>
|
||||
|
||||
{movie.overview ? (
|
||||
<p className="max-w-3xl text-sm leading-relaxed text-slate-400 md:text-base">
|
||||
<p className="max-w-3xl text-sm leading-relaxed text-stone-600 md:text-base">
|
||||
{movie.overview}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
+12
-12
@@ -74,7 +74,7 @@ export function SearchBar({
|
||||
<div className="relative">
|
||||
<Search
|
||||
className={cn(
|
||||
"pointer-events-none absolute top-1/2 -translate-y-1/2 text-amber-200/50",
|
||||
"pointer-events-none absolute top-1/2 -translate-y-1/2 text-stone-400",
|
||||
compact ? "left-3 h-4 w-4" : "left-4 h-5 w-5",
|
||||
)}
|
||||
/>
|
||||
@@ -102,7 +102,7 @@ export function SearchBar({
|
||||
onFocus={() => query.trim() && setOpen(true)}
|
||||
placeholder="Search for a movie..."
|
||||
className={cn(
|
||||
"w-full border border-white/10 bg-white/5 text-white outline-none transition focus:border-amber-400/40 focus:bg-white/[0.07] focus:ring-2 focus:ring-amber-400/20",
|
||||
"w-full border border-stone-300 bg-white text-stone-950 outline-none transition focus:border-amber-500 focus:bg-amber-50 focus:ring-2 focus:ring-amber-200",
|
||||
compact
|
||||
? "rounded-xl py-2 pl-9 pr-3 text-sm"
|
||||
: "rounded-2xl py-4 pl-12 pr-4 text-base",
|
||||
@@ -113,13 +113,13 @@ export function SearchBar({
|
||||
{open && (
|
||||
<ul
|
||||
role="listbox"
|
||||
className="absolute z-50 mt-2 w-full overflow-hidden rounded-2xl border border-white/10 bg-slate-950 shadow-2xl"
|
||||
className="absolute z-50 mt-2 w-full overflow-hidden rounded-2xl border border-stone-200 bg-white shadow-xl"
|
||||
>
|
||||
{loading && (
|
||||
<li className="px-4 py-3 text-sm text-slate-400">Searching...</li>
|
||||
<li className="px-4 py-3 text-sm text-stone-600">Searching...</li>
|
||||
)}
|
||||
{!loading && results.length === 0 && (
|
||||
<li className="px-4 py-3 text-sm text-slate-400">No results found</li>
|
||||
<li className="px-4 py-3 text-sm text-stone-600">No results found</li>
|
||||
)}
|
||||
{!loading &&
|
||||
results.map((result) => {
|
||||
@@ -128,7 +128,7 @@ export function SearchBar({
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
<div className="relative h-14 w-10 shrink-0 overflow-hidden rounded-md bg-slate-800">
|
||||
<div className="relative h-14 w-10 shrink-0 overflow-hidden rounded-md bg-stone-200">
|
||||
{poster ? (
|
||||
<img
|
||||
src={poster}
|
||||
@@ -136,21 +136,21 @@ export function SearchBar({
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-[10px] text-slate-500">
|
||||
<div className="flex h-full items-center justify-center text-[10px] text-stone-500">
|
||||
N/A
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium text-white">
|
||||
<p className="truncate font-medium text-stone-950">
|
||||
{result.title}
|
||||
{year ? (
|
||||
<span className="ml-2 font-normal text-slate-400">
|
||||
<span className="ml-2 font-normal text-stone-500">
|
||||
({year})
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p className="line-clamp-1 text-sm text-slate-400">
|
||||
<p className="line-clamp-1 text-sm text-stone-600">
|
||||
{result.overview || "No overview available"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@ export function SearchBar({
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
}}
|
||||
className="flex w-full items-center gap-3 border-b border-white/5 px-4 py-3 text-left transition hover:bg-white/5 last:border-b-0"
|
||||
className="flex w-full items-center gap-3 border-b border-stone-100 px-4 py-3 text-left transition hover:bg-stone-50 last:border-b-0"
|
||||
>
|
||||
{inner}
|
||||
</button>
|
||||
@@ -176,7 +176,7 @@ export function SearchBar({
|
||||
href={`/movie/${result.id}`}
|
||||
prefetch
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex items-center gap-3 border-b border-white/5 px-4 py-3 transition hover:bg-white/5 last:border-b-0"
|
||||
className="flex items-center gap-3 border-b border-stone-100 px-4 py-3 transition hover:bg-stone-50 last:border-b-0"
|
||||
>
|
||||
{inner}
|
||||
</Link>
|
||||
|
||||
@@ -65,26 +65,26 @@ export function CardContent({ movieId, cardType }: CardContentProps) {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-3 rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="h-4 w-40 animate-pulse rounded bg-white/10" />
|
||||
<div className="h-4 w-full animate-pulse rounded bg-white/10" />
|
||||
<div className="h-4 w-5/6 animate-pulse rounded bg-white/10" />
|
||||
<p className="text-sm text-slate-400">Generating insight...</p>
|
||||
<div className="space-y-3 rounded-2xl border border-stone-200 bg-white p-6">
|
||||
<div className="h-4 w-40 animate-pulse rounded bg-stone-200" />
|
||||
<div className="h-4 w-full animate-pulse rounded bg-stone-200" />
|
||||
<div className="h-4 w-5/6 animate-pulse rounded bg-stone-200" />
|
||||
<p className="text-sm text-stone-600">Generating insight...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-red-400/20 bg-red-400/5 p-6">
|
||||
<p className="text-red-200">{error}</p>
|
||||
<div className="rounded-2xl border border-red-200 bg-red-50 p-6">
|
||||
<p className="text-red-700">{error}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setRetryToken((token) => token + 1);
|
||||
}}
|
||||
className="mt-4 rounded-lg border border-white/10 px-4 py-2 text-sm text-white transition hover:bg-white/5"
|
||||
className="mt-4 rounded-lg border border-stone-300 px-4 py-2 text-sm text-stone-800 transition hover:bg-stone-100"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function HistoricalAccuracyView({ data }: { data: unknown }) {
|
||||
const result = historicalAccuracySchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 text-sm text-slate-400">
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 text-sm text-stone-700">
|
||||
Could not display this insight (data format issue). Try another card or refresh later.
|
||||
</div>
|
||||
);
|
||||
@@ -15,32 +15,32 @@ export function HistoricalAccuracyView({ data }: { data: unknown }) {
|
||||
const parsed = result.data as HistoricalAccuracy;
|
||||
|
||||
return (
|
||||
<div className="space-y-5 rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="space-y-5 rounded-2xl border border-stone-200 bg-white p-6">
|
||||
<div className="flex items-end gap-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">
|
||||
<p className="text-xs uppercase tracking-wide text-stone-500">
|
||||
Accuracy score
|
||||
</p>
|
||||
<p className="text-4xl font-semibold text-amber-200">
|
||||
<p className="text-4xl font-semibold text-amber-700">
|
||||
{parsed.accuracyScore}
|
||||
<span className="text-lg text-slate-500">/100</span>
|
||||
<span className="text-lg text-stone-500">/100</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="pb-1 text-sm capitalize text-slate-400">
|
||||
<p className="pb-1 text-sm capitalize text-stone-600">
|
||||
Confidence: {parsed.confidence}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-slate-300">{parsed.summary}</p>
|
||||
<p className="text-stone-700">{parsed.summary}</p>
|
||||
|
||||
{parsed.accurateElements.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium text-white">What holds up</p>
|
||||
<p className="mb-2 text-sm font-medium text-stone-950">What holds up</p>
|
||||
<ul className="space-y-2">
|
||||
{parsed.accurateElements.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="rounded-lg border border-emerald-400/20 bg-emerald-400/5 px-3 py-2 text-sm text-slate-300"
|
||||
className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-stone-700"
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
@@ -51,20 +51,20 @@ export function HistoricalAccuracyView({ data }: { data: unknown }) {
|
||||
|
||||
{parsed.inventions.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium text-white">
|
||||
<p className="mb-2 text-sm font-medium text-stone-950">
|
||||
Dramatizations & inventions
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{parsed.inventions.map((item, index) => (
|
||||
<div
|
||||
key={`${item.claim}-${index}`}
|
||||
className="rounded-xl border border-white/10 bg-slate-950/40 p-4"
|
||||
className="rounded-xl border border-stone-200 bg-stone-100 p-4"
|
||||
>
|
||||
<p className="text-sm text-slate-300">
|
||||
<span className="text-slate-500">Film claim:</span> {item.claim}
|
||||
<p className="text-sm text-stone-700">
|
||||
<span className="text-stone-500">Film claim:</span> {item.claim}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-slate-300">
|
||||
<span className="text-slate-500">Reality:</span> {item.reality}
|
||||
<p className="mt-2 text-sm text-stone-700">
|
||||
<span className="text-stone-500">Reality:</span> {item.reality}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function InternationalRatingsView({ data }: { data: unknown }) {
|
||||
const result = internationalRatingsSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 text-sm text-slate-400">
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 text-sm text-stone-700">
|
||||
Could not display this insight (data format issue). Try another card or refresh later.
|
||||
</div>
|
||||
);
|
||||
@@ -15,21 +15,21 @@ export function InternationalRatingsView({ data }: { data: unknown }) {
|
||||
const parsed = result.data as InternationalRatings;
|
||||
|
||||
return (
|
||||
<div className="space-y-5 rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="space-y-5 rounded-2xl border border-stone-200 bg-white p-6">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{parsed.ratings.map((rating) => (
|
||||
<div
|
||||
key={rating.countryCode}
|
||||
className="rounded-xl border border-white/10 bg-slate-950/40 p-4"
|
||||
className="rounded-xl border border-stone-200 bg-stone-100 p-4"
|
||||
>
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">
|
||||
<p className="text-xs uppercase tracking-wide text-stone-500">
|
||||
{rating.country} ({rating.countryCode})
|
||||
</p>
|
||||
<p className="mt-1 text-xl font-semibold text-amber-200">
|
||||
<p className="mt-1 text-xl font-semibold text-amber-700">
|
||||
{rating.rating}
|
||||
</p>
|
||||
{rating.descriptors?.length ? (
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
<p className="mt-2 text-sm text-stone-600">
|
||||
{rating.descriptors.join(" · ")}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -37,12 +37,12 @@ export function InternationalRatingsView({ data }: { data: unknown }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
||||
<p className="text-sm leading-relaxed text-slate-300">
|
||||
<div className="rounded-xl border border-stone-200 bg-stone-100 p-4">
|
||||
<p className="text-sm leading-relaxed text-stone-700">
|
||||
{parsed.discrepancyNote}
|
||||
</p>
|
||||
{(parsed.strictestCountry || parsed.mostLenientCountry) && (
|
||||
<div className="mt-3 flex flex-wrap gap-4 text-sm text-slate-400">
|
||||
<div className="mt-3 flex flex-wrap gap-4 text-sm text-stone-600">
|
||||
{parsed.strictestCountry ? (
|
||||
<span>Strictest: {parsed.strictestCountry}</span>
|
||||
) : null}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function ProductionFactsView({ data }: { data: unknown }) {
|
||||
const result = productionFactsSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 text-sm text-slate-400">
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 text-sm text-stone-700">
|
||||
Could not display this insight (data format issue). Try another card or refresh later.
|
||||
</div>
|
||||
);
|
||||
@@ -15,7 +15,7 @@ export function ProductionFactsView({ data }: { data: unknown }) {
|
||||
const parsed = result.data as ProductionFacts;
|
||||
|
||||
return (
|
||||
<div className="space-y-5 rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="space-y-5 rounded-2xl border border-stone-200 bg-white p-6">
|
||||
{(parsed.budget || parsed.boxOffice) && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{parsed.budget ? <Stat label="Budget" value={parsed.budget} /> : null}
|
||||
@@ -27,12 +27,12 @@ export function ProductionFactsView({ data }: { data: unknown }) {
|
||||
|
||||
{parsed.filmingLocations?.length ? (
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-slate-400">Filming locations</p>
|
||||
<p className="mb-2 text-sm text-stone-600">Filming locations</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parsed.filmingLocations.map((location) => (
|
||||
<span
|
||||
key={location}
|
||||
className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-sm text-slate-300"
|
||||
className="rounded-full border border-stone-200 bg-stone-100 px-3 py-1 text-sm text-stone-700"
|
||||
>
|
||||
{location}
|
||||
</span>
|
||||
@@ -45,12 +45,12 @@ export function ProductionFactsView({ data }: { data: unknown }) {
|
||||
{parsed.facts.map((item, index) => (
|
||||
<div
|
||||
key={`${item.category}-${index}`}
|
||||
className="rounded-xl border border-white/10 bg-slate-950/40 p-4"
|
||||
className="rounded-xl border border-stone-200 bg-stone-100 p-4"
|
||||
>
|
||||
<p className="text-xs uppercase tracking-wide text-amber-200/80">
|
||||
<p className="text-xs uppercase tracking-wide text-amber-700">
|
||||
{item.category}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-slate-300">
|
||||
<p className="mt-2 text-sm leading-relaxed text-stone-700">
|
||||
{item.fact}
|
||||
</p>
|
||||
</div>
|
||||
@@ -62,9 +62,9 @@ export function ProductionFactsView({ data }: { data: unknown }) {
|
||||
|
||||
function Stat({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">{label}</p>
|
||||
<p className="mt-1 text-lg font-medium text-white">{value}</p>
|
||||
<div className="rounded-xl border border-stone-200 bg-stone-100 p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-stone-500">{label}</p>
|
||||
<p className="mt-1 text-lg font-medium text-stone-950">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,16 +5,16 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const intensityColors = {
|
||||
mild: "text-emerald-300 bg-emerald-400/10 border-emerald-400/20",
|
||||
moderate: "text-amber-300 bg-amber-400/10 border-amber-400/20",
|
||||
strong: "text-red-300 bg-red-400/10 border-red-400/20",
|
||||
mild: "text-emerald-700 bg-emerald-100 border-emerald-200",
|
||||
moderate: "text-amber-700 bg-amber-100 border-amber-200",
|
||||
strong: "text-red-700 bg-red-100 border-red-200",
|
||||
};
|
||||
|
||||
export function RatingExplainedView({ data }: { data: unknown }) {
|
||||
const result = ratingExplainedSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 text-sm text-slate-400">
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 text-sm text-stone-700">
|
||||
Could not display this insight (data format issue). Try another card or refresh later.
|
||||
</div>
|
||||
);
|
||||
@@ -22,25 +22,25 @@ export function RatingExplainedView({ data }: { data: unknown }) {
|
||||
const parsed = result.data as RatingExplained;
|
||||
|
||||
return (
|
||||
<div className="space-y-5 rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="space-y-5 rounded-2xl border border-stone-200 bg-white p-6">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-wide text-slate-500">
|
||||
<p className="text-sm uppercase tracking-wide text-stone-500">
|
||||
{parsed.region} rating
|
||||
</p>
|
||||
<h3 className="mt-1 text-2xl font-semibold text-amber-200">
|
||||
<h3 className="mt-1 text-2xl font-semibold text-amber-700">
|
||||
{parsed.officialRating}
|
||||
</h3>
|
||||
<p className="mt-3 text-slate-300">{parsed.summary}</p>
|
||||
<p className="mt-3 text-stone-700">{parsed.summary}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{parsed.factors.map((factor, index) => (
|
||||
<div
|
||||
key={`${factor.category}-${index}`}
|
||||
className="rounded-xl border border-white/10 bg-slate-950/40 p-4"
|
||||
className="rounded-xl border border-stone-200 bg-stone-100 p-4"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium capitalize text-white">
|
||||
<span className="text-sm font-medium capitalize text-stone-950">
|
||||
{factor.category}
|
||||
</span>
|
||||
<span
|
||||
@@ -52,12 +52,12 @@ export function RatingExplainedView({ data }: { data: unknown }) {
|
||||
{factor.intensity}
|
||||
</span>
|
||||
{factor.approximateTiming ? (
|
||||
<span className="text-xs text-slate-500">
|
||||
<span className="text-xs text-stone-500">
|
||||
~{factor.approximateTiming}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-slate-300">
|
||||
<p className="text-sm leading-relaxed text-stone-700">
|
||||
{factor.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function SensitivityGuideView({ data }: { data: unknown }) {
|
||||
const result = sensitivityGuideSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 text-sm text-slate-400">
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 text-sm text-stone-700">
|
||||
Could not display this insight (data format issue). Try another card or refresh later.
|
||||
</div>
|
||||
);
|
||||
@@ -22,8 +22,8 @@ export function SensitivityGuideView({ data }: { data: unknown }) {
|
||||
const parsed = result.data as SensitivityGuide;
|
||||
|
||||
return (
|
||||
<div className="space-y-5 rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<p className="text-slate-300">{parsed.summary}</p>
|
||||
<div className="space-y-5 rounded-2xl border border-stone-200 bg-white p-6">
|
||||
<p className="text-stone-700">{parsed.summary}</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{parsed.themes.map((theme, index) => (
|
||||
@@ -35,17 +35,17 @@ export function SensitivityGuideView({ data }: { data: unknown }) {
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium text-white">{theme.theme}</h4>
|
||||
<span className="text-xs capitalize text-slate-400">
|
||||
<h4 className="font-medium text-stone-950">{theme.theme}</h4>
|
||||
<span className="text-xs capitalize text-stone-600">
|
||||
{theme.severity}
|
||||
</span>
|
||||
{theme.approximateTiming ? (
|
||||
<span className="text-xs text-slate-500">
|
||||
<span className="text-xs text-stone-500">
|
||||
~{theme.approximateTiming}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-slate-300">
|
||||
<p className="text-sm leading-relaxed text-stone-700">
|
||||
{theme.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { skipGuideSchema, type SkipGuide } from "@/lib/schemas";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const recommendationStyles = {
|
||||
recommended: "bg-red-400/15 text-red-200 border-red-400/25",
|
||||
optional: "bg-amber-400/15 text-amber-200 border-amber-400/25",
|
||||
do_not_skip: "bg-slate-400/15 text-slate-200 border-slate-400/25",
|
||||
recommended: "bg-red-100 text-red-700 border-red-200",
|
||||
optional: "bg-amber-100 text-amber-700 border-amber-200",
|
||||
do_not_skip: "bg-stone-200 text-stone-700 border-stone-300",
|
||||
};
|
||||
|
||||
const recommendationLabels = {
|
||||
@@ -23,7 +23,7 @@ export function SkipGuideView({ data }: { data: unknown }) {
|
||||
const result = skipGuideSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 text-sm text-slate-400">
|
||||
<div className="rounded-2xl border border-stone-200 bg-white p-6 text-sm text-stone-700">
|
||||
Could not display this insight (data format issue). Try another card or refresh later.
|
||||
</div>
|
||||
);
|
||||
@@ -35,7 +35,7 @@ export function SkipGuideView({ data }: { data: unknown }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-5 rounded-2xl border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="space-y-5 rounded-2xl border border-stone-200 bg-white p-6">
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Stat label="Original rating" value={parsed.originalRating} />
|
||||
<Stat
|
||||
@@ -49,7 +49,7 @@ export function SkipGuideView({ data }: { data: unknown }) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-slate-400">Content timeline</p>
|
||||
<p className="mb-2 text-sm text-stone-600">Content timeline</p>
|
||||
<div className="relative h-4 overflow-hidden rounded-full bg-emerald-400/20">
|
||||
{parsed.segments.map((segment, index) => {
|
||||
const start = parseTimestamp(segment.start);
|
||||
@@ -63,7 +63,7 @@ export function SkipGuideView({ data }: { data: unknown }) {
|
||||
className={cn(
|
||||
"absolute top-0 h-full rounded-full",
|
||||
segment.skipRecommendation === "do_not_skip"
|
||||
? "bg-slate-500/70"
|
||||
? "bg-stone-300"
|
||||
: "bg-red-500/80",
|
||||
)}
|
||||
style={{ left: `${left}%`, width: `${width}%` }}
|
||||
@@ -72,7 +72,7 @@ export function SkipGuideView({ data }: { data: unknown }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 flex justify-between text-xs text-slate-500">
|
||||
<div className="mt-2 flex justify-between text-xs text-stone-500">
|
||||
<span>0:00</span>
|
||||
<span>{Math.floor(maxMinutes / 60)}h {maxMinutes % 60}m</span>
|
||||
</div>
|
||||
@@ -82,10 +82,10 @@ export function SkipGuideView({ data }: { data: unknown }) {
|
||||
{parsed.segments.map((segment, index) => (
|
||||
<div
|
||||
key={`${segment.start}-${segment.end}-${index}`}
|
||||
className="rounded-xl border border-white/10 bg-slate-950/40 p-4"
|
||||
className="rounded-xl border border-stone-200 bg-stone-100 p-4"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium text-white">
|
||||
<span className="font-medium text-stone-950">
|
||||
{segment.start} – {segment.end}
|
||||
</span>
|
||||
<span
|
||||
@@ -96,29 +96,29 @@ export function SkipGuideView({ data }: { data: unknown }) {
|
||||
>
|
||||
{recommendationLabels[segment.skipRecommendation]}
|
||||
</span>
|
||||
<span className="text-xs capitalize text-slate-500">
|
||||
<span className="text-xs capitalize text-stone-500">
|
||||
Plot impact: {segment.plotImpact.replace("_", " ")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-300">{segment.reason}</p>
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
<span className="text-slate-500">Resume hint:</span>{" "}
|
||||
<p className="text-sm text-stone-700">{segment.reason}</p>
|
||||
<p className="mt-2 text-sm text-stone-600">
|
||||
<span className="text-stone-500">Resume hint:</span>{" "}
|
||||
{segment.resumeHint}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500">{parsed.disclaimer}</p>
|
||||
<p className="text-xs text-stone-500">{parsed.disclaimer}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">{label}</p>
|
||||
<p className="mt-1 text-lg font-medium text-white">{value}</p>
|
||||
<div className="rounded-xl border border-stone-200 bg-stone-100 p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-stone-500">{label}</p>
|
||||
<p className="mt-1 text-lg font-medium text-stone-950">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Generated
-101
@@ -22,14 +22,12 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.9",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
@@ -2542,18 +2540,6 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
"pg-types": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
@@ -6641,40 +6627,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
|
||||
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -6746,49 +6698,6 @@
|
||||
"url": "https://github.com/sponsors/porsager"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -8496,16 +8405,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
+1
-3
@@ -7,7 +7,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"db:migrate": "tsx scripts/migrate.ts"
|
||||
"db:migrate": "node scripts/migrate.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -24,14 +24,12 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.9",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
grok --resume 019eb701-da15-75d1-9e5b-9f0e8cadafe2
|
||||
@@ -1,24 +0,0 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import postgres from "postgres";
|
||||
|
||||
async function migrate() {
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
throw new Error("DATABASE_URL is not set");
|
||||
}
|
||||
|
||||
const sql = postgres(connectionString, { max: 1 });
|
||||
const migrationPath = join(process.cwd(), "drizzle/migrations/0000_init.sql");
|
||||
const migration = readFileSync(migrationPath, "utf-8");
|
||||
|
||||
await sql.unsafe(migration);
|
||||
await sql.end();
|
||||
|
||||
console.log("Migrations applied successfully");
|
||||
}
|
||||
|
||||
migrate().catch((error) => {
|
||||
console.error("Migration failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user