[nextjs]SNS Server-44(myapp54)Β 

πŸ‘‰πŸ» 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

Screenshot

μš”ν•œλ³΅μŒ 8μž₯ 32절 / John 8:32

“그리고 λ„ˆν¬λŠ” 진리λ₯Ό μ•Œκ²Œ 될 것이며, 진리가 λ„ˆν¬λ₯Ό 자유둭게 ν•  것이닀.”

“Then you will know the truth ,and the truth will set you free”

Leave a Reply