fix(web): import globals.css directly instead of as a URL stylesheet link

This commit is contained in:
2026-03-19 10:55:01 -04:00
parent d94ec16fae
commit 320852b035
13 changed files with 73 additions and 43 deletions
+8 -1
View File
@@ -1,7 +1,14 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"sortImports": {
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
"groups": [
"builtin",
"external",
["internal", "subpath"],
["parent", "sibling", "index"],
"style",
"unknown"
],
"internalPattern": ["@/", "@sofa/"],
"newlinesBetween": true,
"order": "asc"
+3 -1
View File
@@ -18,7 +18,9 @@
"unicorn/consistent-function-scoping": "off",
"no-new": "off",
"no-await-in-loop": "off",
"react/no-array-index-key": "off"
"react/no-array-index-key": "off",
"react/jsx-no-useless-fragment": "error",
"react/rules-of-hooks": "error"
},
"ignorePatterns": ["node_modules", "dist", "build", "docs", "**/routeTree.gen.ts"]
}
+24 -6
View File
@@ -4,13 +4,17 @@
```bash
# Root commands (via Turborepo)
bun run dev # Start API server + Vite dev server
bun run dev # Start API, Vite dev, and Expo dev servers
bun run dev:docs # Start the fumadocs/Next.js dev server
bun run build # Production build (both apps)
bun run lint # Oxlint lint check
bun run format # Oxfmt format (auto-fix)
bun run check-types # TypeScript type check
bun run test # Run tests
bun run generate:openapi # Regenerate OpenAPI spec + docs API pages (run after contract/schema changes)
bun run i18n:extract # Run LingUI's string extraction
bun run i18n:compile # Run LingUI's typescript compilation
bun run i18n:claude # Prompt Claude Code to fill in untranslated strings
# Database commands (run from packages/db/)
cd packages/db && bun run db:push # Push schema changes to SQLite database
@@ -21,14 +25,15 @@ cd packages/db && bun run db:studio # Open Drizzle Studio (visual DB browser
**Use Bun, not Node.js**`bun <file>`, `bun test`, `bun install`, `bun run <script>`, `bunx <pkg>`. Bun auto-loads `.env`.
## Pre-commit checks
## CRITICAL: Pre-commit checks
All three must pass with zero warnings or errors:
Before declaring a task is complete, all four commands MUST pass with zero errors or warnings:
```bash
bun run lint
bun run format:check
bun run check-types
bun run test
bun test
```
## Architecture
@@ -53,9 +58,9 @@ couch-potato/
│ └── tmdb/ # @sofa/tmdb — TMDB API client + image URL helper (JIT)
├── .oxlintrc.json
├── .oxfmtrc.json
├── turbo.json
├── Dockerfile
── package.json
── package.json
└── turbo.json
```
All shared packages are JIT (raw TypeScript exports, no build step).
@@ -79,6 +84,7 @@ All shared packages are JIT (raw TypeScript exports, no build step).
- **Docs**: Fumadocs (Next.js), fumadocs-openapi for API reference
- **Linting**: Oxlint + Oxfmt (2-space indent, organized imports, Tailwind class sorting)
- **External API**: TMDB (The Movie Database)
- **Translation**: LingUI + Crowdin
### Package imports
@@ -109,6 +115,18 @@ Cross-package imports:
**Database IDs** — All app tables use UUIDv7 text PKs via `Bun.randomUUIDv7()`.
**i18n (LingUI)** — All user-facing strings are wrapped with LingUI macros. The `@sofa/i18n` shared package holds the i18n singleton, locale metadata, and `Intl`-based format utilities. Config lives at the repo root (`lingui.config.ts`). PO catalogs and compiled TS files live in `packages/i18n/src/po/`. Crowdin syncs via `crowdin.yml` with `languages_mapping` to map regional codes (es-ES→es, pt-PT→pt).
- **JSX text** → `<Trans>` from `@lingui/react/macro`
- **Strings in hooks/components** → `const { t } = useLingui()` from `@lingui/react/macro`, then `` t`string` ``
- **Strings outside React** (plain modules) → `import { msg } from "@lingui/core/macro"` + `import { i18n } from "@sofa/i18n"`, then `` i18n._(msg`string`) ``
- **Pluralization** → `import { plural } from "@lingui/core/macro"`, use inside `t`: `` t`${plural(count, { one: "# item", other: "# items" })}` ``
- **DO NOT use** `t(i18n)` — it's deprecated in v5 and removed in v6. Use `i18n._(msg`...`)` instead.
- **Date/number formatting** → use `formatDate`, `formatRelativeTime`, `formatNumber`, `formatBytes` from `@sofa/i18n/format` (Intl-based, locale-aware). Never use `date-fns` in app code.
- **Native Intl polyfills** → `@formatjs/intl-*` polyfills loaded in `apps/native/src/lib/intl-polyfills.ts` (strict dependency order, with locale data for all 6 languages).
- **Error messages** → Server throws `ORPCError` with `data: { code: AppErrorCode.XXX }`. Clients map codes to localized strings via per-app `error-messages.ts`. Never display `error.message` to users.
- After adding/changing strings, run `bun run i18n:extract` then `bun run i18n:compile`.
### Environment variables
Required: `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`.
+2 -1
View File
@@ -134,8 +134,9 @@ bun run dev
Useful commands:
- `bun run test`
- `bun test`
- `bun run lint`
- `bun run format`
- `bun run check-types`
- `cd packages/db && bun run db:generate`
- `cd packages/db && bun run db:migrate`
@@ -105,7 +105,7 @@ function LiveTimeAgo({
fallback?: string;
}) {
const text = useTimeAgo(date, { fallback });
return <>{text}</>;
return <span suppressHydrationWarning>{text}</span>;
}
/** Hydrates system health state and renders the 3 cards */
@@ -192,7 +192,7 @@ function SystemStatusCard({
<CardTitle>
<Trans>Health status</Trans>
</CardTitle>
<CardDescription suppressHydrationWarning>
<CardDescription>
<Trans>
Checked <LiveTimeAgo date={checkedAt} />
</Trans>
@@ -431,10 +431,7 @@ function BackgroundJobsCard({
<Tooltip>
<TooltipTrigger className="cursor-default">
<div className="flex items-baseline gap-1.5">
<span
className="text-muted-foreground/80 text-xs"
suppressHydrationWarning
>
<span className="text-muted-foreground/80 text-xs">
<LiveTimeAgo date={job.lastRunAt} />
</span>
{job.lastDurationMs !== null && job.lastDurationMs > 0 && (
@@ -463,10 +460,7 @@ function BackgroundJobsCard({
{job.nextRunAt ? (
<Tooltip>
<TooltipTrigger className="cursor-default">
<span
className="text-muted-foreground/80 text-xs"
suppressHydrationWarning
>
<span className="text-muted-foreground/80 text-xs">
<LiveTimeAgo date={job.nextRunAt} />
</span>
</TooltipTrigger>
@@ -595,7 +589,7 @@ function StorageCard({
)}
</div>
{backups.backupCount > 0 ? (
<p className="text-muted-foreground mt-1 text-xs" suppressHydrationWarning>
<p className="text-muted-foreground mt-1 text-xs">
<Trans>
{backups.backupCount} backups · last{" "}
<LiveTimeAgo date={backups.lastBackupAt} fallback={t`unknown`} />
@@ -1,4 +1,3 @@
import type { Hotkey } from "@tanstack/react-hotkeys";
import { useHotkey } from "@tanstack/react-hotkeys";
import { useAtomValue } from "jotai";
@@ -37,9 +36,11 @@ export function TitleKeyboardShortcuts() {
{ enabled },
);
for (const n of [1, 2, 3, 4, 5]) {
useHotkey(String(n) as Hotkey, () => handleRating(n), { enabled });
}
useHotkey("1", () => handleRating(1), { enabled });
useHotkey("2", () => handleRating(2), { enabled });
useHotkey("3", () => handleRating(3), { enabled });
useHotkey("4", () => handleRating(4), { enabled });
useHotkey("5", () => handleRating(5), { enabled });
return null;
}
+2 -1
View File
@@ -10,7 +10,8 @@ import { routeTree } from "./routeTree.gen";
const router = createRouter({
routeTree,
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
defaultPreloadStaleTime: 60_000,
defaultPendingMs: 0,
scrollRestoration: true,
context: { orpc, queryClient },
Wrap: function WrapComponent({ children }: { children: React.ReactNode }) {
+2 -2
View File
@@ -15,9 +15,10 @@ import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { initLocale } from "@/lib/i18n";
import type { orpc } from "@/lib/orpc/client";
import globalsCss from "@/styles/globals.css?url";
import { i18n } from "@sofa/i18n";
import "@/styles/globals.css";
export const Route = createRootRouteWithContext<{
orpc: typeof orpc;
queryClient: QueryClient;
@@ -37,7 +38,6 @@ export const Route = createRootRouteWithContext<{
{ name: "description", content: "Track your movies and TV shows" },
],
links: [
{ rel: "stylesheet", href: globalsCss },
{ rel: "manifest", href: "/manifest.webmanifest" },
{ rel: "apple-touch-icon", href: "/apple-icon.png" },
],
+7 -5
View File
@@ -11,19 +11,21 @@ import { Skeleton } from "@/components/ui/skeleton";
import { orpc } from "@/lib/orpc/client";
export const Route = createFileRoute("/_app/explore")({
loader: ({ context }) => {
loader: async ({ context }) => {
await Promise.all([
context.queryClient.ensureQueryData(
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
);
),
context.queryClient.ensureQueryData(
orpc.explore.popular.queryOptions({ input: { type: "tv" } }),
);
),
context.queryClient.ensureQueryData(
orpc.explore.genres.queryOptions({ input: { type: "movie" } }),
);
),
context.queryClient.ensureQueryData(
orpc.explore.genres.queryOptions({ input: { type: "tv" } }),
);
),
]);
},
pendingComponent: ExploreSkeletons,
component: ExplorePage,
+4 -2
View File
@@ -6,9 +6,11 @@ import { client } from "@/lib/orpc/client";
export const Route = createFileRoute("/")({
beforeLoad: async () => {
const { data: session } = await authClient.getSession();
const [{ data: session }, info] = await Promise.all([
authClient.getSession(),
client.system.publicInfo({}),
]);
if (session?.user) throw redirect({ to: "/dashboard" });
const info = await client.system.publicInfo({});
if (!info.tmdbConfigured) throw redirect({ to: "/setup" });
return { info };
},
+1
View File
@@ -41,6 +41,7 @@
"build": "turbo run build",
"lint": "turbo run lint",
"format": "turbo run format",
"format:check": "turbo run format:check",
"check-types": "turbo run check-types",
"test": "turbo run test",
"generate:openapi": "bun scripts/generate-openapi-spec.ts && cd docs && bun run generate:api-docs",
-1
View File
@@ -13,7 +13,6 @@ import { createLogger } from "@sofa/logger";
import { getOrFetchTitleByTmdbId } from "../metadata";
import { logEpisodeWatch, logMovieWatch, rateTitleStars, setTitleStatus } from "../tracking";
import type { ImportEpisode, ImportMovie, ImportRating, ImportWatchlistItem } from "./parsers";
import { resolveMovieTmdbId, resolveShowTmdbId } from "./resolve";
+2
View File
@@ -6,6 +6,8 @@ process.env.LOG_LEVEL ??= "error";
mock.module("@sofa/db/client", () => ({
db: testDb,
optimizeDatabase: () => {},
vacuumDatabase: () => {},
closeDatabase: () => {},
}));