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",
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
"sortImports": {
|
"sortImports": {
|
||||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
|
"groups": [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
["internal", "subpath"],
|
||||||
|
["parent", "sibling", "index"],
|
||||||
|
"style",
|
||||||
|
"unknown"
|
||||||
|
],
|
||||||
"internalPattern": ["@/", "@sofa/"],
|
"internalPattern": ["@/", "@sofa/"],
|
||||||
"newlinesBetween": true,
|
"newlinesBetween": true,
|
||||||
"order": "asc"
|
"order": "asc"
|
||||||
|
|||||||
+3
-1
@@ -18,7 +18,9 @@
|
|||||||
"unicorn/consistent-function-scoping": "off",
|
"unicorn/consistent-function-scoping": "off",
|
||||||
"no-new": "off",
|
"no-new": "off",
|
||||||
"no-await-in-loop": "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"]
|
"ignorePatterns": ["node_modules", "dist", "build", "docs", "**/routeTree.gen.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,17 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Root commands (via Turborepo)
|
# 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 build # Production build (both apps)
|
||||||
bun run lint # Oxlint lint check
|
bun run lint # Oxlint lint check
|
||||||
bun run format # Oxfmt format (auto-fix)
|
bun run format # Oxfmt format (auto-fix)
|
||||||
bun run check-types # TypeScript type check
|
bun run check-types # TypeScript type check
|
||||||
bun run test # Run tests
|
bun run test # Run tests
|
||||||
bun run generate:openapi # Regenerate OpenAPI spec + docs API pages (run after contract/schema changes)
|
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/)
|
# Database commands (run from packages/db/)
|
||||||
cd packages/db && bun run db:push # Push schema changes to SQLite database
|
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`.
|
**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
|
```bash
|
||||||
bun run lint
|
bun run lint
|
||||||
|
bun run format:check
|
||||||
bun run check-types
|
bun run check-types
|
||||||
bun run test
|
bun test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -53,9 +58,9 @@ couch-potato/
|
|||||||
│ └── tmdb/ # @sofa/tmdb — TMDB API client + image URL helper (JIT)
|
│ └── tmdb/ # @sofa/tmdb — TMDB API client + image URL helper (JIT)
|
||||||
├── .oxlintrc.json
|
├── .oxlintrc.json
|
||||||
├── .oxfmtrc.json
|
├── .oxfmtrc.json
|
||||||
├── turbo.json
|
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
└── package.json
|
├── package.json
|
||||||
|
└── turbo.json
|
||||||
```
|
```
|
||||||
|
|
||||||
All shared packages are JIT (raw TypeScript exports, no build step).
|
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
|
- **Docs**: Fumadocs (Next.js), fumadocs-openapi for API reference
|
||||||
- **Linting**: Oxlint + Oxfmt (2-space indent, organized imports, Tailwind class sorting)
|
- **Linting**: Oxlint + Oxfmt (2-space indent, organized imports, Tailwind class sorting)
|
||||||
- **External API**: TMDB (The Movie Database)
|
- **External API**: TMDB (The Movie Database)
|
||||||
|
- **Translation**: LingUI + Crowdin
|
||||||
|
|
||||||
### Package imports
|
### Package imports
|
||||||
|
|
||||||
@@ -109,6 +115,18 @@ Cross-package imports:
|
|||||||
|
|
||||||
**Database IDs** — All app tables use UUIDv7 text PKs via `Bun.randomUUIDv7()`.
|
**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
|
### Environment variables
|
||||||
|
|
||||||
Required: `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`.
|
Required: `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`.
|
||||||
|
|||||||
@@ -134,8 +134,9 @@ bun run dev
|
|||||||
|
|
||||||
Useful commands:
|
Useful commands:
|
||||||
|
|
||||||
- `bun run test`
|
- `bun test`
|
||||||
- `bun run lint`
|
- `bun run lint`
|
||||||
|
- `bun run format`
|
||||||
- `bun run check-types`
|
- `bun run check-types`
|
||||||
- `cd packages/db && bun run db:generate`
|
- `cd packages/db && bun run db:generate`
|
||||||
- `cd packages/db && bun run db:migrate`
|
- `cd packages/db && bun run db:migrate`
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ function LiveTimeAgo({
|
|||||||
fallback?: string;
|
fallback?: string;
|
||||||
}) {
|
}) {
|
||||||
const text = useTimeAgo(date, { fallback });
|
const text = useTimeAgo(date, { fallback });
|
||||||
return <>{text}</>;
|
return <span suppressHydrationWarning>{text}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hydrates system health state and renders the 3 cards */
|
/** Hydrates system health state and renders the 3 cards */
|
||||||
@@ -192,7 +192,7 @@ function SystemStatusCard({
|
|||||||
<CardTitle>
|
<CardTitle>
|
||||||
<Trans>Health status</Trans>
|
<Trans>Health status</Trans>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription suppressHydrationWarning>
|
<CardDescription>
|
||||||
<Trans>
|
<Trans>
|
||||||
Checked <LiveTimeAgo date={checkedAt} />
|
Checked <LiveTimeAgo date={checkedAt} />
|
||||||
</Trans>
|
</Trans>
|
||||||
@@ -431,10 +431,7 @@ function BackgroundJobsCard({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="cursor-default">
|
<TooltipTrigger className="cursor-default">
|
||||||
<div className="flex items-baseline gap-1.5">
|
<div className="flex items-baseline gap-1.5">
|
||||||
<span
|
<span className="text-muted-foreground/80 text-xs">
|
||||||
className="text-muted-foreground/80 text-xs"
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<LiveTimeAgo date={job.lastRunAt} />
|
<LiveTimeAgo date={job.lastRunAt} />
|
||||||
</span>
|
</span>
|
||||||
{job.lastDurationMs !== null && job.lastDurationMs > 0 && (
|
{job.lastDurationMs !== null && job.lastDurationMs > 0 && (
|
||||||
@@ -463,10 +460,7 @@ function BackgroundJobsCard({
|
|||||||
{job.nextRunAt ? (
|
{job.nextRunAt ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="cursor-default">
|
<TooltipTrigger className="cursor-default">
|
||||||
<span
|
<span className="text-muted-foreground/80 text-xs">
|
||||||
className="text-muted-foreground/80 text-xs"
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<LiveTimeAgo date={job.nextRunAt} />
|
<LiveTimeAgo date={job.nextRunAt} />
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -595,7 +589,7 @@ function StorageCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{backups.backupCount > 0 ? (
|
{backups.backupCount > 0 ? (
|
||||||
<p className="text-muted-foreground mt-1 text-xs" suppressHydrationWarning>
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
<Trans>
|
<Trans>
|
||||||
{backups.backupCount} backups · last{" "}
|
{backups.backupCount} backups · last{" "}
|
||||||
<LiveTimeAgo date={backups.lastBackupAt} fallback={t`unknown`} />
|
<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 { useHotkey } from "@tanstack/react-hotkeys";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
|
|
||||||
@@ -37,9 +36,11 @@ export function TitleKeyboardShortcuts() {
|
|||||||
{ enabled },
|
{ enabled },
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const n of [1, 2, 3, 4, 5]) {
|
useHotkey("1", () => handleRating(1), { enabled });
|
||||||
useHotkey(String(n) as Hotkey, () => handleRating(n), { enabled });
|
useHotkey("2", () => handleRating(2), { enabled });
|
||||||
}
|
useHotkey("3", () => handleRating(3), { enabled });
|
||||||
|
useHotkey("4", () => handleRating(4), { enabled });
|
||||||
|
useHotkey("5", () => handleRating(5), { enabled });
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { routeTree } from "./routeTree.gen";
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
defaultPreloadStaleTime: 0,
|
defaultPreloadStaleTime: 60_000,
|
||||||
|
defaultPendingMs: 0,
|
||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
context: { orpc, queryClient },
|
context: { orpc, queryClient },
|
||||||
Wrap: function WrapComponent({ children }: { children: React.ReactNode }) {
|
Wrap: function WrapComponent({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import { Toaster } from "@/components/ui/sonner";
|
|||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { initLocale } from "@/lib/i18n";
|
import { initLocale } from "@/lib/i18n";
|
||||||
import type { orpc } from "@/lib/orpc/client";
|
import type { orpc } from "@/lib/orpc/client";
|
||||||
import globalsCss from "@/styles/globals.css?url";
|
|
||||||
import { i18n } from "@sofa/i18n";
|
import { i18n } from "@sofa/i18n";
|
||||||
|
|
||||||
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{
|
export const Route = createRootRouteWithContext<{
|
||||||
orpc: typeof orpc;
|
orpc: typeof orpc;
|
||||||
queryClient: QueryClient;
|
queryClient: QueryClient;
|
||||||
@@ -37,7 +38,6 @@ export const Route = createRootRouteWithContext<{
|
|||||||
{ name: "description", content: "Track your movies and TV shows" },
|
{ name: "description", content: "Track your movies and TV shows" },
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
{ rel: "stylesheet", href: globalsCss },
|
|
||||||
{ rel: "manifest", href: "/manifest.webmanifest" },
|
{ rel: "manifest", href: "/manifest.webmanifest" },
|
||||||
{ rel: "apple-touch-icon", href: "/apple-icon.png" },
|
{ rel: "apple-touch-icon", href: "/apple-icon.png" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,19 +11,21 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { orpc } from "@/lib/orpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_app/explore")({
|
export const Route = createFileRoute("/_app/explore")({
|
||||||
loader: ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
context.queryClient.ensureQueryData(
|
await Promise.all([
|
||||||
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
|
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.popular.queryOptions({ input: { type: "tv" } }),
|
||||||
context.queryClient.ensureQueryData(
|
),
|
||||||
orpc.explore.genres.queryOptions({ input: { type: "movie" } }),
|
context.queryClient.ensureQueryData(
|
||||||
);
|
orpc.explore.genres.queryOptions({ input: { type: "movie" } }),
|
||||||
context.queryClient.ensureQueryData(
|
),
|
||||||
orpc.explore.genres.queryOptions({ input: { type: "tv" } }),
|
context.queryClient.ensureQueryData(
|
||||||
);
|
orpc.explore.genres.queryOptions({ input: { type: "tv" } }),
|
||||||
|
),
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
pendingComponent: ExploreSkeletons,
|
pendingComponent: ExploreSkeletons,
|
||||||
component: ExplorePage,
|
component: ExplorePage,
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { client } from "@/lib/orpc/client";
|
|||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
beforeLoad: async () => {
|
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" });
|
if (session?.user) throw redirect({ to: "/dashboard" });
|
||||||
const info = await client.system.publicInfo({});
|
|
||||||
if (!info.tmdbConfigured) throw redirect({ to: "/setup" });
|
if (!info.tmdbConfigured) throw redirect({ to: "/setup" });
|
||||||
return { info };
|
return { info };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"format": "turbo run format",
|
"format": "turbo run format",
|
||||||
|
"format:check": "turbo run format:check",
|
||||||
"check-types": "turbo run check-types",
|
"check-types": "turbo run check-types",
|
||||||
"test": "turbo run test",
|
"test": "turbo run test",
|
||||||
"generate:openapi": "bun scripts/generate-openapi-spec.ts && cd docs && bun run generate:api-docs",
|
"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 { getOrFetchTitleByTmdbId } from "../metadata";
|
||||||
import { logEpisodeWatch, logMovieWatch, rateTitleStars, setTitleStatus } from "../tracking";
|
import { logEpisodeWatch, logMovieWatch, rateTitleStars, setTitleStatus } from "../tracking";
|
||||||
|
|
||||||
import type { ImportEpisode, ImportMovie, ImportRating, ImportWatchlistItem } from "./parsers";
|
import type { ImportEpisode, ImportMovie, ImportRating, ImportWatchlistItem } from "./parsers";
|
||||||
import { resolveMovieTmdbId, resolveShowTmdbId } from "./resolve";
|
import { resolveMovieTmdbId, resolveShowTmdbId } from "./resolve";
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ process.env.LOG_LEVEL ??= "error";
|
|||||||
|
|
||||||
mock.module("@sofa/db/client", () => ({
|
mock.module("@sofa/db/client", () => ({
|
||||||
db: testDb,
|
db: testDb,
|
||||||
|
optimizeDatabase: () => {},
|
||||||
|
vacuumDatabase: () => {},
|
||||||
closeDatabase: () => {},
|
closeDatabase: () => {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user