Small UI changes

This commit is contained in:
2025-02-24 21:20:43 -06:00
parent df438ec702
commit 7aff496314
14 changed files with 412 additions and 19 deletions

View 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>

View File

@ -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"

View 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;

View 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>

View File

@ -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>

View File

@ -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">
<AuthLayout title={title}>
<slot />
</AuthLayout>
<body>
<Navigation />
<div class="flex items-center justify-center min-h-screen">
<AuthLayout title={title}>
<slot />
</AuthLayout>
</div>
</body>
</html>

View File

@ -9,5 +9,6 @@ export interface RegistryItem {
export interface RSVPItem {
name: string;
dietaryRestrictions: string;
notes: string;
timestamp: string;
}

View File

@ -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
View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>