๐๐ป myapp26์๋ ์คํจ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
Skin functionality is implemented in myapp26.
๐๐ป ์ฝ๋ ์์ด ๋ง์์ ์ฃผ์ ๋ถ๋ถ๋ง ์ค๋ช
ํฉ๋๋ค .
Due to the large volume of code, I will explain only the key parts.
๐๐ป ์ ์ฒด์ฝ๋๋ ๊นํ๋ธ ์ฝ๋๋ฅผ ์ฐธ์กฐํ์ธ์
Please refer to the GitHub repository for the full code.
https://github.com/gideonslife01/flm-nextjs
๐ ์ ์ฒด ํ๋ก์ ํธ ๊ตฌ์กฐ / Overall Project Structure
myapp26/
โโโ app/ (Next.js App Router)
โ โโโ .well-known/webfinger/route.ts -> webfinger
โ โโโ api/follow/route.ts -> Follow API(temporary)
โ โโโ api/announce/route.ts -> Boost(Announcement)
โ โโโ api/like/route.ts. -> Like API
โ โโโ api/posts/route.ts -> Writing API
โ โโโ api/timeline/route.ts -> Timeline API
โ โโโ users/[username]/
โ โ โโโ statuses/[id]/route.ts -> Indivisual Post
โ โ โโโ route.ts -> Acotr Information
โ โ โโโ followers/route.ts -> Followers List
โ โ โโโ following/route.ts -> Following List
โ โ โโโ inbox/route.ts -> Inbox
โ โ โโโ outbox/route.ts -> outbox
โ โโโ usersui/[username]/
โ โ โโโ page.tsx -> Timeline UI
โ โ โโโ _components/themes/
โ โ โโโ pinafore/Pinaforetheme.tsx -> Theme 1
โ โ โโโ mastodon/Mastodontheme.tsx -> Theme 2
โ โ โโโ minimal/Minimaltheme.tsx -> Theme 3
โ โโโ layout.tsx, page.tsx, globals.css
โ โโโ favicon.ico
โโโ lib/
โ โโโ theme.tsx -> Theme Provider
โ โโโ ap.ts -> Follow Accept
โ โโโ db.ts -> DB connection
โโโ data/
โ โโโ keys/ -> private.pem, public.pem
โโโ data.sqlite -> Database
โโโ Caddyfile -> https
โโโ package.json
๐ ํ๋ก์ ํธ ์์(myapp26)
Project Start (myapp26)
npx create-next-app@latest
๐ SQLite์ค์น / Installing SQLite
cd ~/myapp26
npm install better-sqlite3
npm install -D @types/better-sqlite3
๐ DDNS,https์ค์ / DDNS,https settings
๐ ๋๋ฉ์ธํ์ฉ, @userid ์ฌ์ฉํ๊ธฐ
Allowing domains, using @userid
— next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* allow domain*/
allowedDevOrigins: ['aloy-horizon.duckdns.org', '*.duckdns.org'],
/* host.domain.org/@userid */
async rewrites() {
return [
{
source: '/@:username',
destination: '/usersui/:username',
},
]
}
};
export default nextConfig;
๐ ์ฝ๋์์ / Code Modification
โ๏ธ myapp26/lib/theme.tsxํ์ผ์ ๋ถ๋ฌ์ ์ปจํ
์คํธ ํ๋ก๋ฐ์ด๋๋ฅผ ์ ์ฉํฉ๋๋ค.
Import the myapp26/lib/theme.tsx file and apply the context provider.
๐ก ์ปจํ
์คํธ ํ๋ก๋ฐ์ด๋๋ ํ๋์ ์ด๋ฒคํธ๋ก ์ ์ฒด ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ๊ธฐ๋ฅ์ผ๋ก ํ
๋ง์ ์ฉ์ด๋ ์ธ์ด ์ ์ฉ ์ค์ ์ ๋ง์ด ์ฌ์ฉ๋ฉ๋๋ค.
A Context Provider is a feature that allows the entire state to be updated via a single event; it is commonly used for settings such as applying themes or languages.
— myapp26/app/usersui/[username]/Layout.tsx
import { ThemeProvider } from "@/lib/theme"; // โ
myapp26
... ...
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">
{/* โ
myapp26 - Add ThemeProvide */}
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
โ๏ธ ์ฌ๊ธฐ์ ํ
๋งํ์ผ์ ๋ถ๋ฌ์ค๊ณ ํ
๋ง(์คํจ)์์ ์ฉํฉ๋๋ค.
Here, you load the theme file and apply the theme (skin).
— myapp26/app/paget.tsx
... ...
import { PinaforeTheme } from './_components/themes/pinafore/Pinaforetheme';
import { MastodonTheme } from './_components/themes/mastodon/Mastodontheme';
import { MinimalTheme } from './_components/themes/minimal/Minimaltheme';
... ...
// โ
timeline fetch
useEffect(() => {
params.then(p => {
setUsername(p.username);
fetch(`/api/timeline?username=${p.username}`)
.then(r => r.json())
.then(data => {
setTimeline(data);
setLoading(false);
});
});
}, [params]);
... ...
if (loading) return <div style={{ padding: 20 }}>Loading...</div>;
// โ
theme ๋ถ๊ธฐ + props ์ ๋ฌ / Theme branching + props passing
const props = { timeline, username, onBoost: handleBoost, onLike: handleLike };
// apply theme
if (theme === 'pinafore') return <PinaforeTheme {...props} />
if (theme === 'mastodon') return <MastodonTheme {...props} />
if (theme === 'minimal') return <MinimalTheme {...props} />
return <PinaforeTheme {...props} />;
๐ ๋๋ ํ ๋ฆฌ ๋ฐ ํ์ผ ์ถ๊ฐ / Add directories and files
โ๏ธ ํ์๋ผ์ธ ์คํจ(ํ ๋ง) UI / Timeline Skin (Theme) UI
— ์๋์ ํ์ผ์ ๋ชจ๋ ํ
๋ง ๋์์ธ์ผ๋ก ์ข์์,๋ถ์คํธ๊ธฐ๋ฅ์ ๋น ์ ธ์์ต๋๋ค.
The files below are all theme designs; the boost function is not included.
myapp26/app/usersui/[username]/_components/themes/pinafore/Pinaforetheme.tsx
myapp26/app/usersui/[username]/_components/themes/mastodon/Mastodontheme.tsx
myapp26/app/usersui/[username]/_components/themes/minimal/Minimaltheme.tsx
— ์ธ ํ์ผ ๋ชจ๋ ๋์ผํ๋ฉฐ ํ
๋ง์ ๊ตฌ๋ถ์ด ๊ฐ๋ฅํ๊ฒ ํ
๋ง ์ด๋ฆ๋ง ํ์์คฌ์ต๋๋ค.
All three files are identical; only the theme names are displayed to distinguish between them.
<h2>๐ {username}/ Pinafore</h2> {/* ํ
๋ง์ด๋ฆ / Theme Name */}


— ํ
๋ง ๋ณ์์ ๊ฐ์ด ๋ณํ๋ฉด ์ปจํ
์คํธ ํ๋ก๋ฐ์ด๋์์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ๊ฐ์ ๋ณ๊ฒฝํฉ๋๋ค.
When the value of the theme variable changes, the context provider updates the value in local storage.
... ...
const [theme, setTheme] = useState<ThemeName>('pinafore');
... ...
<select
value={theme} // ํ์ฌ ๋ก์ปฌ์คํ ๋ฆฌ์ง ๊ฐ ํ์ / Display current local storage value
onChange={(e) => {
setTheme(e.target.value as any);
}}>
<option value="pinafore">Pinafore</option>
<option value="mastodon">Mastodon</option>
<option value="minimal">Minimal</option>
</select>


โ๏ธ ์ปจํ ์คํธ ํ๋ก๋ฐ์ด๋ / Context Provider
— myapp26/lib/theme.tsx
... ...
useEffect(() => {
const saved = localStorage.getItem('theme') as ThemeName;
if (saved) setTheme(saved);
}, []);
... ...
return (
<ThemeContext.Provider value={{ theme, setTheme: updateTheme }}>
<div data-theme={theme} style={{ minHeight: '100%', display: 'flex', flexDirection: 'column', flex: 1 }}>
{children}
</div>
</ThemeContext.Provider>
);
1)์ปจํ
์คํธ ํ๋ก๋ฐ์ด๋์ theme๋ณ์๋ฅผ ํ๋ผ๋ฉํฐ๋ก ์ ์ฉํฉ๋๋ค.
Apply the theme variable as a parameter to the context provider.
2)theme ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ๋ค๋ฅธ ํ
๋งํ์ผ์ ๋ถ๋ฌ์ค๊ธฐ ์ํด์ ์ฌ์ฉ๋ฉ๋๋ค.
It is used to load a different theme file when the theme value changes.
3)ํ
๋ง์ด๋ฆ์ ๋ธ๋ผ์ฐ์ ์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅ๋ฉ๋๋ค.
The theme name is stored in the browser’s local storage.
์ํ๋ณต์ 8์ฅ 32์ / John 8:32
“๊ทธ๋ฆฌ๊ณ ๋ํฌ๋ ์ง๋ฆฌ๋ฅผ ์๊ฒ ๋ ๊ฒ์ด๋ฉฐ, ์ง๋ฆฌ๊ฐ ๋ํฌ๋ฅผ ์์ ๋กญ๊ฒ ํ ๊ฒ์ด๋ค.”
“Then you will know the truth ,and the truth will set you free”