Small UI changes
This commit is contained in:
22
src/components/Navigation.astro
Normal file
22
src/components/Navigation.astro
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
import SignOut from "./SignOut.astro";
|
||||
---
|
||||
|
||||
<div class="fixed top-0 right-0 p-4 z-50">
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-square btn-ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/rsvp">RSVP</a></li>
|
||||
<li><a href="/registry">View Registry</a></li>
|
||||
<li><a href="/faq">FAQ</a></li>
|
||||
<li>
|
||||
<SignOut />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
@ -4,6 +4,7 @@ import "../styles/global.css";
|
||||
interface FormData {
|
||||
name: string;
|
||||
dietaryRestrictions: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
@ -15,6 +16,7 @@ const RSVPForm = () => {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: "",
|
||||
dietaryRestrictions: "",
|
||||
notes: "",
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -61,6 +63,7 @@ const RSVPForm = () => {
|
||||
setFormData({
|
||||
name: "",
|
||||
dietaryRestrictions: "",
|
||||
notes: "",
|
||||
});
|
||||
setSuccess(true);
|
||||
} catch (err: any) {
|
||||
@ -163,6 +166,26 @@ const RSVPForm = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Notes Input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="notes" className="label">
|
||||
<span className="label-text">Anything else you'd like Ix and Tasha to know?</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
value={formData.notes}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
notes: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="e.g. only coming for ceremony, bringing a +1, etc."
|
||||
className="textarea textarea-bordered h-24"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
|
71
src/components/RSVPManager.tsx
Normal file
71
src/components/RSVPManager.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { RSVPItem } from "../lib/types";
|
||||
|
||||
const RSVPManager = () => {
|
||||
const [rsvpList, setRSVPList] = useState<RSVPItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRSVPList();
|
||||
}, []);
|
||||
|
||||
const fetchRSVPList = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch("/api/rsvp");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setRSVPList(data);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
console.error("Failed to fetch RSVP list:", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center">Loading RSVP list...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500 text-center">Error: {error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Dietary Restrictions</th>
|
||||
<th>Notes</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rsvpList.map((rsvp, index) => (
|
||||
<tr key={index}>
|
||||
<td>{rsvp.name}</td>
|
||||
<td>{rsvp.dietaryRestrictions || "None"}</td>
|
||||
<td>{(rsvp.notes && rsvp.notes !== "undefined") ? rsvp.notes.trim() : "None"}</td>
|
||||
<td>{new Date(rsvp.timestamp).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<p>Total RSVPs: {rsvpList.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RSVPManager;
|
9
src/components/SignOut.astro
Normal file
9
src/components/SignOut.astro
Normal file
@ -0,0 +1,9 @@
|
||||
<button class="btn btn-secondary" onclick="handleSignOut()">Sign Out</button>
|
||||
|
||||
<script>
|
||||
function handleSignOut() {
|
||||
sessionStorage.removeItem("isAuthenticated");
|
||||
sessionStorage.removeItem("role");
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import Navigation from "../components/Navigation.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@ -21,6 +22,7 @@ const { title } = Astro.props;
|
||||
<title>{title || "Ixabatasha"}</title>
|
||||
</head>
|
||||
<body class="flex items-center justify-center min-h-screen">
|
||||
<Navigation />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import AuthLayout from "./AuthLayout.astro";
|
||||
import Navigation from "../components/Navigation.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@ -18,12 +19,16 @@ const { title } = Astro.props;
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>❤️</text></svg>"
|
||||
/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap" rel="stylesheet">
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title || "Ixabatasha"}</title>
|
||||
</head>
|
||||
<body class="flex items-center justify-center min-h-screen">
|
||||
<body>
|
||||
<Navigation />
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<AuthLayout title={title}>
|
||||
<slot />
|
||||
</AuthLayout>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -9,5 +9,6 @@ export interface RegistryItem {
|
||||
export interface RSVPItem {
|
||||
name: string;
|
||||
dietaryRestrictions: string;
|
||||
notes: string;
|
||||
timestamp: string;
|
||||
}
|
@ -3,7 +3,7 @@ import { getS3Data, putS3Data } from "../../lib/s3";
|
||||
import type { RSVPItem } from "../../lib/types";
|
||||
|
||||
const objectsToCSV = (data: RSVPItem[]): string => {
|
||||
const headers = ["name", "dietaryRestrictions", "timestamp"];
|
||||
const headers = ["name", "dietaryRestrictions", "notes", "timestamp"];
|
||||
const csvRows = [headers.join(",")];
|
||||
|
||||
data.forEach((entry) => {
|
||||
@ -27,7 +27,12 @@ const csvToObjects = (csv: string): RSVPItem[] => {
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => {
|
||||
const values = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
|
||||
const entry: Partial<RSVPItem> = {};
|
||||
const entry: Partial<RSVPItem> = {
|
||||
name: "",
|
||||
dietaryRestrictions: "",
|
||||
notes: "",
|
||||
timestamp: "",
|
||||
};
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
let value = values[index] || "";
|
||||
@ -39,6 +44,44 @@ const csvToObjects = (csv: string): RSVPItem[] => {
|
||||
});
|
||||
};
|
||||
|
||||
// GET: Retrieve all RSVPs
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
try {
|
||||
const FILE_KEY = "rsvp.csv";
|
||||
const fileContent = await getS3Data<string>(FILE_KEY);
|
||||
|
||||
if (!fileContent) {
|
||||
return new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
const rsvpList = csvToObjects(fileContent);
|
||||
return new Response(JSON.stringify(rsvpList), {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error retrieving RSVPs:", error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Failed to retrieve RSVPs",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// POST: Submit a new RSVP
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
console.log("API endpoint hit - starting request processing");
|
||||
|
||||
@ -65,6 +108,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
existingRsvps.push({
|
||||
...newRsvp,
|
||||
notes: newRsvp.notes || "",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
118
src/pages/faq.astro
Normal file
118
src/pages/faq.astro
Normal file
@ -0,0 +1,118 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<Layout title="FAQ">
|
||||
<div class="flex flex-col gap-8 max-w-3xl mx-auto p-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl mb-8">Frequently Asked Questions</h1>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Dress Code -->
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Dress Code?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
Semi-formal, any colour you want. Want to get dressed up? Go for it! Want to wear a shirt with no stains on it? Go for it!
|
||||
There will be dancing, so choose your wardrobe accordingly. Maybe don't wear a white dress...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dietary Restrictions -->
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Dietary Restrictions?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
We will do our best to accommodate any dietary restrictions you have! Our caterer can cook gluten/dairy/peanut free etc.
|
||||
but may not be able to guarantee that there are no traces of allergens in the food. Please contact us if you have any concerns.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alcohol -->
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Alcohol?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
We will not be serving alcohol.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Childcare -->
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Childcare?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
Your young children are adorable and welcome, but we don't have childcare or designated areas for naps etc.
|
||||
If you have any concerns, please contact us.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gifts -->
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Gifts?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
The best gift is you sharing in our joy! Don't feel obligated. If you would like to give a physical gift,
|
||||
cash is preferred, but we do have a registry available on this website as well.
|
||||
Ix would be over the moon if someone found him a
|
||||
<a href="https://www.youtube.com/watch?v=jeT7X3HfXP8" target="_blank" class="link link-primary">hurdy-gurdy</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help -->
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Can I Help With Anything?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p class="text-lg">
|
||||
If you're up for staying a bit later, we could definitely use your help cleaning up the venues when the wedding is done!
|
||||
Tony and Gladys will be coordinating that the day-of, please speak with them. If you want to help with wedding preparations,
|
||||
please contact Ix or Natasha. Thank you!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact -->
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Where Do I Contact You With All of My Concerns?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<div class="text-lg space-y-2">
|
||||
<p>Email us at: <a href="mailto:ixabatasha25@proton.me" class="link link-primary">ixabatasha25@proton.me</a></p>
|
||||
<p>Call/text Natasha: <a href="tel:13062929000" class="link link-primary">1 (306) 292-9000</a></p>
|
||||
<p>Call/text Ix: <a href="tel:13067179403" class="link link-primary">1 (306) 717-9403</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-8">
|
||||
<a href="/" class="btn btn-primary">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
@ -1,16 +1,63 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import SignOut from "../components/SignOut.tsx";
|
||||
---
|
||||
|
||||
<Layout title="Welcome">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-center">❤️ Natasha + Ixabat ❤️</div>
|
||||
<div class="flex flex-col gap-8 max-w-5xl mx-auto p-6">
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-4">
|
||||
<div class="text-5xl font-normal" style="font-family: 'Great Vibes', cursive;">❤️ Natasha + Ixabat ❤️</div>
|
||||
<p class="text-xl text-gray-600">We hope you can join us in celebration on</p>
|
||||
<p class="text-2xl font-semibold">Saturday, June 7, 2025</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/rsvp">RSVP</a>
|
||||
<a class="btn btn-primary" href="/registry">View Registry</a>
|
||||
<SignOut client:load />
|
||||
<!-- Event Details Cards -->
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<!-- Ceremony Card -->
|
||||
<div class="card bg-base-100 shadow-xl flex-1">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl justify-center">Ceremony</h2>
|
||||
<div class="text-center space-y-2">
|
||||
<p class="text-xl">1:00 PM</p>
|
||||
<p>Preston Avenue Community Church</p>
|
||||
<p class="text-gray-600">2216 Preston Avenue, Saskatoon, SK</p>
|
||||
<a
|
||||
href="https://maps.google.com/?q=2216+Preston+Avenue+Saskatoon+SK"
|
||||
target="_blank"
|
||||
class="btn btn-outline btn-sm mt-2"
|
||||
>
|
||||
View on Map
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reception Card -->
|
||||
<div class="card bg-base-100 shadow-xl flex-1">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl justify-center">Reception</h2>
|
||||
<div class="text-center space-y-2">
|
||||
<p class="text-xl">5:00 PM</p>
|
||||
<p>Saskatoon Christian School</p>
|
||||
<p class="text-gray-600">55 Glazier Road, Corman Park, SK</p>
|
||||
<a
|
||||
href="https://maps.google.com/?q=55+Glazier+Road+Corman+Park+SK"
|
||||
target="_blank"
|
||||
class="btn btn-outline btn-sm mt-2"
|
||||
>
|
||||
View on Map
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RSVP Section -->
|
||||
<div class="text-center space-y-4">
|
||||
<p class="text-lg">Please RSVP whether you're able to come or not by <span class="font-semibold">April 1</span></p>
|
||||
<a href="/rsvp" class="btn btn-primary btn-lg">
|
||||
RSVP Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
@ -18,8 +18,7 @@ import SignOut from "../../components/SignOut.tsx";
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/">Home</a>
|
||||
<SignOut client:load />
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
|
@ -13,8 +13,7 @@ import SignOut from "../../components/SignOut.tsx";
|
||||
<RegistryList client:load />
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/">Home</a>
|
||||
<SignOut client:load />
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
@ -13,8 +13,7 @@ import SignOut from "../components/SignOut.tsx";
|
||||
<RSVP client:load />
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/">Home</a>
|
||||
<SignOut client:load />
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
54
src/pages/rsvp/admin.astro
Normal file
54
src/pages/rsvp/admin.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
import AdminLayout from "../../layouts/AdminLayout.astro";
|
||||
import RSVPManager from "../../components/RSVPManager.tsx";
|
||||
import SignIn from "../../components/SignIn.tsx";
|
||||
import SignOut from "../../components/SignOut.tsx";
|
||||
---
|
||||
|
||||
<AdminLayout title="RSVP Manager">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-center text-4xl">RSVP Manager</div>
|
||||
|
||||
<div id="auth-container">
|
||||
<SignIn client:load onSuccess={() => {}} requiredRole="admin" />
|
||||
</div>
|
||||
|
||||
<div id="manager-container" class="hidden">
|
||||
<RSVPManager client:load />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
|
||||
<script>
|
||||
const checkAndUpdateVisibility = (role: string | null) => {
|
||||
if (role === "admin") {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document
|
||||
.getElementById("manager-container")
|
||||
?.classList.remove("hidden");
|
||||
} else {
|
||||
document
|
||||
.getElementById("auth-container")
|
||||
?.classList.remove("hidden");
|
||||
document
|
||||
.getElementById("manager-container")
|
||||
?.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
// Check auth state on page load
|
||||
const isAuthenticated =
|
||||
sessionStorage.getItem("isAuthenticated") === "true";
|
||||
const role = sessionStorage.getItem("role");
|
||||
checkAndUpdateVisibility(role);
|
||||
|
||||
// Add event listener for custom event from SignIn component
|
||||
document.addEventListener("auth-success", ((event: CustomEvent) => {
|
||||
const newRole = event.detail?.role || sessionStorage.getItem("role");
|
||||
checkAndUpdateVisibility(newRole);
|
||||
}) as EventListener);
|
||||
</script>
|
Reference in New Issue
Block a user