feat: add public site components (header, footer, pet card, sponsors, etc.)
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { label = 'Donate Now', href = '/donate' }: {
|
||||||
|
label?: string;
|
||||||
|
href?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a {href} class="inline-block px-6 py-3 bg-accent text-white font-bold rounded-lg hover:bg-accent-dark transition shadow-sm">
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SponsorStrip from './SponsorStrip.svelte';
|
||||||
|
import NewsletterSignup from './NewsletterSignup.svelte';
|
||||||
|
|
||||||
|
let { orgName = 'FosterFlow', sponsors = [], facebookUrl = '', instagramUrl = '' }: {
|
||||||
|
orgName?: string;
|
||||||
|
sponsors?: Array<{ id: number; name: string; logoUrl: string | null; websiteUrl: string | null; tier: string }>;
|
||||||
|
facebookUrl?: string;
|
||||||
|
instagramUrl?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer class="bg-gray-900 text-white">
|
||||||
|
{#if sponsors.length > 0}
|
||||||
|
<SponsorStrip {sponsors} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-lg mb-4">{orgName}</h3>
|
||||||
|
<p class="text-gray-400">Saving lives, one paw at a time.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold mb-4">Quick Links</h4>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li><a href="/pets" class="hover:text-white">Our Pets</a></li>
|
||||||
|
<li><a href="/about" class="hover:text-white">About</a></li>
|
||||||
|
<li><a href="/donate" class="hover:text-white">Donate</a></li>
|
||||||
|
<li><a href="/contact" class="hover:text-white">Contact</a></li>
|
||||||
|
<li><a href="/apply/adopt" class="hover:text-white">Apply to Adopt</a></li>
|
||||||
|
<li><a href="/apply/foster" class="hover:text-white">Apply to Foster</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold mb-4">Stay Connected</h4>
|
||||||
|
<NewsletterSignup />
|
||||||
|
<div class="flex gap-4 mt-4">
|
||||||
|
{#if facebookUrl}
|
||||||
|
<a href={facebookUrl} target="_blank" rel="noopener" class="text-gray-400 hover:text-white">Facebook</a>
|
||||||
|
{/if}
|
||||||
|
{#if instagramUrl}
|
||||||
|
<a href={instagramUrl} target="_blank" rel="noopener" class="text-gray-400 hover:text-white">Instagram</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-500 text-sm">
|
||||||
|
<p>© {new Date().getFullYear()} {orgName}. All rights reserved.</p>
|
||||||
|
<p class="mt-1">Powered by <a href="https://fosterflow.app" class="text-primary hover:underline">FosterFlow</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { orgName = 'FosterFlow', logoUrl = '' }: {
|
||||||
|
orgName?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let mobileOpen = $state(false);
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ href: '/pets', label: 'Our Pets' },
|
||||||
|
{ href: '/about', label: 'About' },
|
||||||
|
{ href: '/events', label: 'Events' },
|
||||||
|
{ href: '/happy-tails', label: 'Happy Tails' },
|
||||||
|
{ href: '/how-to-help', label: 'How to Help', children: [
|
||||||
|
{ href: '/become-a-foster', label: 'Become a Foster' },
|
||||||
|
{ href: '/volunteer', label: 'Volunteer' },
|
||||||
|
{ href: '/donate', label: 'Donate' },
|
||||||
|
{ href: '/sponsors', label: 'Sponsors' }
|
||||||
|
]},
|
||||||
|
{ href: '/contact', label: 'Contact' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="bg-white shadow-sm sticky top-0 z-50">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<a href="/" class="flex items-center gap-2">
|
||||||
|
{#if logoUrl}
|
||||||
|
<img src={logoUrl} alt={orgName} class="h-10 w-auto" />
|
||||||
|
{:else}
|
||||||
|
<span class="text-xl font-bold text-primary">{orgName}</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav class="hidden lg:flex items-center gap-6">
|
||||||
|
{#each navItems as item}
|
||||||
|
{#if item.children}
|
||||||
|
<div class="relative group">
|
||||||
|
<button class="text-gray-700 hover:text-primary transition">{item.label}</button>
|
||||||
|
<div class="absolute top-full left-0 hidden group-hover:block bg-white shadow-lg rounded-lg py-2 min-w-[180px]">
|
||||||
|
{#each item.children as child}
|
||||||
|
<a href={child.href} class="block px-4 py-2 text-gray-700 hover:bg-gray-50 hover:text-primary">{child.label}</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<a href={item.href} class="text-gray-700 hover:text-primary transition">{item.label}</a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<a href="/donate" class="px-4 py-2 bg-accent text-white rounded-lg hover:bg-accent-dark transition font-medium">Donate</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<button class="lg:hidden p-2" onclick={() => mobileOpen = !mobileOpen}>
|
||||||
|
<span class="text-2xl">{mobileOpen ? '✕' : '☰'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if mobileOpen}
|
||||||
|
<nav class="lg:hidden pb-4 border-t">
|
||||||
|
{#each navItems as item}
|
||||||
|
<a href={item.href} class="block py-2 text-gray-700" onclick={() => mobileOpen = false}>{item.label}</a>
|
||||||
|
{#if item.children}
|
||||||
|
{#each item.children as child}
|
||||||
|
<a href={child.href} class="block py-2 pl-4 text-gray-500 text-sm" onclick={() => mobileOpen = false}>{child.label}</a>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<a href="/donate" class="block mt-2 px-4 py-2 bg-accent text-white rounded-lg text-center font-medium">Donate</a>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let email = $state('');
|
||||||
|
let submitted = $state(false);
|
||||||
|
|
||||||
|
async function subscribe(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
const res = await fetch('/api/newsletter', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email })
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
submitted = true;
|
||||||
|
email = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if submitted}
|
||||||
|
<p class="text-green-400 text-sm">Thanks for subscribing!</p>
|
||||||
|
{:else}
|
||||||
|
<form onsubmit={subscribe} class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
placeholder="Your email"
|
||||||
|
required
|
||||||
|
class="flex-1 px-3 py-2 rounded-lg text-gray-900 text-sm"
|
||||||
|
/>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark text-sm">
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { pet }: {
|
||||||
|
pet: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
species: string;
|
||||||
|
breed: string | null;
|
||||||
|
age: string | null;
|
||||||
|
sex: string;
|
||||||
|
photoUrl: string | null;
|
||||||
|
};
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href="/pets/{pet.slug}" class="group block bg-white rounded-xl shadow-sm overflow-hidden hover:shadow-md transition">
|
||||||
|
<div class="aspect-[4/3] bg-gray-200 overflow-hidden">
|
||||||
|
{#if pet.photoUrl}
|
||||||
|
<img src={pet.photoUrl} alt={pet.name} class="w-full h-full object-cover group-hover:scale-105 transition duration-300" />
|
||||||
|
{:else}
|
||||||
|
<div class="w-full h-full flex items-center justify-center text-gray-400 text-4xl">🐾</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="font-bold text-lg text-gray-900">{pet.name}</h3>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{pet.breed || pet.species}
|
||||||
|
{#if pet.age} · {pet.age}{/if}
|
||||||
|
· {pet.sex}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { sponsors }: {
|
||||||
|
sponsors: Array<{ id: number; name: string; logoUrl: string | null; websiteUrl: string | null; tier: string }>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-gray-800 py-6">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 flex items-center justify-center gap-8 flex-wrap">
|
||||||
|
{#each sponsors as sponsor}
|
||||||
|
{#if sponsor.logoUrl}
|
||||||
|
<a href={sponsor.websiteUrl || '#'} target="_blank" rel="noopener" class="opacity-70 hover:opacity-100 transition">
|
||||||
|
<img src={sponsor.logoUrl} alt={sponsor.name} class="h-10 w-auto" />
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<span class="text-gray-400 text-sm">{sponsor.name}</span>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { stats }: {
|
||||||
|
stats: Array<{ label: string; value: number }>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-6" style="grid-template-columns: repeat({Math.min(stats.length, 4)}, minmax(0, 1fr));">
|
||||||
|
{#each stats as stat}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl md:text-4xl font-bold text-primary">{stat.value.toLocaleString()}</div>
|
||||||
|
<div class="text-sm text-gray-600 mt-1">{stat.label}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user