feat: Add visual feedback and polish (Task 15)
- Implement toast notification system for success/error messages - Add loading states with skeleton loaders for async operations - Create smooth CSS transitions and animations - Enhance empty states with EmptyState component - Add loading.tsx files for Next.js loading states - Integrate toast notifications across all CRUD operations - Add fade-in and scale-in animations for better UX - Improve visual feedback for user actions
This commit is contained in:
@@ -30,6 +30,57 @@ mark {
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations for visual feedback */
|
||||
@layer utilities {
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slide-in-right 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scale-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Smooth transitions for interactive elements */
|
||||
.transition-smooth {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scrolling for better mobile experience */
|
||||
@layer base {
|
||||
html {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
|
||||
import OfflineIndicator from '@/components/OfflineIndicator'
|
||||
import { ToastProvider } from '@/components/ToastContainer'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Ultimate Lists',
|
||||
@@ -67,9 +68,11 @@ export default function RootLayout({
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
</head>
|
||||
<body className="bg-gray-50 min-h-screen">
|
||||
<ServiceWorkerRegistration />
|
||||
<OfflineIndicator />
|
||||
{children}
|
||||
<ToastProvider>
|
||||
<ServiceWorkerRegistration />
|
||||
<OfflineIndicator />
|
||||
{children}
|
||||
</ToastProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
35
app/list/[id]/loading.tsx
Normal file
35
app/list/[id]/loading.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ListDetailSkeleton } from '@/components/SkeletonLoader'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-6xl">
|
||||
{/* Header Skeleton */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4 transition-colors tap-target-sm text-sm sm:text-base"
|
||||
>
|
||||
<ArrowLeft size={20} className="flex-shrink-0" />
|
||||
<span>Back to Lists</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-2">
|
||||
<div className="w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-gray-200 animate-pulse" />
|
||||
<div className="h-8 sm:h-10 w-48 bg-gray-200 rounded animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-32 bg-gray-200 rounded animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Inline Item Creation Skeleton */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<div className="h-14 bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Items List Skeleton */}
|
||||
<ListDetailSkeleton />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
24
app/loading.tsx
Normal file
24
app/loading.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ListCardSkeleton } from '@/components/SkeletonLoader'
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-7xl">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6 sm:mb-8">
|
||||
<div className="h-10 w-48 bg-gray-200 rounded animate-pulse" />
|
||||
<div className="h-12 w-32 bg-gray-200 rounded-lg animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Global Search Skeleton */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<div className="h-12 bg-gray-100 border border-gray-300 rounded-lg animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
|
||||
<ListCardSkeleton />
|
||||
<ListCardSkeleton />
|
||||
<ListCardSkeleton />
|
||||
<ListCardSkeleton />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
23
app/page.tsx
23
app/page.tsx
@@ -2,6 +2,7 @@ import { getLists } from '@/lib/actions'
|
||||
import ListCard from '@/components/ListCard'
|
||||
import NewListButton from '@/components/NewListButton'
|
||||
import GlobalSearch from '@/components/GlobalSearch'
|
||||
import EmptyState from '@/components/EmptyState'
|
||||
|
||||
export default async function Home() {
|
||||
const lists = await getLists()
|
||||
@@ -18,16 +19,18 @@ export default async function Home() {
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
|
||||
{lists.map((list) => (
|
||||
<ListCard key={list.id} list={list} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{lists.length === 0 && (
|
||||
<div className="text-center py-12 sm:py-16">
|
||||
<p className="text-gray-500 text-base sm:text-lg mb-3 sm:mb-4">No lists yet</p>
|
||||
<p className="text-gray-400 text-sm sm:text-base">Create your first list to get started</p>
|
||||
{lists.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="list"
|
||||
title="No lists yet"
|
||||
description="Create your first list to start organizing your tasks"
|
||||
action={<NewListButton />}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 animate-fade-in">
|
||||
{lists.map((list) => (
|
||||
<ListCard key={list.id} list={list} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user