ππ» myapp44μμλ νμλΌμΈ λΆλΆμ λ€λ₯Έ νμΌλ‘ λΆλ¦¬ν©λλ€.
In myapp44, the timeline section is separated into a different file.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
You can find the full code on GitHub.
https://github.com/gideonslife01/flm-nextjs
π μ 체 νλ‘μ νΈ κ΅¬μ‘° / Overall Project Structure
myapp project/
βββ 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(deprecated)
β βββ api/timeline/route.ts -> Timeline API
β βββ api/followinglist/route.ts -> followinglist API
β βββ api/followerslist/route.ts -> followerslist API
β βββ api/v1/instance/route.ts -> auth
β βββ api/v1/apps/route.ts. -> auth
β βββ api/v1/accounts/verify_credentials/route.ts -> auth
β βββ api/v1/statuses/route.ts -> Write Post
β βββ api/v1/timelines/home/route.ts -> timeline
β βββ api/v1/media/route.ts -> Image upload
β βββ api/v1/media/[id]/route.ts -> Image Delete
β βββ oauth/authorize/route.ts -> auth
β βββ oauth/token/route.ts -> auth
β βββ api/v1/search/home/route.ts -> search
β βββ api/v2/search/home/route.ts -> search
β βββ api/v1/accounts/[id]/followers/route.ts -> followers
β βββ api/v1/accounts/[id]/following/route.ts -> following
β βββ api/v1/statuses/[id]/route.ts -> -> Post delete(pinafore)
β βββ api/auth/signup/route.ts -> Signup
β βββ api/auth/login/route.ts -> Login
β βββ api/auth/logout/route.ts -> Logout
β βββ api/auth/refresh/route.ts -> Refresh Token
β βββ api/cleanup-orphan/route.ts -> post,outbox clean up
β βββ api/auth/me/route.tsx -> Login Check
β βββ auth/signup/page.tsx -> Signup UI
β βββ auth/signup/page.tsx -> Login UI
β βββ 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(Server, 404)
β β βββ ClientPage.tsx. -> Timeline UI(Client,Call Active Themes)
β β βββ _components/themes/
β β βββ themeex/ThemeexTheme.tsx -> Example Theme
β β βββ pinafore/PinaforeTheme.tsx -> Theme 1
β β βββ pinafore/FollowersList.tsx -> Theme 1, FollowersList
β β βββ pinafore/FollowingList.tsx -> Theme 1, FollowingList
β β βββ pinafore/NotificationsList.tsx -> Theme 1, NotificationsList
β β βββ pinafore/TimelineList.tsx -> Theme 1, TimelineList
β β βββ mastodon/MastodonTheme.tsx -> Theme 2
β β βββ minimal/MinimaltTheme.tsx -> Theme 3
β βββ layout.tsx, page.tsx, globals.css
β βββ favicon.ico
βββ lib/
β βββ theme.tsx -> Theme Provider
β βββ watchThemes.ts -> Check real-time theme changes
β βββ auth.ts -> Authentication, User Management
β βββ ap.ts -> Follow,Undo,Create,Likes,Announce
β βββ visibility.ts -> Visibility(Public,Unlisted,Private,Direct)
β βββ db.ts -> DB connection
βββ data/
β βββ keys/userIDs/ -> private.pem, public.pem(New)
β βββ keys/ -> private.pem, public.pem(legacy)
βββ data.sqlite -> Database(1/3)
βββ data.sqlite-wal -> Database(2/3)
βββ data.sqlite-shm -> Database(3/3)
βββ Caddyfile -> https
βββ instrumentation.ts -> Background Server
βββ package.json
π νλ‘μ νΈ μμ / Project Start
π μ½λμμ / Code Modification
βοΈ λ²κ·Έμμ / Bug fix
— μ΄μ νλ‘μ νΈμμ 컀λ©νΈ μ²λ¦¬μ application/jsonκΉμ§ μ½λ©νΈμ²λ¦¬ λλΆλΆ μ½λ©νΈ μ²λ¦¬ν΄μ
Uncomment the part up to application/json that was commented out in the previous project
— app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx
const handlePost = async () => {
... ...
const res = await fetch('/api/v1/statuses', {
method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// ...(token? { Authorization: `Bearer ${token}` } : {})
// },
headers: {
'Content-Type': 'application/json' // β
myapp54 edit: add Content-Type header for JSON
},
credentials: 'include',
body: JSON.stringify({
status: composeText,
visibility: composeVis,
media_ids: mediaIds // β
myapp51 - add media_ids
})
});
... ...
};
π ν λ§ νμΌ λΆλ¦¬ / Separating theme files
— κΈ°μ‘΄μ PinaforeTheme.tsxνμΌμ μ½λλΌμΈμ΄ λ무길μ΄μ μ½λλ₯Ό λΆλ¦¬ν¨.
Split the code because the lines in the existing PinaforeTheme.tsx file were too long.
— app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx
1)μ½λ μμ / Code Modification
{view === 'timeline' && (
<TimelineList
timeline={timeline}
currentUser={currentUser}
onDelete={handleDelete}
onBoost={handleBoostClick}
onLike={handleLikeClick}
onShare={onShare}
/>
)}
— app/usersui/[username]/_components/themes/pinafore/TimelineList.tsx
1) νμΌ μμ± / Create file
A.μ΄ λΆλΆμ κΈ°μ‘΄μ νμλΌμΈμμ λΆλ¦¬λ μ½λμ
λλ€.
This part is code that has been separated from the existing timeline.
// app/usersui/[username]/_components/themes/pinafore/TimelineList.tsx
// β
myapp54 - νμλΌμΈ λΆλ¦¬
'use client';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
const toAbsolute = (u: string) => {
if (!u) return '';
return u.startsWith('http') ? u : `https://${DOMAIN}${u}`;
};
function Avatar({ actor, username, size = 46 }: { actor: string, username: string, size?: number }) {
const seed = actor?.startsWith('https://')
? (() => { try { return new URL(actor).pathname.split('/').pop() || username } catch { return username } })()
: actor || username;
const initial = (seed?.[0] || 'U').toUpperCase();
return (
<div style={{
width: size, height: size, borderRadius: '50%',
background: '#6364ff', color: 'white',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontWeight: 'bold', fontSize: size * 0.4, flexShrink: 0
}}>{initial}</div>
);
}
function getDisplayName(actorOrUsername: string) {
if (!actorOrUsername) return 'unknown';
if (actorOrUsername.startsWith('https://')) {
try {
const url = new URL(actorOrUsername);
const username = url.pathname.split('/').pop() || 'user';
return `${username}@${url.hostname}`;
} catch { return actorOrUsername.split('/').pop() || actorOrUsername; }
}
return actorOrUsername;
}
type Props = {
timeline: any[];
currentUser: string | null;
onDelete: (id: string) => void;
onBoost: (p: any) => void;
onLike: (p: any) => void;
onShare: (p: any) => void;
};
export default function TimelineList({ timeline, currentUser, onDelete, onBoost, onLike, onShare }: Props) {
if (timeline.length === 0) {
return <div style={{ padding: 20, color: '#999' }}>νμλΌμΈμ΄ λΉμ΄μμ΅λλ€ / No posts</div>;
}
return (
<>
{timeline.map((p: any) => (
<article key={`${p.source}-${p.id}`} className="pinafore-status">
<Avatar actor={p.actor} username={p.username} />
<div className="status-content">
<div className="status-header">
<b>{getDisplayName(p.actor)}</b>
<span className="status-time">{new Date(p.created_at).toLocaleString()}</span>
{p.isBoostedPost && <span className="boost-label">π {getDisplayName(p.actor)}</span>}
{currentUser && p.isMine && (
<button onClick={() => onDelete(p.id)} style={{ color: '#e0245e', marginLeft: 'auto' }}>
π Delete
</button>
)}
</div>
<div className="status-text" dangerouslySetInnerHTML={{ __html: p.content }} />
{p.media_attachments && p.media_attachments.length > 0 && (
<div className={`status-media ${p.media_attachments.length === 1 ? 'single' : ''}`}>
{p.media_attachments.map((m: any) => (
<img key={m.id} src={toAbsolute(m.preview_url || m.url)} alt="media" loading="lazy" />
))}
</div>
)}
<div className="status-actions">
<button onClick={() => onBoost(p)} className={p.isMyBoost ? 'boosted' : ''}>π {p.boostCount || ''}</button>
<button onClick={() => onLike(p)} className={p.isMyLike ? 'liked' : ''}>β {p.likeCount || ''}</button>
<button onClick={() => onShare(p)}>β</button>
<button>π¬</button>
</div>
</div>
</article>
))}
</>
);
}
π ν μ€νΈ / Test

μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”