Compare commits
7
Commits
864b9226fc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff18fc353d
|
||
|
|
0c30c18e24 | ||
|
|
ed149ded52
|
||
|
|
7fa3a94278
|
||
|
|
5837e77be8 | ||
|
|
0504baf77b
|
||
|
|
62b1aa9e33
|
No files matched your search
@@ -0,0 +1,61 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Lint, Test & Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
- name: Get pnpm store path
|
||||||
|
id: pnpm-store
|
||||||
|
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-store.outputs.path }}
|
||||||
|
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Check formatting
|
||||||
|
run: pnpm format:check
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
|
- name: Test
|
||||||
|
run: pnpm test
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
health:
|
||||||
|
name: Code Health
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Scan with desloppify (CI profile, objective only)
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
pipx run --spec desloppify desloppify scan --profile ci --path . --no-badge
|
||||||
|
- name: Upload scorecard
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: desloppify-scorecard
|
||||||
|
path: scorecard.png
|
||||||
|
if-no-files-found: ignore
|
||||||
@@ -1,51 +1,21 @@
|
|||||||
name: Build & Push
|
name: Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
tags: ["v*"]
|
tags: ["v*"]
|
||||||
pull_request:
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.fristy.net
|
REGISTRY: git.fristy.net
|
||||||
IMAGE_NAME: fristy/fristynet
|
IMAGE_NAME: fristy/fristydev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
|
||||||
name: Lint & Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
- name: Get pnpm store path
|
|
||||||
id: pnpm-store
|
|
||||||
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Cache pnpm store
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pnpm-store.outputs.path }}
|
|
||||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
- name: Check formatting
|
|
||||||
run: pnpm format:check
|
|
||||||
- name: Lint
|
|
||||||
run: pnpm lint
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
name: Build & Push Image
|
name: Build & Push Image
|
||||||
needs: check
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -79,7 +49,7 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
name: Trigger Deploy
|
name: Trigger Deploy
|
||||||
needs: image
|
needs: image
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger Deploy
|
- name: Trigger Deploy
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# fristy.net
|
# fristy.dev
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export async function GET(
|
|||||||
try {
|
try {
|
||||||
info = await getDiscordAppInfo(id);
|
info = await getDiscordAppInfo(id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
warnDev(`discord-app upstream failed: ${id} -> ${error instanceof Error ? error.message : error}`);
|
warnDev(
|
||||||
|
`discord-app upstream failed: ${id} -> ${error instanceof Error ? error.message : error}`,
|
||||||
|
);
|
||||||
return jsonErrorWithCors({ error: "Upstream unavailable" }, 502, 60, origin);
|
return jsonErrorWithCors({ error: "Upstream unavailable" }, 502, 60, origin);
|
||||||
}
|
}
|
||||||
if (!info) {
|
if (!info) {
|
||||||
|
|||||||
@@ -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
@@ -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: {
|
||||||
|
|||||||
@@ -285,7 +285,9 @@ export function GameProfileModal({
|
|||||||
</dl>
|
</dl>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{profile ? <ModalReviews profile={profile} dict={dict} localeTag={localeTag} /> : null}
|
{profile ? (
|
||||||
|
<ModalReviews profile={profile} dict={dict} localeTag={localeTag} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
{profile?.itad && (bestPrice || historyLow) ? (
|
{profile?.itad && (bestPrice || historyLow) ? (
|
||||||
<ModalPrices
|
<ModalPrices
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ describe("ModalReviews", () => {
|
|||||||
it("renders nothing without review sources", () => {
|
it("renders nothing without review sources", () => {
|
||||||
const dict = getDictionary("en");
|
const dict = getDictionary("en");
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ModalReviews profile={profileFixture()} dict={dict.discord.gameModal} localeTag="en-US" />,
|
<ModalReviews
|
||||||
|
profile={profileFixture()}
|
||||||
|
dict={dict.discord.gameModal}
|
||||||
|
localeTag="en-US"
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.textContent).toBe("");
|
expect(container.textContent).toBe("");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+2
-9
@@ -22,10 +22,7 @@ export function isAllowedOrigin(request: Request): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveAllowedOrigin(origin: string | null): string {
|
function resolveAllowedOrigin(origin: string | null): string {
|
||||||
if (
|
if (origin !== null && (origin === ALLOWED_ORIGIN || (isDev() && DEV_ORIGINS.has(origin)))) {
|
||||||
origin !== null &&
|
|
||||||
(origin === ALLOWED_ORIGIN || (isDev() && DEV_ORIGINS.has(origin)))
|
|
||||||
) {
|
|
||||||
return origin;
|
return origin;
|
||||||
}
|
}
|
||||||
return ALLOWED_ORIGIN;
|
return ALLOWED_ORIGIN;
|
||||||
@@ -79,11 +76,7 @@ export function jsonErrorWithCors(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export function jsonWithCors(
|
export function jsonWithCors(data: unknown, sMaxage = 3600, origin?: string | null): NextResponse {
|
||||||
data: unknown,
|
|
||||||
sMaxage = 3600,
|
|
||||||
origin?: string | null,
|
|
||||||
): NextResponse {
|
|
||||||
return NextResponse.json(data, {
|
return NextResponse.json(data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": `public, s-maxage=${sMaxage}, stale-while-revalidate=86400`,
|
"Cache-Control": `public, s-maxage=${sMaxage}, stale-while-revalidate=86400`,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ export function formatReleaseDate(value: string, localeTag: string): string {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCount(value: number | null | undefined, localeTag: string, unit: string): string {
|
export function formatCount(
|
||||||
|
value: number | null | undefined,
|
||||||
|
localeTag: string,
|
||||||
|
unit: string,
|
||||||
|
): string {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { buildLinks, mergePeople, mergePlatforms, selectScreenshots } from "@/lib/game/game-profile";
|
import {
|
||||||
|
buildLinks,
|
||||||
|
mergePeople,
|
||||||
|
mergePlatforms,
|
||||||
|
selectScreenshots,
|
||||||
|
} from "@/lib/game/game-profile";
|
||||||
import type { IgdbGame } from "@/lib/game/game-profile-types";
|
import type { IgdbGame } from "@/lib/game/game-profile-types";
|
||||||
|
|
||||||
describe("mergePeople", () => {
|
describe("mergePeople", () => {
|
||||||
|
|||||||
@@ -186,7 +186,9 @@ export function toPricePoint(p: ItadOverviewPrice["current"]): GamePricePoint |
|
|||||||
|
|
||||||
export function mapItadBlock(
|
export function mapItadBlock(
|
||||||
itadInfo: ItadGameInfo | null,
|
itadInfo: ItadGameInfo | null,
|
||||||
priceEntry: { current?: ItadOverviewPrice["current"]; lowest?: ItadOverviewPrice["current"] } | undefined,
|
priceEntry:
|
||||||
|
| { current?: ItadOverviewPrice["current"]; lowest?: ItadOverviewPrice["current"] }
|
||||||
|
| undefined,
|
||||||
): NonNullable<GameProfile["itad"]> | null {
|
): NonNullable<GameProfile["itad"]> | null {
|
||||||
if (!process.env.ITAD_API_KEY || (!itadInfo && !priceEntry)) {
|
if (!process.env.ITAD_API_KEY || (!itadInfo && !priceEntry)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -197,10 +199,7 @@ export function mapItadBlock(
|
|||||||
pageUrl: itadInfo?.urls?.game ?? null,
|
pageUrl: itadInfo?.urls?.game ?? null,
|
||||||
playerCount: itadInfo?.players?.recent ?? null,
|
playerCount: itadInfo?.players?.recent ?? null,
|
||||||
reviews: (itadInfo?.reviews ?? [])
|
reviews: (itadInfo?.reviews ?? [])
|
||||||
.filter(
|
.filter((r): r is ItadReview & { source: string; url: string } => !!r.source && !!r.url)
|
||||||
(r): r is ItadReview & { source: string; url: string } =>
|
|
||||||
!!r.source && !!r.url,
|
|
||||||
)
|
|
||||||
.map((r) => ({
|
.map((r) => ({
|
||||||
score: r.score ?? null,
|
score: r.score ?? null,
|
||||||
source: r.source,
|
source: r.source,
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ describe("activityImageUrl", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("resolves Spotify keys", () => {
|
it("resolves Spotify keys", () => {
|
||||||
expect(activityImageUrl(activity({ assets: { large_image: "spotify:img" } }), "large")).toBe(
|
expect(
|
||||||
"https://i.scdn.co/image/img",
|
activityImageUrl(activity({ assets: { large_image: "spotify:img" } }), "large"),
|
||||||
);
|
).toBe("https://i.scdn.co/image/img");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves app assets with application id", () => {
|
it("resolves app assets with application id", () => {
|
||||||
|
|||||||
+4
-1
@@ -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,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,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": {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user