This commit is contained in:
+149
@@ -123,3 +123,152 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Animation keyframes ── */
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fade-in-down {
|
||||
from { opacity: 0; transform: translateY(-12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slide-in-left {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes scale-bounce {
|
||||
0% { opacity: 0; transform: scale(0.5); }
|
||||
60% { opacity: 1; transform: scale(1.08); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes float-in {
|
||||
0% { opacity: 0; transform: translateY(24px) scale(0.97); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes pulse-soft {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* ── Utility classes ── */
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.5s ease-out both;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.5s ease-out both;
|
||||
}
|
||||
|
||||
.animate-fade-in-down {
|
||||
animation: fade-in-down 0.4s ease-out both;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slide-in-left 0.5s ease-out both;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scale-in 0.4s ease-out both;
|
||||
}
|
||||
|
||||
.animate-scale-bounce {
|
||||
animation: scale-bounce 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
}
|
||||
|
||||
.animate-float-in {
|
||||
animation: float-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
background: linear-gradient(90deg, transparent 25%, var(--primary) 50%, transparent 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.animate-pulse-soft {
|
||||
animation: pulse-soft 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Stagger delays for children */
|
||||
.stagger-1 { animation-delay: 0.05s; }
|
||||
.stagger-2 { animation-delay: 0.1s; }
|
||||
.stagger-3 { animation-delay: 0.15s; }
|
||||
.stagger-4 { animation-delay: 0.2s; }
|
||||
.stagger-5 { animation-delay: 0.25s; }
|
||||
.stagger-6 { animation-delay: 0.3s; }
|
||||
.stagger-7 { animation-delay: 0.35s; }
|
||||
.stagger-8 { animation-delay: 0.4s; }
|
||||
|
||||
/* Button press effect */
|
||||
.btn-press {
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.btn-press:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.btn-press:active {
|
||||
transform: translateY(0.5px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Card hover lift */
|
||||
.card-hover {
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* Smooth loading skeleton */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--muted) 25%, var(--accent) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-fade-in,
|
||||
.animate-fade-in-up,
|
||||
.animate-fade-in-down,
|
||||
.animate-slide-in-left,
|
||||
.animate-scale-in,
|
||||
.animate-scale-bounce,
|
||||
.animate-float-in {
|
||||
animation: none !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: none;
|
||||
}
|
||||
.btn-press:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function CreateFormPage() {
|
||||
Back to forms
|
||||
</Link>
|
||||
|
||||
<h1 className="mb-6 text-2xl font-bold text-foreground">
|
||||
<h1 className="mb-6 text-2xl font-bold text-foreground animate-fade-in-up">
|
||||
Create New Form
|
||||
</h1>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function CreateFormPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8 rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<div className="mb-8 rounded-xl border border-border bg-card p-6 shadow-sm animate-float-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="h-2 -mx-6 -mt-6 mb-6 rounded-t-xl bg-primary" />
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormInput
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function EditFormPage() {
|
||||
Back to form
|
||||
</Link>
|
||||
|
||||
<h1 className="mb-6 text-2xl font-bold text-foreground">
|
||||
<h1 className="mb-6 text-2xl font-bold text-foreground animate-fade-in-up">
|
||||
Edit Form
|
||||
</h1>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function EditFormPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8 rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<div className="mb-8 rounded-xl border border-border bg-card p-6 shadow-sm animate-float-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="h-2 -mx-6 -mt-6 mb-6 rounded-t-xl bg-primary" />
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormInput
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function FormResponsesPage() {
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 rounded-xl border border-border bg-card shadow-sm">
|
||||
<div className="mb-8 rounded-xl border border-border bg-card shadow-sm animate-float-in">
|
||||
<div className="h-2 rounded-t-xl bg-primary" />
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
@@ -183,7 +183,7 @@ export default function FormResponsesPage() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-6 flex gap-1 rounded-lg border border-border bg-card p-1">
|
||||
<div className="mb-6 flex gap-1 rounded-lg border border-border bg-card p-1 animate-fade-in-up" style={{ animationDelay: '0.15s' }}>
|
||||
<button
|
||||
onClick={() => setActiveTab("summary")}
|
||||
className={`flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
||||
|
||||
+8
-7
@@ -104,7 +104,7 @@ export default function FormPreviewPage() {
|
||||
Back to forms
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 rounded-xl border border-border bg-card shadow-sm">
|
||||
<div className="mb-8 rounded-xl border border-border bg-card shadow-sm animate-float-in">
|
||||
<div className="h-2 rounded-t-xl bg-primary" />
|
||||
<div className="p-6">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||
@@ -172,15 +172,16 @@ export default function FormPreviewPage() {
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{form.questions.map((question, index) => (
|
||||
<QuestionPreview
|
||||
key={question.id}
|
||||
question={question}
|
||||
index={index}
|
||||
/>
|
||||
<div key={question.id} className="animate-fade-in-up" style={{ animationDelay: `${0.15 + index * 0.08}s` }}>
|
||||
<QuestionPreview
|
||||
question={question}
|
||||
index={index}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col items-center gap-3 rounded-xl border border-dashed border-border bg-card/50 p-6">
|
||||
<div className="mt-8 flex flex-col items-center gap-3 rounded-xl border border-dashed border-border bg-card/50 p-6 animate-fade-in" style={{ animationDelay: '0.4s' }}>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is a read-only preview. Click below to fill out this form.
|
||||
</p>
|
||||
|
||||
@@ -207,8 +207,10 @@ export default function FormsPage() {
|
||||
</div>
|
||||
) : forms.length > 0 ? (
|
||||
<div className="grid gap-5 sm:grid-cols-2">
|
||||
{forms.map((form) => (
|
||||
<FormCard key={form.id} form={form} />
|
||||
{forms.map((form, index) => (
|
||||
<div key={form.id} className="animate-float-in" style={{ animationDelay: `${index * 0.08}s` }}>
|
||||
<FormCard form={form} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function LoginPage() {
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 flex flex-col items-center gap-3">
|
||||
<div className="mb-8 flex flex-col items-center gap-3 animate-fade-in-down">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Welcome back
|
||||
</h1>
|
||||
@@ -67,7 +67,7 @@ export default function LoginPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<div className="rounded-xl border border-border bg-card p-6 shadow-sm animate-float-in" style={{ animationDelay: '0.15s' }}>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{errors.general && (
|
||||
<p className="rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
@@ -127,7 +127,7 @@ export default function LoginPage() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-sm text-muted-foreground">
|
||||
<p className="mt-6 text-center text-sm text-muted-foreground animate-fade-in" style={{ animationDelay: '0.3s' }}>
|
||||
{"Don't have an account? "}
|
||||
<Link
|
||||
to="/register"
|
||||
|
||||
@@ -4,10 +4,10 @@ export function meta() {
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div style={{ textAlign: "center", padding: "4rem" }}>
|
||||
<h1>404</h1>
|
||||
<p>Page not found.</p>
|
||||
<a href="/">Go home</a>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-background px-4" style={{ textAlign: "center" }}>
|
||||
<h1 className="text-6xl font-bold text-foreground animate-scale-bounce">404</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground animate-fade-in-up" style={{ animationDelay: '0.2s' }}>Page not found.</p>
|
||||
<a href="/" className="mt-6 inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground btn-press animate-fade-in-up" style={{ animationDelay: '0.35s' }}>Go home</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export default function RegisterPage() {
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 flex flex-col items-center gap-3">
|
||||
<div className="mb-8 flex flex-col items-center gap-3 animate-fade-in-down">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Create your account
|
||||
</h1>
|
||||
@@ -73,7 +73,7 @@ export default function RegisterPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<div className="rounded-xl border border-border bg-card p-6 shadow-sm animate-float-in" style={{ animationDelay: '0.15s' }}>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div className="flex gap-3">
|
||||
<FormInput
|
||||
@@ -142,7 +142,7 @@ export default function RegisterPage() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-sm text-muted-foreground">
|
||||
<p className="mt-6 text-center text-sm text-muted-foreground animate-fade-in" style={{ animationDelay: '0.3s' }}>
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
to="/login"
|
||||
|
||||
+12
-11
@@ -140,8 +140,8 @@ export default function SubmitFormPage() {
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<Navbar />
|
||||
<main className="flex flex-1 items-center justify-center">
|
||||
<div className="mx-4 flex max-w-md flex-col items-center rounded-xl border border-border bg-card p-8 text-center shadow-sm">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
||||
<div className="mx-4 flex max-w-md flex-col items-center rounded-xl border border-border bg-card p-8 text-center shadow-sm animate-scale-in">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30 animate-scale-bounce">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-foreground">
|
||||
@@ -198,7 +198,7 @@ export default function SubmitFormPage() {
|
||||
Back to form preview
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 rounded-xl border border-border bg-card shadow-sm">
|
||||
<div className="mb-8 rounded-xl border border-border bg-card shadow-sm animate-float-in">
|
||||
<div className="h-2 rounded-t-xl bg-primary" />
|
||||
<div className="p-6">
|
||||
<h1 className="text-xl font-bold text-foreground text-balance sm:text-2xl">
|
||||
@@ -223,14 +223,15 @@ export default function SubmitFormPage() {
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{form.questions.map((question, index) => (
|
||||
<QuestionField
|
||||
key={question.id}
|
||||
question={question}
|
||||
index={index}
|
||||
value={answers[question.id] ?? ""}
|
||||
onChange={(val) => updateAnswer(question.id, val)}
|
||||
error={validationErrors[question.id]}
|
||||
/>
|
||||
<div key={question.id} className="animate-fade-in-up" style={{ animationDelay: `${0.1 + index * 0.08}s` }}>
|
||||
<QuestionField
|
||||
question={question}
|
||||
index={index}
|
||||
value={answers[question.id] ?? ""}
|
||||
onChange={(val) => updateAnswer(question.id, val)}
|
||||
error={validationErrors[question.id]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user