Redesign blog as desktop OS
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m10s

This commit is contained in:
박원엽
2026-05-29 09:46:00 +09:00
parent ab585b5faa
commit 7f9bc1f83b
24 changed files with 1016 additions and 592 deletions

View File

@@ -5,6 +5,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import { Folder, FolderOpen, FolderPlus, FolderTree, Loader2, Save, Trash2 } from 'lucide-react';
import { createCategory, deleteCategory, getCategories, updateCategory } from '@/api/category';
import WindowSurface from '@/components/ui/WindowSurface';
import { Category, CategoryUpdateRequest } from '@/types';
import { clsx } from 'clsx';
@@ -342,7 +343,7 @@ export default function AdminCategoryPanel() {
};
return (
<section className="rounded-lg border border-gray-200 bg-white p-5">
<WindowSurface title="Categories" subtitle="Finder tree" bodyClassName="p-5">
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<h2 className="flex items-center gap-2 text-lg font-bold text-gray-950">
@@ -426,6 +427,6 @@ export default function AdminCategoryPanel() {
)}
</div>
</div>
</section>
</WindowSurface>
);
}

View File

@@ -14,6 +14,7 @@ import {
X,
} from 'lucide-react';
import { deleteAdminComment, getAdminComments } from '@/api/comments';
import WindowSurface from '@/components/ui/WindowSurface';
import { AdminComment, AdminCommentListResponse, PageMeta } from '@/types';
const PAGE_SIZE = 10;
@@ -168,7 +169,7 @@ export default function AdminCommentsPanel() {
};
return (
<section className="rounded-lg border border-gray-200 bg-white p-5">
<WindowSurface title="Comments" subtitle="Moderation" bodyClassName="p-5">
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<h2 className="flex items-center gap-2 text-lg font-bold text-gray-950">
@@ -283,6 +284,6 @@ export default function AdminCommentsPanel() {
onConfirm={handleConfirmDelete}
/>
)}
</section>
</WindowSurface>
);
}

View File

@@ -18,6 +18,7 @@ import {
} from 'lucide-react';
import { getCategories } from '@/api/category';
import { deletePost, getPosts } from '@/api/posts';
import WindowSurface from '@/components/ui/WindowSurface';
import { Category, PageMeta, Post, PostListResponse } from '@/types';
const PAGE_SIZE = 10;
@@ -399,7 +400,7 @@ export default function AdminPostsPanel() {
};
return (
<section className="rounded-lg border border-gray-200 bg-white p-5">
<WindowSurface title="Posts" subtitle="Management" bodyClassName="p-5">
<div className="mb-5 flex flex-col justify-between gap-4 md:flex-row md:items-end">
<div>
<h2 className="flex items-center gap-2 text-lg font-bold text-gray-950">
@@ -665,6 +666,6 @@ export default function AdminPostsPanel() {
onConfirm={handleConfirmBulkDelete}
/>
)}
</section>
</WindowSurface>
);
}

View File

@@ -7,6 +7,7 @@ import toast from 'react-hot-toast';
import { Camera, Loader2, Save, UserRound } from 'lucide-react';
import { getProfile, updateProfile } from '@/api/profile';
import { uploadImage } from '@/api/image';
import WindowSurface from '@/components/ui/WindowSurface';
import { Profile, ProfileUpdateRequest } from '@/types';
const defaultProfile: Profile = {
@@ -206,7 +207,7 @@ export default function AdminProfilePanel() {
].join('|');
return (
<section className="rounded-lg border border-gray-200 bg-white p-5">
<WindowSurface title="Profile" subtitle="Public identity" bodyClassName="p-5">
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<h2 className="flex items-center gap-2 text-lg font-bold text-gray-950">
@@ -224,6 +225,6 @@ export default function AdminProfilePanel() {
) : (
<ProfileForm key={formKey} profile={displayProfile} />
)}
</section>
</WindowSurface>
);
}

View File

@@ -6,6 +6,7 @@ import { useEffect } from 'react';
import toast from 'react-hot-toast';
import { BarChart3, FileText, FolderTree, Loader2, MessageSquareText, UserRound } from 'lucide-react';
import { clsx } from 'clsx';
import WindowSurface from '@/components/ui/WindowSurface';
import { useAuthStore } from '@/store/authStore';
const navItems = [
@@ -57,31 +58,33 @@ export default function AdminRouteShell({ children }: { children: React.ReactNod
return (
<div className="mx-auto max-w-7xl space-y-6 px-1 py-4 md:px-4">
<nav
className="flex gap-2 overflow-x-auto rounded-full border border-[var(--color-line)] bg-[var(--color-control)] p-1 shadow-[var(--shadow-control)] backdrop-blur-2xl"
aria-label="관리자 메뉴"
>
{navItems.map((item) => {
const Icon = item.icon;
const isActive = item.exact ? pathname === item.href : pathname === item.href || pathname.startsWith(`${item.href}/`);
<WindowSurface title="Admin Console" subtitle="Operations toolbar" bodyClassName="p-2">
<nav
className="flex gap-2 overflow-x-auto rounded-lg bg-[var(--color-control)] p-1 backdrop-blur-xl"
aria-label="관리자 메뉴"
>
{navItems.map((item) => {
const Icon = item.icon;
const isActive = item.exact ? pathname === item.href : pathname === item.href || pathname.startsWith(`${item.href}/`);
return (
<Link
key={item.href}
href={item.href}
className={clsx(
'inline-flex h-9 shrink-0 items-center gap-2 rounded-full px-4 text-sm font-semibold transition',
isActive
? 'bg-[var(--color-surface-strong)] text-[var(--color-text)] shadow-sm'
: 'text-[var(--color-text-muted)] hover:bg-black/[0.04] hover:text-[var(--color-text)] dark:hover:bg-white/10',
)}
>
<Icon size={16} />
{item.label}
</Link>
);
})}
</nav>
return (
<Link
key={item.href}
href={item.href}
className={clsx(
'inline-flex h-9 shrink-0 items-center gap-2 rounded-lg px-4 text-sm font-semibold transition',
isActive
? 'bg-[var(--window-bg-strong)] text-[var(--color-text)] shadow-sm'
: 'text-[var(--color-text-muted)] hover:bg-black/[0.04] hover:text-[var(--color-text)] dark:hover:bg-white/10',
)}
>
<Icon size={16} />
{item.label}
</Link>
);
})}
</nav>
</WindowSurface>
{children}
</div>