Domain degisikligi: fristy.net -> fristy.dev

This commit is contained in:
fristy committed 2026-10-15 21:43:19 +03:00
1 parent ed149ded52
commit b98acd0cde
18 files changed
+165 -37

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ concurrency:
env: env:
REGISTRY: git.fristy.net REGISTRY: git.fristy.net
IMAGE_NAME: fristy/fristynet IMAGE_NAME: fristy/fristydev
jobs: jobs:
image: image:
+125
View File
@@ -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:** `// <source>` 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.
+5 -5
View File
@@ -1,10 +1,10 @@
# fristy.net # fristy.dev
![Code Health](scorecard.png) ![Code Health](scorecard.png)
My personal corner of the internet — projects, live Discord status, and ways to reach me. 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 ## 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: No need to build it yourself though — CI publishes every `main` push:
```bash ```bash
docker pull git.fristy.net/fristy/fristynet:latest docker pull git.fristy.net/fristy/fristydev:latest
``` ```
## CI/CD ## CI/CD
Pushes to `main` (and `<sha>` tags) run format check, lint and build first. If those pass, the Docker image is built and pushed to the Gitea registry: Pushes to `main` (and `<sha>` 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/fristydev:latest` on every `main` push
- `git.fristy.net/fristy/fristynet:<sha>` for every pushed commit - `git.fristy.net/fristy/fristydev:<sha>` for every pushed commit
Pull requests only run the checks — nothing is pushed. Pull requests only run the checks — nothing is pushed.
+3 -3
View File
@@ -13,7 +13,7 @@ function contextWith(id: string) {
describe("GET /api/discord-app/[id]", () => { describe("GET /api/discord-app/[id]", () => {
it("returns 400 for non-numeric ids", async () => { it("returns 400 for non-numeric ids", async () => {
const response = await GET( const response = await GET(
new Request("https://fristy.net/api/discord-app/abc"), new Request("https://fristy.dev/api/discord-app/abc"),
contextWith("abc"), contextWith("abc"),
); );
expect(response.status).toBe(400); expect(response.status).toBe(400);
@@ -25,7 +25,7 @@ describe("GET /api/discord-app/[id]", () => {
vi.fn(async () => ({ ok: false, status: 404 })), vi.fn(async () => ({ ok: false, status: 404 })),
); );
const response = await GET( const response = await GET(
new Request("https://fristy.net/api/discord-app/123"), new Request("https://fristy.dev/api/discord-app/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(404); expect(response.status).toBe(404);
@@ -39,7 +39,7 @@ describe("GET /api/discord-app/[id]", () => {
}), }),
); );
const response = await GET( const response = await GET(
new Request("https://fristy.net/api/discord-app/123"), new Request("https://fristy.dev/api/discord-app/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(502); expect(response.status).toBe(502);
+5 -5
View File
@@ -17,11 +17,11 @@ function requestWith(url: string) {
describe("GET /api/game/[id]", () => { describe("GET /api/game/[id]", () => {
it("returns 400 for non-numeric ids", async () => { it("returns 400 for non-numeric ids", async () => {
const response = await GET( const response = await GET(
requestWith("https://fristy.net/api/game/abc"), requestWith("https://fristy.dev/api/game/abc"),
contextWith("abc"), contextWith("abc"),
); );
expect(response.status).toBe(400); 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 () => { it("returns 404 when no profile resolves", async () => {
@@ -30,7 +30,7 @@ describe("GET /api/game/[id]", () => {
vi.fn(async () => ({ ok: false, status: 404 })), vi.fn(async () => ({ ok: false, status: 404 })),
); );
const response = await GET( const response = await GET(
requestWith("https://fristy.net/api/game/123"), requestWith("https://fristy.dev/api/game/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(404); expect(response.status).toBe(404);
@@ -44,10 +44,10 @@ describe("GET /api/game/[id]", () => {
}), }),
); );
const response = await GET( const response = await GET(
requestWith("https://fristy.net/api/game/123"), requestWith("https://fristy.dev/api/game/123"),
contextWith("123"), contextWith("123"),
); );
expect(response.status).toBe(502); 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");
}); });
}); });
+1 -1
View File
@@ -17,7 +17,7 @@ export async function generateMetadata(): Promise<Metadata> {
metadataBase: new URL(SITE_URL), metadataBase: new URL(SITE_URL),
title: { title: {
default: dict.meta.title, default: dict.meta.title,
template: "%s · fristy.net", template: "%s · fristy.dev",
}, },
description: dict.meta.description, description: dict.meta.description,
openGraph: { openGraph: {
+1 -1
View File
@@ -7,7 +7,7 @@ describe("SiteFooter", () => {
it("renders brand, rights, and source links", () => { it("renders brand, rights, and source links", () => {
const dict = getDictionary("en"); const dict = getDictionary("en");
const { container } = render(<SiteFooter dict={dict.footer} />); const { container } = render(<SiteFooter dict={dict.footer} />);
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); expect(container.textContent).toContain(dict.footer.rights);
const links = screen.getAllByRole("link"); const links = screen.getAllByRole("link");
expect(links.length).toBeGreaterThanOrEqual(3); expect(links.length).toBeGreaterThanOrEqual(3);
+1 -1
View File
@@ -28,7 +28,7 @@ export function SiteFooter({ dict }: { dict: Dictionary["footer"] }) {
<p> <p>
© {year}{" "} © {year}{" "}
<a href={SITE_URL} className="transition-colors hover:text-white"> <a href={SITE_URL} className="transition-colors hover:text-white">
fristy.net fristy.dev
</a>{" "} </a>{" "}
· {dict.rights} · {dict.rights}
</p> </p>
+1 -1
View File
@@ -7,7 +7,7 @@ export function SiteHeader({ dict, locale }: { dict: Dictionary["nav"]; locale:
<header className="sticky top-0 z-50 border-b border-white/5 bg-void/80 backdrop-blur"> <header className="sticky top-0 z-50 border-b border-white/5 bg-void/80 backdrop-blur">
<div className="mx-auto flex h-16 w-full max-w-4xl items-center justify-between px-5"> <div className="mx-auto flex h-16 w-full max-w-4xl items-center justify-between px-5">
<a href="#top" className="font-mono text-lg font-bold tracking-tight"> <a href="#top" className="font-mono text-lg font-bold tracking-tight">
fristy<span className="text-blurple">.net</span> fristy<span className="text-blurple">.dev</span>
</a> </a>
<nav className="hidden items-center gap-6 text-sm text-zinc-400 sm:flex"> <nav className="hidden items-center gap-6 text-sm text-zinc-400 sm:flex">
<a href="#discord" className="transition-colors hover:text-white"> <a href="#discord" className="transition-colors hover:text-white">
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
web: web:
image: git.fristy.net/fristy/fristynet:latest image: git.fristy.net/fristy/fristydev:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
TWITCH_CLIENT_ID: ${TWITCH_CLIENT_ID} TWITCH_CLIENT_ID: ${TWITCH_CLIENT_ID}
+3 -3
View File
@@ -29,7 +29,7 @@ describe("jsonErrorWithCors", () => {
const response = jsonErrorWithCors({ error: "Nope" }, 404); const response = jsonErrorWithCors({ error: "Nope" }, 404);
expect(response.status).toBe(404); expect(response.status).toBe(404);
expect(response.headers.get("Cache-Control")).toContain("s-maxage=60"); expect(response.headers.get("Cache-Control")).toContain("s-maxage=60");
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
await expect(response.json()).resolves.toEqual({ error: "Nope" }); await expect(response.json()).resolves.toEqual({ error: "Nope" });
}); });
}); });
@@ -37,7 +37,7 @@ describe("jsonWithCors", () => {
it("sets cache and CORS headers", async () => { it("sets cache and CORS headers", async () => {
const response = jsonWithCors({ ok: true }, 3600); const response = jsonWithCors({ ok: true }, 3600);
expect(response.headers.get("Cache-Control")).toContain("s-maxage=3600"); expect(response.headers.get("Cache-Control")).toContain("s-maxage=3600");
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
await expect(response.json()).resolves.toEqual({ ok: true }); await expect(response.json()).resolves.toEqual({ ok: true });
}); });
@@ -48,6 +48,6 @@ describe("jsonWithCors", () => {
it("falls back to production for untrusted origins", async () => { it("falls back to production for untrusted origins", async () => {
const response = jsonWithCors({ ok: true }, 3600, "https://evil.example"); const response = jsonWithCors({ ok: true }, 3600, "https://evil.example");
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
}); });
}); });
+1 -1
View File
@@ -57,7 +57,7 @@ export async function getDiscordAppInfo(applicationId: string): Promise<DiscordA
let response: Response; let response: Response;
try { try {
response = await fetch(`https://discord.com/api/v10/applications/${applicationId}/rpc`, { response = await fetch(`https://discord.com/api/v10/applications/${applicationId}/rpc`, {
headers: { "User-Agent": "fristy.net (personal site)" }, headers: { "User-Agent": "fristy.dev (personal site)" },
next: { revalidate: 86_400 }, next: { revalidate: 86_400 },
}); });
} catch (error) { } catch (error) {
+4 -1
View File
@@ -5,6 +5,8 @@ const GITEA_USER = "fristy";
const EXCLUDED_REPOS = new Set([".profile"]); const EXCLUDED_REPOS = new Set([".profile"]);
const MAX_REPOS = 8;
export interface GiteaRepo { export interface GiteaRepo {
id: number; id: number;
name: string; name: string;
@@ -56,7 +58,8 @@ export async function getRepos(): Promise<GiteaRepo[]> {
forks: repo.forks_count, forks: repo.forks_count,
updatedAt: repo.updated_at, updatedAt: repo.updated_at,
})) }))
.sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt)); .sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt))
.slice(0, MAX_REPOS);
} catch { } catch {
warnDev("cannot fetch gitea repos"); warnDev("cannot fetch gitea repos");
return []; return [];
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"title": "fristy.net — fristy love you", "title": "fristy.dev — fristy love you",
"description": "Fristys persönliche Ecke: Projekte, Live-Discord-Status und Kontakt." "description": "Fristys persönliche Ecke: Projekte, Live-Discord-Status und Kontakt."
}, },
"nav": { "nav": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"title": "fristy.net — fristy love you", "title": "fristy.dev — fristy love you",
"description": "Fristy's personal corner: projects, live Discord status and contact." "description": "Fristy's personal corner: projects, live Discord status and contact."
}, },
"nav": { "nav": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"title": "fristy.net - fristy love you", "title": "fristy.dev - fristy love you",
"description": "Fristy'nin kişisel köşesi: projeler, canlı Discord durumu ve iletişim." "description": "Fristy'nin kişisel köşesi: projeler, canlı Discord durumu ve iletişim."
}, },
"nav": { "nav": {
@@ -71,7 +71,7 @@
}, },
"projects": { "projects": {
"title": "Projeler", "title": "Projeler",
"subtitle": "Açık kaynak projelerim git.fristy.net üzerinde. Liste otomatik güncelleniyor.", "subtitle": "Açık kaynak projelerim git.fristy.net adresinde barınıyor. Liste her saat otomatik güncelleniyor.",
"updated": "Güncellendi", "updated": "Güncellendi",
"stars": "Yıldız", "stars": "Yıldız",
"forks": "Çatal", "forks": "Çatal",
+3 -3
View File
@@ -1,7 +1,7 @@
export const SITE_URL = "https://fristy.net"; export const SITE_URL = "https://fristy.dev";
export const CONTACT_EMAIL = "iletisim@fristy.net"; export const CONTACT_EMAIL = "iletisim@fristy.dev";
export const GITEA_PROFILE_URL = "https://git.fristy.net/fristy"; export const GITEA_PROFILE_URL = "https://git.fristy.net/fristy";
export const SOURCE_URL = "https://git.fristy.net/fristy/fristynet"; export const SOURCE_URL = "https://git.fristy.net/fristy/fristydev";
export const GITHUB_URL = "https://github.com/fristyxd"; export const GITHUB_URL = "https://github.com/fristyxd";
export const DISCORD_SERVER_URL = "https://discord.gg/fristy"; export const DISCORD_SERVER_URL = "https://discord.gg/fristy";
export const SPOTIFY_URL = "https://open.spotify.com/user/gayiw5j9i0rtvyra6gfzhpfwf"; export const SPOTIFY_URL = "https://open.spotify.com/user/gayiw5j9i0rtvyra6gfzhpfwf";
+6 -6
View File
@@ -9,9 +9,9 @@ afterEach(() => {
function apiRequest(path: string, init?: { method?: string; origin?: string | null }): NextRequest { function apiRequest(path: string, init?: { method?: string; origin?: string | null }): NextRequest {
const headers = new Headers(); const headers = new Headers();
if (init?.origin !== null) { if (init?.origin !== null) {
headers.set("origin", init?.origin ?? "https://fristy.net"); headers.set("origin", init?.origin ?? "https://fristy.dev");
} }
return new NextRequest(`https://fristy.net${path}`, { return new NextRequest(`https://fristy.dev${path}`, {
method: init?.method ?? "GET", method: init?.method ?? "GET",
headers, headers,
}); });
@@ -21,19 +21,19 @@ describe("proxy /api/*", () => {
it("answers OPTIONS preflight with CORS headers", () => { it("answers OPTIONS preflight with CORS headers", () => {
const response = proxy(apiRequest("/api/game/1", { method: "OPTIONS" })); const response = proxy(apiRequest("/api/game/1", { method: "OPTIONS" }));
expect(response.status).toBe(204); expect(response.status).toBe(204);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, OPTIONS"); expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, OPTIONS");
}); });
it("rejects foreign origins", () => { it("rejects foreign origins", () => {
const response = proxy(apiRequest("/api/game/1", { origin: "https://evil.test" })); const response = proxy(apiRequest("/api/game/1", { origin: "https://evil.test" }));
expect(response.status).toBe(403); expect(response.status).toBe(403);
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("passes same-origin API requests with CORS headers", () => { it("passes same-origin API requests with CORS headers", () => {
const response = proxy(apiRequest("/api/game/1")); const response = proxy(apiRequest("/api/game/1"));
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
}); });
}); });
@@ -57,7 +57,7 @@ describe("proxy rate limit", () => {
const limited = proxy(ipRequest("10.0.0.2")); const limited = proxy(ipRequest("10.0.0.2"));
expect(limited.status).toBe(429); expect(limited.status).toBe(429);
expect(limited.headers.get("Retry-After")).toBe("60"); expect(limited.headers.get("Retry-After")).toBe("60");
expect(limited.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.net"); expect(limited.headers.get("Access-Control-Allow-Origin")).toBe("https://fristy.dev");
await expect(limited.json()).resolves.toEqual({ error: "Too many requests" }); await expect(limited.json()).resolves.toEqual({ error: "Too many requests" });
}); });