Skip to content

Commit a9efffa

Browse files
Refactor to CRT 3D portfolio
1 parent 9ccf851 commit a9efffa

23 files changed

Lines changed: 2032 additions & 718 deletions

README.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Kinin Code
22

3-
Portfolio + full-stack landing site built with Next.js and an optional 3D model.
3+
edh.dev-inspired portfolio with a 3D retro computer + CRT terminal hero and scroll transition into 2D sections.
44

55
## Development
66

@@ -21,16 +21,60 @@ PowerShell starter:
2121

2222
This opens `http://localhost:3000/?dev=1` and enables the live dev panel.
2323

24-
## 3D Model
24+
## Content (single source of truth)
2525

26-
Export the Blender model to GLB and place it here:
26+
Edit these files:
27+
28+
```
29+
src/content/profile.ts
30+
src/content/projects.ts
31+
src/content/theme.ts
32+
src/content/pages/about.md
33+
src/content/pages/contact.md
34+
src/content/pages/title.md
35+
```
36+
37+
Validate content:
38+
39+
```bash
40+
npm run validate-content
41+
```
42+
43+
## 3D Model (GLB)
44+
45+
Place the model here:
2746

2847
```
2948
public/models/computer.glb
3049
```
3150

3251
If the model is missing, the hero uses a placeholder mesh.
3352

53+
### Screen mesh requirement
54+
55+
For the CRT terminal texture to map correctly, the screen mesh **must** be a separate object and named:
56+
57+
- `Screen`, `Display`, or `Monitor` (case-insensitive)
58+
59+
If no screen mesh is found, a fallback plane is attached and the dev panel shows:
60+
61+
```
62+
screen mesh not found, using fallback plane
63+
```
64+
65+
### Debug (`?dev=1`)
66+
67+
The dev panel lists:
68+
- All mesh names in the GLB
69+
- The selected screen mesh name
70+
- Whether fallback plane is used
71+
72+
## Terminal commands
73+
74+
```
75+
help, ls, cd, pwd, show <file.md>, show -all, echo <text>, hello, mkdir <name>, touch <name>
76+
```
77+
3478
## Deploy (Cloud Run + Cloudflare)
3579

3680
See `docs/deploy.md` for setup steps and GitHub Actions secrets.

package-lock.json

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@
77
"build": "next build",
88
"start": "next start",
99
"lint": "eslint",
10-
"test": "vitest run"
10+
"test": "vitest run",
11+
"validate-content": "tsx scripts/validate-content.ts"
1112
},
1213
"dependencies": {
1314
"@react-three/drei": "^10.7.7",
1415
"@react-three/fiber": "^9.5.0",
1516
"next": "16.1.5",
1617
"react": "19.2.3",
1718
"react-dom": "19.2.3",
18-
"three": "^0.182.0"
19+
"three": "^0.182.0",
20+
"zod": "^4.3.6"
1921
},
2022
"devDependencies": {
2123
"@types/node": "^20",
2224
"@types/react": "^19",
2325
"@types/react-dom": "^19",
2426
"eslint": "^9",
2527
"eslint-config-next": "16.1.5",
28+
"tsx": "^4.21.0",
2629
"typescript": "^5",
2730
"vitest": "^4.0.18"
2831
}

scripts/validate-content.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { readFile } from "node:fs/promises";
2+
import { contentSchema } from "../src/content/schema";
3+
import { profile } from "../src/content/profile";
4+
import { projects } from "../src/content/projects";
5+
import { theme } from "../src/content/theme";
6+
7+
const pages = ["about", "contact", "title"];
8+
9+
const run = async () => {
10+
const result = contentSchema.safeParse({ profile, projects, theme });
11+
12+
if (!result.success) {
13+
console.error("Content schema validation failed:");
14+
console.error(result.error.format());
15+
process.exit(1);
16+
}
17+
18+
for (const page of pages) {
19+
const path = new URL(`../src/content/pages/${page}.md`, import.meta.url);
20+
const text = await readFile(path, "utf8");
21+
if (!text.trim()) {
22+
console.error(`Page ${page}.md is empty.`);
23+
process.exit(1);
24+
}
25+
}
26+
27+
console.log("Content validated successfully.");
28+
};
29+
30+
run().catch((error) => {
31+
console.error(error);
32+
process.exit(1);
33+
});

src/app/about/page.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,22 @@
11
import type { Metadata } from "next";
22
import Link from "next/link";
3-
import { cookies } from "next/headers";
4-
import { copy } from "@/lib/i18n";
3+
import Markdown from "@/components/Markdown";
4+
import { getContent } from "@/lib/content";
55

66
export const metadata: Metadata = {
77
title: "About — Yamac",
8-
description:
9-
"About Yamac, an Electrical & Electronics student building full-stack projects.",
8+
description: "About Yamac and the work in electronics + software.",
109
};
1110

1211
export default async function AboutPage() {
13-
const cookieStore = await cookies();
14-
const language = cookieStore.get("lang")?.value === "en" ? "en" : "tr";
15-
const t = copy[language].pages.about;
16-
12+
const content = await getContent();
1713
return (
1814
<main className="simple-page">
1915
<section className="simple-card">
20-
<p className="eyebrow">{t.eyebrow}</p>
21-
<h1>{t.title}</h1>
22-
<p className="simple-text">{t.body}</p>
16+
<Markdown content={content.pages.about} />
2317
<div className="simple-actions">
24-
<Link className="button primary" href="/">
25-
{t.primary}
26-
</Link>
27-
<Link className="button ghost" href="/projects">
28-
{t.secondary}
18+
<Link className="button ghost" href="/">
19+
Back
2920
</Link>
3021
</div>
3122
</section>

src/app/contact/page.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
11
import type { Metadata } from "next";
22
import Link from "next/link";
3-
import { cookies } from "next/headers";
43
import ContactForm from "@/components/ContactForm";
5-
import { copy } from "@/lib/i18n";
4+
import Markdown from "@/components/Markdown";
5+
import { getContent } from "@/lib/content";
66

77
export const metadata: Metadata = {
88
title: "Contact — Yamac",
9-
description: "Contact Yamac for projects and collaborations.",
9+
description: "Get in touch with Yamac.",
1010
};
1111

1212
export default async function ContactPage() {
13-
const cookieStore = await cookies();
14-
const language = cookieStore.get("lang")?.value === "en" ? "en" : "tr";
15-
const t = copy[language].pages.contact;
13+
const content = await getContent();
1614

1715
return (
1816
<main className="simple-page">
1917
<section className="simple-card">
20-
<p className="eyebrow">{t.eyebrow}</p>
21-
<h1>{t.title}</h1>
22-
<p className="simple-text">{t.body}</p>
23-
<div className="contact-card">
24-
<ContactForm labels={t.form} />
18+
<Markdown content={content.pages.contact} />
19+
<div className="contact-form">
20+
<ContactForm labels={content.profile.contactForm} />
2521
</div>
2622
<div className="simple-actions">
2723
<Link className="button ghost" href="/">
28-
{t.primary}
24+
Back
2925
</Link>
3026
</div>
3127
</section>

0 commit comments

Comments
 (0)