refactor/better-suspend-model #3

Merged
HideyoshiNakazone merged 9 commits from refactor/better-suspend-model into main 2026-04-17 04:50:56 +00:00
7 changed files with 140 additions and 88 deletions
Showing only changes of commit 79e6fae0f9 - Show all commits

View File

@@ -1,94 +1,18 @@
import { getArticlesPaginated } from '@/lib/feature/article/article.external';
import { ArticleCard } from '@/ui/components/internal/article-card';
import { ArticleListPagination } from '@/ui/components/internal/article-list-pagination';
import { FileTextIcon } from 'lucide-react';
import { ArticleList } from '@/ui/components/internal/article/article-list';
import { ArticleListSkeleton } from '@/ui/components/internal/article/article-list-skeleton';
import { Suspense } from 'react';
const PAGE_SIZE = 9;
const ArticleCardSkeleton = () => (
<div className='flex flex-col overflow-hidden rounded-xl border border-border bg-card'>
<div className='aspect-video w-full animate-pulse bg-muted' />
<div className='flex flex-1 flex-col gap-3 p-5'>
<div className='h-3 w-24 animate-pulse rounded bg-muted' />
<div className='space-y-1.5'>
<div className='h-4 w-full animate-pulse rounded bg-muted' />
<div className='h-4 w-3/4 animate-pulse rounded bg-muted' />
</div>
<div className='flex-1 space-y-1.5'>
<div className='h-3 w-full animate-pulse rounded bg-muted' />
<div className='h-3 w-full animate-pulse rounded bg-muted' />
<div className='h-3 w-2/3 animate-pulse rounded bg-muted' />
</div>
<div className='mt-1 h-3 w-16 animate-pulse rounded bg-muted' />
</div>
</div>
);
const ArticleListSkeleton = () => (
<>
<div className='mb-10 h-4 w-32 animate-pulse rounded bg-muted' />
<div className='grid gap-6 sm:grid-cols-2 xl:grid-cols-3'>
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
<ArticleCardSkeleton key={i} />
))}
</div>
</>
);
type ArticleListProps = {
searchParams: Promise<{ page?: string; pageSize?: string }>;
};
const ArticleList = async ({ searchParams }: ArticleListProps) => {
const { page: pageParam, pageSize: pageSizeParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const pageSize = Number(pageSizeParam) || PAGE_SIZE;
const paginationResult = await getArticlesPaginated(page, pageSize);
if (!paginationResult.ok) throw paginationResult.error;
const { data: articles, totalPages, total } = paginationResult.value;
return (
<>
<p className='mb-10 text-muted-foreground'>
{total === 0
? 'No articles published yet.'
: `${total} article${total === 1 ? '' : 's'} published`}
</p>
{articles.length === 0 ? (
<div className='flex min-h-64 flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border text-center'>
<FileTextIcon className='size-10 text-muted-foreground/40' />
<p className='text-muted-foreground'>
No articles yet. Check back soon!
</p>
</div>
) : (
<div className='grid gap-6 sm:grid-cols-2 xl:grid-cols-3'>
{articles.map((article) => (
<ArticleCard
key={article.externalId}
article={article}
/>
))}
</div>
)}
{totalPages > 1 && (
<div className='mt-12 flex items-center justify-between gap-4'>
<p className='text-sm text-muted-foreground'>
Page {page} of {totalPages}
</p>
<ArticleListPagination
currentPage={page}
totalPages={totalPages}
/>
</div>
)}
</>
);
};
const PAGE_SIZE = 4;
type HomeProps = {
searchParams: Promise<{ page?: string; pageSize?: string }>;
@@ -105,8 +29,13 @@ const Home = async ({ searchParams }: HomeProps) => {
Latest Articles
</h1>
</div>
<Suspense fallback={<ArticleListSkeleton />}>
<ArticleList searchParams={searchParams} />
<Suspense
fallback={<ArticleListSkeleton skeletonSize={PAGE_SIZE} />}
>
<ArticleList
searchParams={searchParams}
defaultPageSize={PAGE_SIZE}
/>
</Suspense>
</div>
);

View File

@@ -39,6 +39,8 @@ export const getArticlesPaginated: (
page: number = 1,
pageSize: number = 10
): Promise<PaginatedArticlesResult> => {
// await new Promise((r) => setTimeout(r, 1000));
const result = await service.getArticlesPaginated(page, pageSize);
if (!result.ok) throw result.error;
return result.value;

View File

@@ -3,7 +3,6 @@ import { TypedResult, wrap } from '@/utils/types/results';
import {
DeleteObjectCommand,
HeadObjectCommand,
ObjectCannedACL,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';

View File

@@ -14,10 +14,6 @@ export const getPublicUrl = async (
if (!storageProvider) {
storageProvider = storage;
}
const session = await getSessionData();
if (!session || !session?.user || session?.user.role !== 'admin') {
throw new Error('Unauthorized: Only admin users can delete articles.');
}
return await storageProvider.get(key);
};

View File

@@ -2,9 +2,15 @@ import { getSessionData } from '@/lib/session/session-storage';
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
const isPublic = createRouteMatcher([
'/home(.*)?',
'/about(.*)?',
'/article(.*)?',
'/api/user(.*)?',
]);

View File

@@ -0,0 +1,33 @@
export const ArticleCardSkeleton = () => (
<div className='flex flex-col overflow-hidden rounded-xl border border-border bg-card'>
<div className='aspect-video w-full animate-pulse bg-muted' />
<div className='flex flex-1 flex-col gap-3 p-5'>
<div className='h-3 w-24 animate-pulse rounded bg-muted' />
<div className='space-y-1.5'>
<div className='h-4 w-full animate-pulse rounded bg-muted' />
<div className='h-4 w-3/4 animate-pulse rounded bg-muted' />
</div>
<div className='flex-1 space-y-1.5'>
<div className='h-3 w-full animate-pulse rounded bg-muted' />
<div className='h-3 w-full animate-pulse rounded bg-muted' />
<div className='h-3 w-2/3 animate-pulse rounded bg-muted' />
</div>
<div className='mt-1 h-3 w-16 animate-pulse rounded bg-muted' />
</div>
</div>
);
export const ArticleListSkeleton = ({
skeletonSize,
}: {
skeletonSize: number;
}) => (
<>
<div className='mb-10 h-4 w-32 animate-pulse rounded bg-muted' />
<div className='grid gap-6 sm:grid-cols-2 xl:grid-cols-3'>
{Array.from({ length: skeletonSize }).map((_, i) => (
<ArticleCardSkeleton key={i} />
))}
</div>
</>
);

View File

@@ -0,0 +1,87 @@
// 'use client';
import { getArticlesPaginated } from '@/lib/feature/article/article.external';
import { ArticleCard } from '@/ui/components/internal/article-card';
import { ArticleListPagination } from '@/ui/components/internal/article-list-pagination';
import { FileTextIcon } from 'lucide-react';
type ArticleListProps = {
searchParams: Promise<{ page?: string; pageSize?: string }>;
defaultPageSize: number;
};
export const ArticleList = async ({
searchParams,
defaultPageSize,
}: ArticleListProps) => {
const { page: pageParam, pageSize: pageSizeParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const pageSize = Number(pageSizeParam) || defaultPageSize;
const paginationResult = await getArticlesPaginated(page, pageSize);
if (!paginationResult.ok) {
return (
<div className='flex min-h-64 flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border text-center'>
<FileTextIcon className='size-10 text-muted-foreground/40' />
<p className='text-muted-foreground'>
Failed to load articles. Please try again later.
</p>
</div>
);
}
const { data: articles, totalPages, total } = paginationResult.value;
return (
<>
<p className='mb-10 text-muted-foreground'>
{total === 0
? 'No articles published yet.'
: `${total} article${total === 1 ? '' : 's'} published`}
</p>
{articles.length === 0 ? (
<div className='flex min-h-64 flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border text-center'>
<FileTextIcon className='size-10 text-muted-foreground/40' />
<p className='text-muted-foreground'>
No articles yet. Check back soon!
</p>
</div>
) : (
<div className='grid gap-6 sm:grid-cols-2 xl:grid-cols-3'>
{articles.map((article) => (
<ArticleCard
key={article.externalId}
article={article}
/>
))}
</div>
)}
{totalPages > 1 && (
<div className='mt-12 flex items-center justify-between gap-4'>
<p className='text-sm text-muted-foreground'>
Page {page} of {totalPages}
</p>
<ArticleListPagination
currentPage={page}
totalPages={totalPages}
/>
</div>
)}
</>
);
};