diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 079da79..0f3b236 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -11,7 +11,7 @@ concurrency: env: REGISTRY: git.fristy.net - IMAGE_NAME: fristy/fristynet + IMAGE_NAME: fristy/fristydev jobs: image: diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ccd2778 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,125 @@ +--- +name: fristy.dev +description: Dark Discord-gaming personal hub. Single-column mono-type aesthetic, hairline cards, blurple accents. +colors: + void: "#060913" + panel: "#0C1222" + blurple: "#5865F2" + blurple-soft: "#8891FF" + mint: "#57F287" + ink: "#E8ECF6" + muted: "#A1A1AA" + faint: "#71717A" + aqua: "#7EE7D8" + sky: "#7CCBFF" + periwinkle: "#7F8CFF" + violet: "#B56DFF" + pink: "#F38AD8" +typography: + display: + fontFamily: Cascadia Mono + fontSize: 3.75rem + fontWeight: 900 + letterSpacing: -0.02em + h2: + fontFamily: Cascadia Mono + fontSize: 1.875rem + fontWeight: 700 + body: + fontFamily: Cascadia Mono + fontSize: 1rem + mono-sm: + fontFamily: Cascadia Mono + fontSize: 0.875rem + kicker: + fontFamily: Cascadia Mono + fontSize: 0.875rem +rounded: + card: 16px + image: 12px + pill: 9999px +spacing: + sm: 8px + md: 16px + lg: 24px + section: 80px +components: + page: + backgroundColor: "{colors.void}" + textColor: "{colors.ink}" + header: + backgroundColor: "{colors.void}" + textColor: "{colors.ink}" + hero-title: + typography: "{typography.display}" + card: + backgroundColor: "{colors.panel}" + textColor: "{colors.ink}" + rounded: "{rounded.card}" + button-primary: + backgroundColor: "{colors.blurple}" + textColor: "#FFFFFF" + rounded: "{rounded.pill}" + button-primary-hover: + backgroundColor: "{colors.blurple-soft}" + textColor: "{colors.void}" + link-accent: + textColor: "{colors.blurple-soft}" + status-live: + textColor: "{colors.mint}" +--- + +## Overview + +Midnight-terminal personal hub. One idea per screen: a sticky blurred header, a gradient hero, then stacked anchor sections (Discord, Projects, Contact) inside a narrow centered column. Everything is set in Cascadia Mono — the whole site reads like a friendly terminal, warmed up by Discord blurple and a five-stop pastel gradient. + +## Colors + +Near-black blue foundation, hairline borders (`white/10`), single accent family. + +- **Void (#060913):** Page background, fixed. Two faint radial glows sit on top (blurple top-right, mint top-left). +- **Panel (#0C1222):** Card fill for Discord, projects, contact, footer-adjacent surfaces. +- **Blurple (#5865F2):** Primary actions, active language, focus borders. Hover lightens to Blurple-soft. +- **Blurple-soft (#8891FF):** Kickers (`// lanyard`), links, mono metadata, gradient midpoint. +- **Mint (#57F287):** "Live" states only — never body text. +- **Ink (#E8ECF6) / Muted (#A1A1AA) / Faint (#71717A):** Text hierarchy. Body copy is always Muted on Void. +- **Hero gradient:** Aqua → Sky → Periwinkle → Violet → Pink, left to right, clipped to the `fristy` headline only. + +## Typography + +Cascadia Mono everywhere — display, body, labels, buttons. No second typeface. + +- **Display:** `fristy` hero and `404`, weight 900, tight tracking, gradient-clipped (hero) or solid (404). +- **H2:** Section titles, bold, tight. Always paired with a kicker above. +- **Kicker:** `// ` in Blurple-soft mono-sm (e.g. `// lanyard`, `// git.fristy.net`, `// iletisim@fristy.dev`). +- **Voice:** Sentence case, always — first letter capitalized, brand names capitalized (Discord, Gitea, Spotify). Never all-lowercase sentences. Turkish-first, mirrored in English and German. + +## Layout + +Single column, `max-w-4xl`, horizontal padding 20px, 80px section gaps. Sticky header (blur + hairline bottom border, 64px) with brand left, anchors center, language pill right. Sections carry `scroll-mt` so anchors land below the header. Footer is centered small-print. 404 reuses header + footer with a centered message block. Loading state is a pulse skeleton in the same column. + +## Elevation & Depth + +Flat surfaces, no drop shadows. Depth comes from layering: hairline `white/10` card borders, `white/5` inner rows, backdrop-blurred sticky header, and the two fixed background glows. Status is conveyed with color dots and pills, never shadows. + +## Shapes + +Cards are `rounded-2xl` (16px). Buttons, pills, badges and the language switcher are fully round. Avatars match card radius (large) with circular status dots overlapping the corner. Activity art is small rounded squares with circular overlay icons. Scrollbar is thin, thumb Blurple on hover. + +## Components + +- **Language switcher:** Pill group (TR/EN/DE), active option filled Blurple. Persists to a `locale` cookie via client write + `window.location.reload()` — no URL prefixes, ever. +- **Discord card:** Avatar (72px) + status dot, display name, `@username`, status pill (dot + localized status + `· Live`). Below: custom status, game rows (56px art + name/details/state/elapsed), Spotify row (48px cover), or a quiet-state line. 15s client polling over server-rendered initial data. +- **Project card:** Mono repo path, bold name (links to website if set, else repo), 3-line description, language dot + stars/forks/updated row, split footer links (website left, Gitea right). +- **Contact card:** 28px brand icon + label + mono handle, full-card link, arrow on hover. Email card swaps the arrow for mailto + copy button. +- **Section heading:** Kicker, H2, optional subtitle; optional right-aligned action (e.g. profile link). + +## Do's and Don'ts + +- Do keep every user-facing sentence capitalized and brand names exact. +- Do render data sections on the server first, enhance on the client (polling, clipboard, locale switch). +- Do use `next/image` with explicit sizes; add remote hosts to `next.config.ts` image patterns. +- Do keep Turkish, English and German dictionaries in sync — same keys, same structure. +- Don't add locale prefixes to paths (`/tr`, `/en` are banned by design). +- Don't use emojis in UI chrome; status color + typography carry meaning. +- Don't put code comments in source files; the design rationale lives here. diff --git a/README.md b/README.md index fcc8daf..50385a6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# fristy.net +# fristy.dev ![Code Health](scorecard.png) My personal corner of the internet — projects, live Discord status, and ways to reach me. -**Live:** https://fristy.net +**Live:** https://fristy.dev ## What's inside @@ -48,15 +48,15 @@ docker run --rm -p 3000:3000 fristynet No need to build it yourself though — CI publishes every `main` push: ```bash -docker pull git.fristy.net/fristy/fristynet:latest +docker pull git.fristy.net/fristy/fristydev:latest ``` ## CI/CD Pushes to `main` (and `` tags) run format check, lint and build first. If those pass, the Docker image is built and pushed to the Gitea registry: -- `git.fristy.net/fristy/fristynet:latest` on every `main` push -- `git.fristy.net/fristy/fristynet:` for every pushed commit +- `git.fristy.net/fristy/fristydev:latest` on every `main` push +- `git.fristy.net/fristy/fristydev:` for every pushed commit Pull requests only run the checks — nothing is pushed. diff --git a/app/api/discord-app/[id]/route.test.ts b/app/api/discord-app/[id]/route.test.ts index 9fa1f72..8ad5fa8 100644 --- a/app/api/discord-app/[id]/route.test.ts +++ b/app/api/discord-app/[id]/route.test.ts @@ -13,7 +13,7 @@ function contextWith(id: string) { describe("GET /api/discord-app/[id]", () => { it("returns 400 for non-numeric ids", async () => { const response = await GET( - new Request("https://fristy.net/api/discord-app/abc"), + new Request("https://fristy.dev/api/discord-app/abc"), contextWith("abc"), ); expect(response.status).toBe(400); @@ -25,7 +25,7 @@ describe("GET /api/discord-app/[id]", () => { vi.fn(async () => ({ ok: false, status: 404 })), ); const response = await GET( - new Request("https://fristy.net/api/discord-app/123"), + new Request("https://fristy.dev/api/discord-app/123"), contextWith("123"), ); expect(response.status).toBe(404); @@ -39,7 +39,7 @@ describe("GET /api/discord-app/[id]", () => { }), ); const response = await GET( - new Request("https://fristy.net/api/discord-app/123"), + new Request("https://fristy.dev/api/discord-app/123"), contextWith("123"), ); expect(response.status).toBe(502); diff --git a/app/api/game/[id]/route.test.ts b/app/api/game/[id]/route.test.ts index dfcd46f..053c753 100644 --- a/app/api/game/[id]/route.test.ts +++ b/app/api/game/[id]/route.test.ts @@ -17,11 +17,11 @@ function requestWith(url: string) { describe("GET /api/game/[id]", () => { it("returns 400 for non-numeric ids", async () => { const response = await GET( - requestWith("https://fristy.net/api/game/abc"), + requestWith("https://fristy.dev/api/game/abc"), contextWith("abc"), ); expect(response.status).toBe(400); - expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev"); }); it("returns 404 when no profile resolves", async () => { @@ -30,7 +30,7 @@ describe("GET /api/game/[id]", () => { vi.fn(async () => ({ ok: false, status: 404 })), ); const response = await GET( - requestWith("https://fristy.net/api/game/123"), + requestWith("https://fristy.dev/api/game/123"), contextWith("123"), ); expect(response.status).toBe(404); @@ -44,10 +44,10 @@ describe("GET /api/game/[id]", () => { }), ); const response = await GET( - requestWith("https://fristy.net/api/game/123"), + requestWith("https://fristy.dev/api/game/123"), contextWith("123"), ); expect(response.status).toBe(502); - expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev"); }); }); diff --git a/app/layout.tsx b/app/layout.tsx index d2f7779..b4471af 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -17,7 +17,7 @@ export async function generateMetadata(): Promise { metadataBase: new URL(SITE_URL), title: { default: dict.meta.title, - template: "%s · fristy.net", + template: "%s · fristy.dev", }, description: dict.meta.description, openGraph: { diff --git a/components/site-footer.test.tsx b/components/site-footer.test.tsx index 6dc241d..4f793e1 100644 --- a/components/site-footer.test.tsx +++ b/components/site-footer.test.tsx @@ -7,7 +7,7 @@ describe("SiteFooter", () => { it("renders brand, rights, and source links", () => { const dict = getDictionary("en"); const { container } = render(); - expect(container.querySelector('a[href="https://fristy.net"]')).not.toBeNull(); + expect(container.querySelector('a[href="https://fristy.dev"]')).not.toBeNull(); expect(container.textContent).toContain(dict.footer.rights); const links = screen.getAllByRole("link"); expect(links.length).toBeGreaterThanOrEqual(3); diff --git a/components/site-footer.tsx b/components/site-footer.tsx index d6d9730..9b7a764 100644 --- a/components/site-footer.tsx +++ b/components/site-footer.tsx @@ -28,7 +28,7 @@ export function SiteFooter({ dict }: { dict: Dictionary["footer"] }) {

© {year}{" "} - fristy.net + fristy.dev {" "} · {dict.rights}

diff --git a/components/site-header.tsx b/components/site-header.tsx index c906a79..a8d831f 100644 --- a/components/site-header.tsx +++ b/components/site-header.tsx @@ -7,7 +7,7 @@ export function SiteHeader({ dict, locale }: { dict: Dictionary["nav"]; locale:
- fristy.net + fristy.dev