mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
fix(web): import globals.css directly instead of as a URL stylesheet link
This commit is contained in:
+8
-1
@@ -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
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ process.env.LOG_LEVEL ??= "error";
|
||||
|
||||
mock.module("@sofa/db/client", () => ({
|
||||
db: testDb,
|
||||
optimizeDatabase: () => {},
|
||||
vacuumDatabase: () => {},
|
||||
closeDatabase: () => {},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user