feat: add animation
Docker Build and Push / build-and-push (push) Successful in 7m14s

This commit is contained in:
2026-02-25 10:11:22 +07:00
parent ecaaf437f0
commit 5ce2890023
12 changed files with 191 additions and 37 deletions
+149
View File
@@ -123,3 +123,152 @@
@apply bg-background text-foreground; @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;
}
}
+2 -2
View File
@@ -87,7 +87,7 @@ export default function CreateFormPage() {
Back to forms Back to forms
</Link> </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 Create New Form
</h1> </h1>
@@ -98,7 +98,7 @@ export default function CreateFormPage() {
</div> </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="h-2 -mx-6 -mt-6 mb-6 rounded-t-xl bg-primary" />
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<FormInput <FormInput
+2 -2
View File
@@ -123,7 +123,7 @@ export default function EditFormPage() {
Back to form Back to form
</Link> </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 Edit Form
</h1> </h1>
@@ -134,7 +134,7 @@ export default function EditFormPage() {
</div> </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="h-2 -mx-6 -mt-6 mb-6 rounded-t-xl bg-primary" />
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<FormInput <FormInput
+2 -2
View File
@@ -130,7 +130,7 @@ export default function FormResponsesPage() {
</Link> </Link>
{/* Header */} {/* 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="h-2 rounded-t-xl bg-primary" />
<div className="p-6"> <div className="p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"> <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> </div>
{/* Tabs */} {/* 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 <button
onClick={() => setActiveTab("summary")} onClick={() => setActiveTab("summary")}
className={`flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors ${ className={`flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
+8 -7
View File
@@ -104,7 +104,7 @@ export default function FormPreviewPage() {
Back to forms Back to forms
</Link> </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="h-2 rounded-t-xl bg-primary" />
<div className="p-6"> <div className="p-6">
<div className="mb-4 flex flex-wrap items-center gap-3"> <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"> <div className="flex flex-col gap-4">
{form.questions.map((question, index) => ( {form.questions.map((question, index) => (
<QuestionPreview <div key={question.id} className="animate-fade-in-up" style={{ animationDelay: `${0.15 + index * 0.08}s` }}>
key={question.id} <QuestionPreview
question={question} question={question}
index={index} index={index}
/> />
</div>
))} ))}
</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"> <p className="text-sm text-muted-foreground">
This is a read-only preview. Click below to fill out this form. This is a read-only preview. Click below to fill out this form.
</p> </p>
+4 -2
View File
@@ -207,8 +207,10 @@ export default function FormsPage() {
</div> </div>
) : forms.length > 0 ? ( ) : forms.length > 0 ? (
<div className="grid gap-5 sm:grid-cols-2"> <div className="grid gap-5 sm:grid-cols-2">
{forms.map((form) => ( {forms.map((form, index) => (
<FormCard key={form.id} form={form} /> <div key={form.id} className="animate-float-in" style={{ animationDelay: `${index * 0.08}s` }}>
<FormCard form={form} />
</div>
))} ))}
</div> </div>
) : ( ) : (
+3 -3
View File
@@ -58,7 +58,7 @@ export default function LoginPage() {
<div className="flex min-h-screen flex-col bg-background"> <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="flex flex-1 items-center justify-center px-4 py-12">
<div className="w-full max-w-md"> <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"> <h1 className="text-2xl font-bold text-foreground">
Welcome back Welcome back
</h1> </h1>
@@ -67,7 +67,7 @@ export default function LoginPage() {
</p> </p>
</div> </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"> <form onSubmit={handleSubmit} className="flex flex-col gap-4">
{errors.general && ( {errors.general && (
<p className="rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive"> <p className="rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive">
@@ -127,7 +127,7 @@ export default function LoginPage() {
</form> </form>
</div> </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? "} {"Don't have an account? "}
<Link <Link
to="/register" to="/register"
+4 -4
View File
@@ -4,10 +4,10 @@ export function meta() {
export default function NotFound() { export default function NotFound() {
return ( return (
<div style={{ textAlign: "center", padding: "4rem" }}> <div className="flex min-h-screen flex-col items-center justify-center bg-background px-4" style={{ textAlign: "center" }}>
<h1>404</h1> <h1 className="text-6xl font-bold text-foreground animate-scale-bounce">404</h1>
<p>Page not found.</p> <p className="mt-4 text-lg text-muted-foreground animate-fade-in-up" style={{ animationDelay: '0.2s' }}>Page not found.</p>
<a href="/">Go home</a> <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> </div>
); );
} }
+3 -3
View File
@@ -64,7 +64,7 @@ export default function RegisterPage() {
<div className="flex min-h-screen flex-col bg-background"> <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="flex flex-1 items-center justify-center px-4 py-12">
<div className="w-full max-w-md"> <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"> <h1 className="text-2xl font-bold text-foreground">
Create your account Create your account
</h1> </h1>
@@ -73,7 +73,7 @@ export default function RegisterPage() {
</p> </p>
</div> </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"> <form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="flex gap-3"> <div className="flex gap-3">
<FormInput <FormInput
@@ -142,7 +142,7 @@ export default function RegisterPage() {
</form> </form>
</div> </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?{" "} Already have an account?{" "}
<Link <Link
to="/login" to="/login"
+12 -11
View File
@@ -140,8 +140,8 @@ export default function SubmitFormPage() {
<div className="flex min-h-screen flex-col bg-background"> <div className="flex min-h-screen flex-col bg-background">
<Navbar /> <Navbar />
<main className="flex flex-1 items-center justify-center"> <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="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"> <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" /> <CheckCircle2 className="h-8 w-8 text-green-600 dark:text-green-400" />
</div> </div>
<h2 className="text-xl font-bold text-foreground"> <h2 className="text-xl font-bold text-foreground">
@@ -198,7 +198,7 @@ export default function SubmitFormPage() {
Back to form preview Back to form preview
</Link> </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="h-2 rounded-t-xl bg-primary" />
<div className="p-6"> <div className="p-6">
<h1 className="text-xl font-bold text-foreground text-balance sm:text-2xl"> <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 onSubmit={handleSubmit} className="flex flex-col gap-4">
{form.questions.map((question, index) => ( {form.questions.map((question, index) => (
<QuestionField <div key={question.id} className="animate-fade-in-up" style={{ animationDelay: `${0.1 + index * 0.08}s` }}>
key={question.id} <QuestionField
question={question} question={question}
index={index} index={index}
value={answers[question.id] ?? ""} value={answers[question.id] ?? ""}
onChange={(val) => updateAnswer(question.id, val)} onChange={(val) => updateAnswer(question.id, val)}
error={validationErrors[question.id]} error={validationErrors[question.id]}
/> />
</div>
))} ))}
<div className="mt-4 flex items-center justify-between"> <div className="mt-4 flex items-center justify-between">
+1 -1
View File
@@ -14,7 +14,7 @@ interface FormCardProps {
export function FormCard({ form }: FormCardProps) { export function FormCard({ form }: FormCardProps) {
return ( return (
<div className="group flex flex-col rounded-xl border border-border bg-card shadow-sm transition-shadow hover:shadow-md"> <div className="group flex flex-col rounded-xl border border-border bg-card shadow-sm card-hover">
<div className="h-1.5 rounded-t-xl bg-primary" /> <div className="h-1.5 rounded-t-xl bg-primary" />
<div className="flex flex-1 flex-col p-5"> <div className="flex flex-1 flex-col p-5">
+1
View File
@@ -34,6 +34,7 @@ export const FormButton = forwardRef<HTMLButtonElement, FormButtonProps>(
className={cn( className={cn(
"inline-flex items-center justify-center gap-2 font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "inline-flex items-center justify-center gap-2 font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50", "disabled:pointer-events-none disabled:opacity-50",
"btn-press",
variantStyles[variant], variantStyles[variant],
sizeStyles[size], sizeStyles[size],
className className