diff options
| author | Anjana Vakil <contact@anjana.dev> | 2025-08-26 12:40:16 -0500 |
|---|---|---|
| committer | Anjana Vakil <contact@anjana.dev> | 2025-08-26 12:40:16 -0500 |
| commit | 1dc4f56425209d4ce1d7bb78ec8b5e7b5a755a82 (patch) | |
| tree | 58d06cd695ae17302daff7a87d9096f1d39ea54a /frontend/src | |
reset
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/components/Events.js | 106 | ||||
| -rw-r--r-- | frontend/src/components/Footer.js | 10 | ||||
| -rw-r--r-- | frontend/src/components/Forms.js | 67 | ||||
| -rw-r--r-- | frontend/src/components/Header.js | 33 | ||||
| -rw-r--r-- | frontend/src/components/Icons.js | 19 | ||||
| -rw-r--r-- | frontend/src/components/Main.js | 9 | ||||
| -rw-r--r-- | frontend/src/components/Modal.js | 80 | ||||
| -rw-r--r-- | frontend/src/icons/calendar.svg | 4 | ||||
| -rw-r--r-- | frontend/src/icons/heart.svg | 4 | ||||
| -rw-r--r-- | frontend/src/icons/pico.svg | 21 | ||||
| -rw-r--r-- | frontend/src/icons/sun-moon.svg | 4 | ||||
| -rw-r--r-- | frontend/src/icons/typescript.svg | 1 | ||||
| -rw-r--r-- | frontend/src/icons/vite.svg | 1 | ||||
| -rw-r--r-- | frontend/src/main.js | 26 | ||||
| -rw-r--r-- | frontend/src/style.css | 81 |
15 files changed, 466 insertions, 0 deletions
diff --git a/frontend/src/components/Events.js b/frontend/src/components/Events.js new file mode 100644 index 0000000..bca948a --- /dev/null +++ b/frontend/src/components/Events.js @@ -0,0 +1,106 @@ +import { Calendar } from './Icons.js'; + +const API_URL = '/api'; + +const loadEventsData = async () => { + try { + const response = await fetch(`${API_URL}/events`); + return response.json(); + } catch (e) { + console.error(e); + } +} + + +export const EventModal = (event) => { + const formId = `rsvp-form-${event.ID}`; + const modalId = `modal-event-${event.id}` + return `<dialog id="${modalId}"> + <article> + <header> + <button + aria-label="Close" + rel="prev" + data-target="${modalId}" + class="toggle-modal" + ></button> + <h3>RSVP to ${event.title}</h3> + </header> + <form id="${formId}" data-modal="${modalId}" + action="${API_URL}/events/${event.id}/rsvp" + method="POST" + > + <label for="rsvp-name">Name: + <input type="text" class="rsvp-name" name="name" required /> + </label> + <label for="rsvp-email">Email: + <input type="email" class="rsvp-email" name="email" required /> + </label> + + </form> + <footer> + <button + role="button" + class="toggle-modal cancel" + data-target="${modalId}" + >Cancel</button> + + <button id="submit-${formId}" role="button" form="${formId}" type="submit">Submit RSVP</button> + + </footer> + </article> + </dialog>` +} + +export const EventCard = (e) => { + const eventDate = new Date(e.date); + const isPast = eventDate < new Date(); + return ` +<article class="event" > +<header> + ${e.image_url && `<img src=${e.image_url} alt="${e.title} thumbnail" />`} +</header> + <main> + <h4>${e.title}</h4> + <p>${Calendar} ${eventDate.toLocaleDateString()}</p> + <p>Host: ${e.host?.name || `User ${e.host_id}`}</p> + + ${e.description && `<p>${e.description}</p>`} + </main> + <footer> + <span> + ${e.rsvps.length} + ${isPast ? 'went' : 'going'} + </span> + ${!isPast ? ` + <button role="button" data-target="modal-event-${e.id}" class="toggle-modal" + title="RSVP to ${e.title}" + > + RSVP + </button>`: ''} + </footer> + ${EventModal(e)} +</article> + ` +} + +export const EventsSection = (title, events) => { + return ` + <section class='events'> + <h2>${title} events </h2> + <div role = "group"> + ${events.map((e) => EventCard(e)).join('') || 'No events'} + </div> + </section>`; +} + +// IIFE to asynchronously load the Event data before exporting the component +// https://developer.mozilla.org/en-US/docs/Glossary/IIFE +export const Events = await (async () => { + const all = await loadEventsData(); + const past = all.filter((e) => (new Date(e.date) < new Date())); + const upcoming = all.filter((e) => (new Date(e.date) > new Date())); + return ` + ${EventsSection('Upcoming', upcoming)} + ${EventsSection('Past', past)} +`})()
\ No newline at end of file diff --git a/frontend/src/components/Footer.js b/frontend/src/components/Footer.js new file mode 100644 index 0000000..86f36da --- /dev/null +++ b/frontend/src/components/Footer.js @@ -0,0 +1,10 @@ +import * as Icons from './Icons.js'; + +const Footer = ` +<footer> +<p>Built with ${Icons.TypeScript} + ${Icons.Vite} + ${Icons.Pico} + ${Icons.Heart} at FrontendMasters</p> +<p>© 2024</p> +</footer> +`; + +export default Footer
\ No newline at end of file diff --git a/frontend/src/components/Forms.js b/frontend/src/components/Forms.js new file mode 100644 index 0000000..9a0c087 --- /dev/null +++ b/frontend/src/components/Forms.js @@ -0,0 +1,67 @@ +const API_URL = import.meta.env.VITE_API_URL; + +export const setupForm = (form) => { + if (!form) return; + + const eventId = form.id.replace('rsvp-form-', ''); + const url = `${API_URL}/events/${eventId}/rsvp`; + const btn = document.getElementById(`submit-${form.id}`); + + async function sendData(form) { + const formData = new FormData(form); + const encoded = new URLSearchParams(); + for (let [key, value] of formData.entries) { + encoded.append(key, value); + } + try { + const response = await fetch(url, { + method: "POST", + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: encoded, + }); + if (response.status === 201) { + form.dataset.submitted = 'success'; + if (btn) { + btn.textContent = "You're going!"; + btn.setAttribute('disabled', ''); + } + } else if (response.status === 200) { + form.dataset.submitted = 'duplicate'; + if (btn) { + btn.textContent = "You're already going!"; + btn.setAttribute('disabled', ''); + } + } + } catch (e) { + form.dataset.submitted = 'error'; + console.error(e); + } + } + + form.addEventListener("submit", (event) => { + event.preventDefault(); + sendData(form); + }); + + if (btn) { + const inputs = form.querySelectorAll('input.rsvp-email'); + for (let input of inputs) { + input.addEventListener('input', () => { + if (!btn.textContent.startsWith('Submit')) { + btn.removeAttribute('disabled'); + btn.textContent = 'Submit RSVP'; + } + }); + } + } +}; + +export const setupForms = () => { + const rsvpForms = document.querySelectorAll('form'); + if (!rsvpForms) return; + for (let form of rsvpForms) { + setupForm(form); + } +};
\ No newline at end of file diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js new file mode 100644 index 0000000..cd50ba6 --- /dev/null +++ b/frontend/src/components/Header.js @@ -0,0 +1,33 @@ +import { Theme as ThemeIcon } from './Icons'; + +const themeToggleId = 'theme'; + +const Header = ` +<header> + <hgroup> + <h1 class=".parkinsans">event me</h1> + <p>All the events you never knew you needed to attend!</p> + </hgroup> + <a href="#" role="toggle" id="${themeToggleId}" title="Toggle color scheme" > + ${ThemeIcon} + </a> +</header> +`; + +export function setupThemeToggle() { + const toggleDarkMode = () => { + const doc = document.documentElement; + const currentTheme = doc.getAttribute('data-theme'); + if (currentTheme === 'dark') { + doc.setAttribute('data-theme', 'lite'); + } else if (currentTheme === 'light') { + doc.setAttribute('data-theme', 'dark'); + } + } + const themeToggle = document.getElementById(themeToggleId); + themeToggle.addEventListener('click', toggleDarkMode); + +} + + +export default Header diff --git a/frontend/src/components/Icons.js b/frontend/src/components/Icons.js new file mode 100644 index 0000000..c2eb993 --- /dev/null +++ b/frontend/src/components/Icons.js @@ -0,0 +1,19 @@ +import heart from '../icons/heart.svg?raw'; +import sunMoon from '../icons/sun-moon.svg?raw'; +import typescript from '../icons/typescript.svg?raw'; +import vite from '../icons/vite.svg?raw'; +import pico from '../icons/pico.svg?raw'; +import calendar from '../icons/calendar.svg?raw'; + +// Export the raw SVG strings +export const Heart = heart; +export const Theme = sunMoon; +export const TypeScript = typescript; +export const Vite = vite; +export const Pico = pico; +export const Calendar = calendar; + + + + + diff --git a/frontend/src/components/Main.js b/frontend/src/components/Main.js new file mode 100644 index 0000000..275578b --- /dev/null +++ b/frontend/src/components/Main.js @@ -0,0 +1,9 @@ +import { Events } from './Events'; + +const Main = ` +<main class="container"> + ${Events} + </main> +`; + +export default Main;
\ No newline at end of file diff --git a/frontend/src/components/Modal.js b/frontend/src/components/Modal.js new file mode 100644 index 0000000..d72385b --- /dev/null +++ b/frontend/src/components/Modal.js @@ -0,0 +1,80 @@ +/* + * Modal + * + * Pico.css - https://picocss.com + * Copyright 2019-2024 - Licensed under MIT + */ + +// Config +const isOpenClass = "modal-is-open"; +const openingClass = "modal-is-opening"; +const closingClass = "modal-is-closing"; +const scrollbarWidthCssVar = "--pico-scrollbar-width"; +const animationDuration = 400; // ms +let visibleModal = null; + +// Toggle modal +const toggleModal = (event) => { + event.preventDefault(); + const modal = document.getElementById(event.currentTarget.dataset.target); + if (!modal) return; + modal && (modal.open ? closeModal(modal) : openModal(modal)); +}; + +// Open modal +const openModal = (modal) => { + const { documentElement: html } = document; + const scrollbarWidth = getScrollbarWidth(); + if (scrollbarWidth) { + html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`); + } + html.classList.add(isOpenClass, openingClass); + setTimeout(() => { + visibleModal = modal; + html.classList.remove(openingClass); + }, animationDuration); + modal.showModal(); +}; + +// Close modal +const closeModal = (modal) => { + visibleModal = null; + const { documentElement: html } = document; + html.classList.add(closingClass); + setTimeout(() => { + html.classList.remove(closingClass, isOpenClass); + html.style.removeProperty(scrollbarWidthCssVar); + modal.close(); + }, animationDuration); +}; + +// Close with a click outside +document.addEventListener("click", (event) => { + if (visibleModal === null) return; + const modalContent = visibleModal.querySelector("article"); + const isClickInside = modalContent.contains(event.target); + !isClickInside && closeModal(visibleModal); +}); + +// Close with Esc key +document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && visibleModal) { + closeModal(visibleModal); + } +}); + +// Get scrollbar width +const getScrollbarWidth = () => { + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + return scrollbarWidth; +}; + + +// Initialize event listeners to open/close event pages +export const setupModals = () => { + // Add open/close button handlers + const togglers = document.querySelectorAll('.toggle-modal'); + for (let el of togglers) { + el.addEventListener("click", toggleModal); + } +};
\ No newline at end of file diff --git a/frontend/src/icons/calendar.svg b/frontend/src/icons/calendar.svg new file mode 100644 index 0000000..0360850 --- /dev/null +++ b/frontend/src/icons/calendar.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path fill="currentColor" + d="M19 19H5V8h14m-3-7v2H8V1H6v2H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-1V1m-1 11h-5v5h5z" /> +</svg>
\ No newline at end of file diff --git a/frontend/src/icons/heart.svg b/frontend/src/icons/heart.svg new file mode 100644 index 0000000..ae83485 --- /dev/null +++ b/frontend/src/icons/heart.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path fill="currentColor" + d="m12 21.35l-1.45-1.32C5.4 15.36 2 12.27 2 8.5C2 5.41 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.41 22 8.5c0 3.77-3.4 6.86-8.55 11.53z" /> +</svg>
\ No newline at end of file diff --git a/frontend/src/icons/pico.svg b/frontend/src/icons/pico.svg new file mode 100644 index 0000000..2494250 --- /dev/null +++ b/frontend/src/icons/pico.svg @@ -0,0 +1,21 @@ +<svg fill="none" height="458" viewBox="0 0 1064 458" width="1064" xmlns="http://www.w3.org/2000/svg"> + <path + d="m993.708 161.083c-1.475.567-2.641 1.733-3.208 3.208l-21.822 56.738c-1.836 4.774-8.59 4.774-10.426 0l-21.823-56.738c-.567-1.475-1.733-2.641-3.208-3.208l-56.738-21.823c-4.774-1.836-4.774-8.59 0-10.426l56.738-21.822c1.475-.567 2.641-1.733 3.208-3.208l21.823-56.7383c1.836-4.7737 8.59-4.7737 10.426 0l21.822 56.7383c.567 1.475 1.733 2.641 3.208 3.208l56.742 21.822c4.77 1.836 4.77 8.59 0 10.426z" + fill="#ffbf00" /> + <path + d="m834.63 86.9817c-1.836 4.7738-8.59 4.7738-10.426 0l-7.859-20.4337c-.567-1.475-1.733-2.6407-3.208-3.208l-20.433-7.8592c-4.774-1.836-4.774-8.59 0-10.426l20.433-7.8591c1.475-.5674 2.641-1.733 3.208-3.2081l7.859-20.4337c1.836-4.77377 8.59-4.77375 10.426 0l7.86 20.4337c.567 1.4751 1.733 2.6407 3.208 3.2081l20.433 7.8591c4.774 1.836 4.774 8.59 0 10.426l-20.433 7.8592c-1.475.5673-2.641 1.733-3.208 3.208z" + fill="#ff9500" /> + <path + d="m879.209 230.899c-1.475.568-2.64 1.733-3.208 3.208l-7.859 20.434c-1.836 4.774-8.59 4.774-10.426 0l-7.859-20.434c-.567-1.475-1.733-2.64-3.208-3.208l-20.434-7.859c-4.773-1.836-4.773-8.59 0-10.426l20.434-7.859c1.475-.567 2.641-1.733 3.208-3.208l7.859-20.434c1.836-4.774 8.59-4.774 10.426 0l7.859 20.434c.568 1.475 1.733 2.641 3.208 3.208l20.434 7.859c4.774 1.836 4.774 8.59 0 10.426z" + fill="#ff9500" /> + <g fill="currentcolor"> + <path + d="m0 457.995v-282.83h59.1396l3.6208 37.172v245.658zm119.889-75.96c-16.629 0-30.5761-4.175-41.8408-12.525-11.2647-8.62-19.7132-20.876-25.3455-36.768-5.6324-15.892-8.4485-34.748-8.4485-56.566 0-22.088 2.8161-40.943 8.4485-56.566 5.6323-15.893 14.0808-28.014 25.3455-36.364 11.2647-8.62 25.2118-12.93 41.8408-12.93 18.774 0 35.001 4.31 48.679 12.93 13.679 8.35 24.139 20.471 31.38 36.364 7.242 15.623 10.863 34.478 10.863 56.566 0 21.818-3.621 40.674-10.863 56.566-7.241 15.892-17.701 28.148-31.38 36.768-13.678 8.35-29.905 12.525-48.679 12.525zm-16.093-58.182c8.046 0 15.154-2.02 21.323-6.061 6.168-4.04 11.13-9.562 14.885-16.566 3.755-7.272 5.632-15.623 5.632-25.05 0-9.428-1.743-17.643-5.23-24.647-3.486-7.273-8.448-12.929-14.885-16.97-6.169-4.04-13.276-6.06-21.323-6.06-8.0458 0-15.2874 2.02-21.7244 6.06-6.1687 4.041-10.9964 9.697-14.4831 16.97s-5.2301 15.488-5.2301 24.647c0 9.427 1.7434 17.778 5.2301 25.05 3.4867 7.004 8.3144 12.526 14.4831 16.566 6.1688 4.041 13.2763 6.061 21.3224 6.061z" /> + <path + d="m245.196 377.187v-202.022h62.76v202.022zm31.38-227.881c-9.387 0-17.702-3.502-24.943-10.505-6.974-7.273-10.46-15.623-10.46-25.051 0-9.966 3.486-18.3165 10.46-25.0505 7.241-7.0035 15.556-10.5052 24.943-10.5052 9.655 0 17.97 3.5017 24.943 10.5052 6.973 6.734 10.46 15.0845 10.46 25.0505 0 9.428-3.487 17.778-10.46 25.051-6.973 7.003-15.288 10.505-24.943 10.505z" /> + <path + d="m452.936 382.035c-21.457 0-40.634-4.444-57.531-13.333-16.629-9.159-29.637-21.684-39.024-37.576-9.387-16.162-14.081-34.479-14.081-54.95 0-20.741 4.694-39.058 14.081-54.95 9.387-15.893 22.261-28.283 38.622-37.172 16.629-9.159 35.671-13.738 57.128-13.738 20.652 0 39.56 5.253 56.726 15.758 17.165 10.505 29.502 25.724 37.012 45.657l-59.542 20.202c-2.95-6.465-7.912-11.717-14.885-15.758-6.705-4.309-14.215-6.464-22.53-6.464-8.314 0-15.69 2.02-22.127 6.06-6.168 3.771-11.13 9.159-14.885 16.162-3.487 7.004-5.23 15.084-5.23 24.243 0 9.158 1.743 17.239 5.23 24.242 3.755 6.734 8.851 12.122 15.288 16.162 6.705 4.041 14.215 6.061 22.529 6.061 8.315 0 15.824-2.155 22.529-6.465 6.706-4.31 11.667-9.966 14.886-16.97l59.542 20.202c-7.778 20.203-20.25 35.691-37.415 46.465-16.897 10.775-35.672 16.162-56.323 16.162z" /> + <path + d="m672.537 382.035c-21.188 0-39.962-4.444-56.323-13.333-16.093-9.159-28.832-21.684-38.22-37.576-9.119-15.893-13.678-34.209-13.678-54.95s4.559-39.058 13.678-54.95c9.119-15.893 21.725-28.283 37.818-37.172 16.36-9.159 34.866-13.738 55.518-13.738 21.189 0 39.829 4.579 55.922 13.738 16.36 8.889 29.1 21.279 38.219 37.172 9.119 15.892 13.679 34.209 13.679 54.95s-4.56 39.057-13.679 54.95c-9.119 15.892-21.725 28.417-37.817 37.576-15.824 8.889-34.196 13.333-55.117 13.333zm-.402-59.798c8.314 0 15.422-1.886 21.322-5.657 6.169-4.04 10.997-9.428 14.484-16.162 3.486-7.003 5.23-15.084 5.23-24.242 0-9.159-1.878-17.105-5.633-23.839-3.486-7.003-8.314-12.39-14.483-16.162-6.169-4.04-13.276-6.06-21.322-6.06s-15.154 2.02-21.323 6.06c-6.168 3.772-10.996 9.159-14.483 16.162-3.487 6.734-5.23 14.68-5.23 23.839 0 8.889 1.743 16.835 5.23 23.838 3.487 7.004 8.315 12.526 14.483 16.566 6.437 3.771 13.679 5.657 21.725 5.657z" /> + </g> +</svg>
\ No newline at end of file diff --git a/frontend/src/icons/sun-moon.svg b/frontend/src/icons/sun-moon.svg new file mode 100644 index 0000000..fb0a165 --- /dev/null +++ b/frontend/src/icons/sun-moon.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path fill="currentcolor" + d="M7.5 2c-1.79 1.15-3 3.18-3 5.5s1.21 4.35 3.03 5.5C4.46 13 2 10.54 2 7.5A5.5 5.5 0 0 1 7.5 2m11.57 1.5l1.43 1.43L4.93 20.5L3.5 19.07zm-6.18 2.43L11.41 5L9.97 6l.42-1.7L9 3.24l1.75-.12l.58-1.65L12 3.1l1.73.03l-1.35 1.13zm-3.3 3.61l-1.16-.73l-1.12.78l.34-1.32l-1.09-.83l1.36-.09l.45-1.29l.51 1.27l1.36.03l-1.05.87zM19 13.5a5.5 5.5 0 0 1-5.5 5.5c-1.22 0-2.35-.4-3.26-1.07l7.69-7.69c.67.91 1.07 2.04 1.07 3.26m-4.4 6.58l2.77-1.15l-.24 3.35zm4.33-2.7l1.15-2.77l2.2 2.54zm1.15-4.96l-1.14-2.78l3.34.24zM9.63 18.93l2.77 1.15l-2.53 2.19z" /> +</svg>
\ No newline at end of file diff --git a/frontend/src/icons/typescript.svg b/frontend/src/icons/typescript.svg new file mode 100644 index 0000000..d91c910 --- /dev/null +++ b/frontend/src/icons/typescript.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
\ No newline at end of file diff --git a/frontend/src/icons/vite.svg b/frontend/src/icons/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/src/icons/vite.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..cf91fa0 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,26 @@ +import './style.css' +import Header, { setupThemeToggle } from './components/Header'; +import Main from './components/Main'; +import Footer from './components/Footer'; +import { setupModals } from './components/Modal'; +import { setupForms } from './components/Forms'; + + +// Quick and dirty - not for production! +const render = (html) => { + const app = document.querySelector('#app'); + app.innerHTML = html; + setupThemeToggle(); + setupModals(); + setupForms(); +} + + +render(` + ${Header} + ${Main} + ${Footer} +`); + + + diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..c7ca91e --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,81 @@ +/* Layout */ +body>header, +footer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +nav[aria-label="breadcrumb"] { + --pico-nav-breadcrumb-divider: '/'; +} + +h1, +h2, +h3, +h4 { + font-family: 'Parkinsans'; +} + +header h1, +header h2, +header h3, +header h4 { + color: var(--pico-primary); +} + +header svg { + color: var(--pico-contrast); +} + +body>header { + padding-bottom: 0px; +} + +svg { + color: inherit; +} + + +footer svg { + height: 1em; + width: auto; +} + +section.events [role="group"] { + display: grid; + max-width: 100%; + grid-template-columns: repeat(3, 1fr); + gap: .5em; +} + +article.event { + margin: 0px; + display: flex; + flex-direction: column; + justify-content: space-between; + border-radius: var(--pico-border-radius); +} + + +article.event main { + display: flex; + flex-direction: column; + justify-content: start; + height: 100%; +} + +article.event img { + object-fit: cover; + width: 100%; + display: block; + padding: 0px; + margin: 0px; + border-radius: inherit; +} + +article.event h4 { + color: var(--pico-primary); + min-height: 2em; +}
\ No newline at end of file |
