Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.9"
},
Expand Down
14 changes: 14 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { QueryProvider } from "@/components/providers/query-provider";
import { AuthProvider } from "@/contexts/auth-context";
import { ToastProvider } from "@/components/providers/toast-provider";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -32,6 +33,7 @@ export default function RootLayout({
<QueryProvider>
<AuthProvider>
{children}
<ToastProvider />
</AuthProvider>
</QueryProvider>
</body>
Expand Down
235 changes: 105 additions & 130 deletions frontend/src/app/posts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { useLogout } from '@/hooks/use-auth';
import { AppLayout } from '@/components/layout/app-layout';
import { PostsListSkeleton } from '@/components/ui/loading-skeletons';
import { usePosts } from '@/hooks/use-posts';
import { formatDistanceToNow } from 'date-fns';
import { uk } from 'date-fns/locale';

export default function PostsPage() {
const { isAuthenticated, isLoading, user } = useAuth();
const { isAuthenticated, isLoading } = useAuth();
const router = useRouter();
const logout = useLogout();
const { data: postsData, isLoading: postsLoading, error: postsError } = usePosts();

useEffect(() => {
Expand All @@ -22,7 +22,7 @@ export default function PostsPage() {
}
}, [isAuthenticated, isLoading, router]);

if (isLoading || postsLoading) {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
Expand All @@ -34,141 +34,116 @@ export default function PostsPage() {
return null; // Will redirect
}

const posts = postsData?.posts || [];

return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
onClick={() => router.push('/')}
className="text-xl font-semibold text-gray-900"
>
Міні-блог
</Button>
<span className="text-gray-400">|</span>
<span className="text-gray-600">Всі пости</span>
</div>
<div className="flex items-center space-x-4">
<Button
onClick={() => router.push('/posts/create')}
size="sm"
>
Створити пост
</Button>
<Button
variant="outline"
onClick={() => router.push('/profile')}
size="sm"
>
Профіль
</Button>
<span className="text-sm text-gray-600">
{user?.name || user?.email}
</span>
<Button variant="outline" onClick={logout} size="sm">
Вийти
</Button>
</div>
</div>
</div>
</header>

{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
if (postsLoading) {
return (
<AppLayout currentPage="Всі пости">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Всі пости</h1>
<p className="text-gray-600 mt-2">
{postsData?.total ? `Знайдено ${postsData.total} ${postsData.total === 1 ? 'пост' : 'постів'}` : 'Завантаження...'}
</p>
<p className="text-gray-600 mt-2">Завантаження...</p>
</div>
<Button onClick={() => router.push('/posts/create')}>
Написати пост
</Button>
</div>
<PostsListSkeleton />
</AppLayout>
);
}

const posts = postsData?.posts || [];

return (
<AppLayout currentPage="Всі пости">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-8 gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900">Всі пости</h1>
<p className="text-gray-600 mt-2">
{postsData?.total ? `Знайдено ${postsData.total} ${postsData.total === 1 ? 'пост' : 'постів'}` : 'Завантаження...'}
</p>
</div>
<Button onClick={() => router.push('/posts/create')} className="w-full sm:w-auto">
Написати пост
</Button>
</div>

{postsError && (
<Card className="mb-6 border-red-200 bg-red-50">
<CardContent className="pt-6">
<p className="text-red-600">
Помилка завантаження постів: {(postsError as any)?.response?.data?.message || 'Невідома помилка'}
</p>
</CardContent>
</Card>
)}
{postsError && (
<Card className="mb-6 border-red-200 bg-red-50">
<CardContent className="pt-6">
<p className="text-red-600">
Помилка завантаження постів: {(postsError as any)?.response?.data?.message || 'Невідома помилка'}
</p>
</CardContent>
</Card>
)}

{/* Posts Grid */}
{posts.length === 0 && !postsError ? (
<Card className="text-center py-16">
<CardContent>
<h3 className="text-lg font-medium text-gray-900 mb-2">
Постів поки що немає
</h3>
<p className="text-gray-500 mb-6">
Станьте першим, хто поділиться своїми думками!
</p>
<Button onClick={() => router.push('/posts/create')}>
Створити перший пост
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Card
key={post.id}
className="hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => router.push(`/posts/${post.id}`)}
>
<CardHeader>
<CardTitle className="line-clamp-2 text-lg">
{post.title}
</CardTitle>
{post.description && (
<CardDescription className="line-clamp-3">
{post.description}
</CardDescription>
)}
</CardHeader>
<CardContent>
<div className="text-sm text-gray-500 line-clamp-3">
{post.content.substring(0, 150)}
{post.content.length > 150 && '...'}
</div>
</CardContent>
<CardFooter className="flex justify-between items-center pt-4">
<div className="text-sm text-gray-500">
<p className="font-medium">
{post.author.name || post.author.email}
</p>
<p>
{formatDistanceToNow(new Date(post.createdAt), {
addSuffix: true,
locale: uk
})}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
router.push(`/posts/${post.id}`);
}}
>
Читати →
</Button>
</CardFooter>
</Card>
))}
</div>
)}
</main>
</div>
{/* Posts Grid */}
{posts.length === 0 && !postsError ? (
<Card className="text-center py-16">
<CardContent>
<h3 className="text-lg font-medium text-gray-900 mb-2">
Постів поки що немає
</h3>
<p className="text-gray-500 mb-6">
Станьте першим, хто поділиться своїми думками!
</p>
<Button onClick={() => router.push('/posts/create')}>
Створити перший пост
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4 sm:gap-6 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Card
key={post.id}
className="hover:shadow-lg transition-all duration-200 cursor-pointer hover:scale-[1.02]"
onClick={() => router.push(`/posts/${post.id}`)}
>
<CardHeader className="pb-4">
<CardTitle className="line-clamp-2 text-lg leading-tight">
{post.title}
</CardTitle>
{post.description && (
<CardDescription className="line-clamp-2 text-sm">
{post.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="text-sm text-gray-500 line-clamp-3 leading-relaxed">
{post.content.substring(0, 120)}
{post.content.length > 120 && '...'}
</div>
</CardContent>
<CardFooter className="flex justify-between items-center pt-4 border-t border-gray-100">
<div className="text-xs text-gray-500">
<p className="font-medium text-gray-700 truncate max-w-24">
{post.author.name || post.author.email}
</p>
<p>
{formatDistanceToNow(new Date(post.createdAt), {
addSuffix: true,
locale: uk
})}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
router.push(`/posts/${post.id}`);
}}
className="text-xs hover:text-primary"
>
Читати →
</Button>
</CardFooter>
</Card>
))}
</div>
)}
</AppLayout>
);
}
Loading