Redesign some components of the council page

This commit is contained in:
2026-02-03 10:17:54 -05:00
parent a3908745d7
commit 7ad2bfba10
5 changed files with 186 additions and 95 deletions

View File

@@ -16,7 +16,7 @@
} }
</script> </script>
<div class="relative size-26 overflow-hidden rounded-full md:size-32"> <div class="relative size-24 overflow-hidden rounded-full shadow-md ring-2 ring-ecsess-500/20 transition group-hover:ring-ecsess-400/40 sm:size-28 md:size-32 lg:size-36">
{#if src && !imageError} {#if src && !imageError}
<img <img
{src} {src}

View File

@@ -4,14 +4,17 @@
import CouncilAvatar from 'components/council/CouncilAvatar.svelte'; import CouncilAvatar from 'components/council/CouncilAvatar.svelte';
</script> </script>
<div class="text-ecsess-100 flex max-w-md items-center gap-6 p-3"> <article
<!-- Profile picture --> class="group flex w-full min-w-0 items-center gap-5 rounded-xl border border-ecsess-600/20 bg-ecsess-800/40 p-5 text-ecsess-100 shadow-lg transition hover:border-ecsess-500/30 hover:bg-ecsess-800/60 hover:shadow-xl focus-within:ring-2 focus-within:ring-ecsess-400/50 sm:gap-6 sm:p-6 md:p-6 lg:gap-8 lg:p-8"
<div> >
<div class="shrink-0">
<CouncilAvatar {name} src={image} /> <CouncilAvatar {name} src={image} />
</div> </div>
<div class="text-left"> <div class="min-w-0 flex-1 text-left">
<div class="text-xl font-bold md:text-2xl">{name}</div> <h3 class="text-lg font-bold leading-tight sm:text-xl md:text-2xl lg:text-3xl">{name}</h3>
<div class="text-ecsess-200 mb-2 text-sm italic md:text-base">{position}</div> <p class="text-ecsess-200 mt-1 text-sm italic sm:text-base lg:text-lg">{position}</p>
<div class="mt-4 lg:mt-5">
<Button onclick={onViewProfile}>View Profile</Button> <Button onclick={onViewProfile}>View Profile</Button>
</div> </div>
</div> </div>
</article>

View File

@@ -1,42 +1,78 @@
<script> <script>
let { name, position, email, positionDescription, yearProgram, image } = $props(); let {
import { Mail } from '@lucide/svelte'; name,
position,
email,
positionDescription,
yearProgram,
image,
onClose,
id = 'popup-title'
} = $props();
import { Mail, X } from '@lucide/svelte';
import placeholder from 'assets/placeholderAvatar.png'; import placeholder from 'assets/placeholderAvatar.png';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
</script> </script>
<div <div
class="text-ecsess-800 from-ecsess-50 to-ecsess-300 m-4 flex class="relative flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-ecsess-600/30 bg-gradient-to-br from-ecsess-50 to-ecsess-200/90 shadow-2xl shadow-black/30 text-ecsess-800"
h-70 max-w-142 items-center gap-3 rounded-md
border-transparent bg-transparent bg-linear-to-br p-6"
transition:slide transition:slide
role="article"
> >
<!-- AVATAR --> <!-- Close button: visible and tappable on all screens -->
<div class="avatar"> <button
<div class="size-36 justify-center place-self-center-safe overflow-hidden rounded-md shadow-md"> type="button"
<img src={image || placeholder} alt={name} class="h-full w-full object-cover" /> onclick={onClose}
class="absolute right-3 top-3 z-10 flex size-10 items-center justify-center rounded-full bg-ecsess-800/90 text-ecsess-100 shadow-lg transition hover:bg-ecsess-700 focus:outline-none focus:ring-2 focus:ring-ecsess-400 focus:ring-offset-2"
aria-label="Close profile"
>
<X class="size-5" />
</button>
<div class="flex flex-1 flex-col overflow-y-auto sm:flex-row">
<!-- Avatar block -->
<div
class="flex shrink-0 flex-col items-center gap-2 border-b border-ecsess-300/50 bg-ecsess-100/50 p-6 sm:border-b-0 sm:border-r sm:min-w-[180px]"
>
<div class="size-28 overflow-hidden rounded-xl shadow-md sm:size-32">
<img
src={image || placeholder}
alt={name}
class="h-full w-full object-cover"
/>
</div> </div>
<span class="yearProgram justify-center text-sm"> {yearProgram} </span> <span class="text-center text-sm font-medium text-ecsess-700">{yearProgram}</span>
</div> </div>
<!-- CONTENT -->
<div class="content"> <!-- Content block: more space for long text -->
<div class="my-2"> <div class="flex min-w-0 flex-1 flex-col p-6 pt-5 sm:p-8 sm:pt-6">
<p class="text-2xl font-bold lg:text-3xl">{name}</p> <div class="mb-4">
<p class="text-base italic">{position}</p> <h2 {id} class="text-xl font-bold leading-tight sm:text-2xl lg:text-3xl">{name}</h2>
<p class="mt-0.5 text-base italic text-ecsess-700 sm:text-lg">{position}</p>
</div> </div>
<hr class="hr border-ecsess-600 border border-dashed" />
<div class="my-2 text-left"> <hr class="border-ecsess-400/60 my-3 border-t border-dashed" />
<ul>
<li> <div class="space-y-4 text-left">
<p class="py-2 text-base">{positionDescription}</p> {#if positionDescription}
</li> <div class="max-w-prose">
<li class="flex flex-row place-items-center gap-2"> <p class="text-sm leading-relaxed text-ecsess-800 sm:text-base">
<Mail class="size-4" /> {positionDescription}
<p>
Email: <a href="mailto:{email}" class="py-2 text-base underline">{email}</a>
</p> </p>
</li> </div>
</ul> {/if}
{#if email}
<div class="flex flex-wrap items-center gap-2">
<Mail class="size-4 shrink-0 text-ecsess-600" aria-hidden="true" />
<a
href="mailto:{email}"
class="break-all text-sm font-medium text-ecsess-700 underline decoration-ecsess-500 underline-offset-2 transition hover:text-ecsess-800 hover:decoration-ecsess-600 sm:text-base"
>
{email}
</a>
</div>
{/if}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -11,20 +11,23 @@
to = '', to = '',
via = '', via = '',
direction = 'to-b', // to bottom direction = 'to-b', // to bottom
black = false black = false,
contentStart = false
} = $props(); } = $props();
const base = const base =
'mx-auto flex min-h-[90vh] flex-col items-center justify-center gap-4 p-4 text-center text-ecsess-100'; 'mx-auto flex min-h-[90vh] flex-col items-center gap-4 p-4 text-center text-ecsess-100';
const justifyClass = contentStart ? 'justify-start' : 'justify-center';
// Compute classes: prefer gradient when from/to provided; otherwise fallback to previous behavior // Compute classes: prefer gradient when from/to provided; otherwise fallback to previous behavior
let tailwindClasses = $state(base); let tailwindClasses = $state(base);
$effect(() => { $effect(() => {
const withJustify = `${base} ${justifyClass}`;
if (from && to) { if (from && to) {
tailwindClasses = `${base} bg-gradient-${direction} ${from} ${to} ${via}`; tailwindClasses = `${withJustify} bg-gradient-${direction} ${from} ${to} ${via}`;
} else { } else {
tailwindClasses = base + (black ? ' bg-ecsess-black' : ' bg-ecsess-800'); tailwindClasses = withJustify + (black ? ' bg-ecsess-black' : ' bg-ecsess-800');
} }
}); });
</script> </script>

View File

@@ -4,39 +4,69 @@
import CardCouncil from 'components/council/CouncilCard.svelte'; import CardCouncil from 'components/council/CouncilCard.svelte';
import type { CouncilMember } from '$lib/schemas'; import type { CouncilMember } from '$lib/schemas';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Button from 'components/Button.svelte';
import SeoMetaTags from 'components/layout/SeoMetaTags.svelte'; import SeoMetaTags from 'components/layout/SeoMetaTags.svelte';
import { onMount } from 'svelte';
import { tick } from 'svelte';
let { data } = $props(); let { data } = $props();
// Get members by 3 main categories: const years = ['U4', 'U3', 'U2', 'U1', 'U0'];
// - Preseident
// - VPs + Equity and Mental Health Officer
// - UReps
let president: CouncilMember = data.members.filter((member: CouncilMember) =>
member.position.includes('President')
)[0];
let vps: CouncilMember[] = data.members.filter( // Get members by 3 main categories (reactive from data)
(member: CouncilMember) => member.position.includes('VP') || member.position.includes('Equity') let president = $derived(
data.members.filter((member: CouncilMember) => member.position.includes('President'))[0]
); );
let vps = $derived(
let ureps: CouncilMember[] = data.members.filter((member: CouncilMember) => data.members.filter(
(member: CouncilMember) =>
member.position.includes('VP') || member.position.includes('Equity')
)
);
let ureps = $derived(
(() => {
const list = data.members.filter((member: CouncilMember) =>
member.position.includes('Representative') member.position.includes('Representative')
); );
return [...list].sort((a, b) => {
let years = ['U4', 'U3', 'U2', 'U1', 'U0'];
ureps.sort((a, b) => {
const aYear = years.findIndex((year) => a.position.includes(year)); const aYear = years.findIndex((year) => a.position.includes(year));
const bYear = years.findIndex((year) => b.position.includes(year)); const bYear = years.findIndex((year) => b.position.includes(year));
return aYear - bYear; return aYear - bYear;
}); });
})()
);
let selectedMember = $state<CouncilMember | null>(null); let selectedMember = $state<CouncilMember | null>(null);
let modalRef = $state<HTMLDivElement | null>(null);
function handleViewProfile(member: CouncilMember) { function handleViewProfile(member: CouncilMember) {
selectedMember = member; selectedMember = member;
} }
function closeModal() {
selectedMember = null;
}
function onBackdropKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') closeModal();
}
onMount(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') closeModal();
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
});
$effect(() => {
if (selectedMember) {
document.body.style.overflow = 'hidden';
tick().then(() => modalRef?.focus());
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
});
</script> </script>
<SeoMetaTags <SeoMetaTags
@@ -45,36 +75,41 @@
canonical={data.canonical} canonical={data.canonical}
/> />
<Section from="from-ecsess-black" to="to-ecsess-black" via="via-ecsess-800" direction="to-b"> <Section from="from-ecsess-black" to="to-ecsess-black" via="via-ecsess-800" direction="to-b" contentStart>
<div class="flex flex-col place-items-center"> <div class="flex w-full max-w-4xl flex-col items-center gap-6 py-6 text-left lg:max-w-6xl">
<p class="page-title">Meet the council!</p> <p class="page-title text-balance">Meet the council!</p>
<img <img
src={data.councilGoofyPic.url} src={data.councilGoofyPic.url}
alt="ECSESS Council, but we are goofy" alt="ECSESS Council, but we are goofy"
class="ring-ecsess-400 shadow-ecsess-black hover:ring-ecsess-300 mb-8 rounded-md shadow-2xl ring-4 transition-all lg:w-[90%]" class="mb-8 rounded-xl shadow-2xl ring-2 ring-ecsess-400/50 shadow-black/40 transition-all hover:ring-ecsess-300/60 hover:shadow-ecsess-900/30 lg:w-[90%]"
transition:fly transition:fly
/> />
</div> </div>
<h1 class="border-b-ecsess-300 w-full border-b-2 lg:w-1/2">Our Student Council!</h1> <h1 class="border-b-ecsess-300/80 w-full max-w-6xl border-b-2 pb-3 text-center font-semibold tracking-tight">
Our Student Council
</h1>
<div> {#if president}
<div class="w-full max-w-2xl lg:max-w-3xl">
<CardCouncil <CardCouncil
name={president.name} name={president.name}
position={president.position} position={president.position}
image={president.image} image={president.image}
onViewProfile={() => handleViewProfile(president!)} onViewProfile={() => handleViewProfile(president)}
/> />
</div> </div>
{/if}
<div class="grid place-items-center"> <div class="grid w-full max-w-6xl place-items-center gap-6">
<h2 <h2
class="border-b-ecsess-300 w-full place-self-center-safe border-b-2 border-dashed md:w-1/2 lg:w-1/3" class="border-b-ecsess-300/70 w-full border-b-2 border-dashed pb-2 text-center text-lg font-semibold tracking-tight"
> >
Vice Presidents Vice Presidents
</h2> </h2>
<div class="grid gap-2 py-8 md:grid-cols-2 lg:grid-cols-3"> <div
class="grid w-full grid-cols-1 gap-5 py-4 sm:gap-6 md:grid-cols-2 md:gap-6 lg:gap-8 xl:gap-10"
>
{#each vps as vp} {#each vps as vp}
<CardCouncil <CardCouncil
name={vp.name} name={vp.name}
@@ -85,11 +120,13 @@
{/each} {/each}
</div> </div>
<h2 <h2
class="border-b-ecsess-300 w-full place-self-center-safe border-b-2 border-dashed md:w-1/2 lg:w-1/3" class="border-b-ecsess-300/70 w-full border-b-2 border-dashed pb-2 text-center text-lg font-semibold tracking-tight"
> >
Year Representative Year Representatives
</h2> </h2>
<div class="grid gap-2 py-8 md:grid-cols-2 lg:grid-cols-3"> <div
class="grid w-full grid-cols-1 gap-5 py-4 sm:gap-6 md:grid-cols-2 md:gap-6 lg:gap-8 xl:gap-10"
>
{#each ureps as urep} {#each ureps as urep}
<CardCouncil <CardCouncil
name={urep.name} name={urep.name}
@@ -100,18 +137,30 @@
{/each} {/each}
</div> </div>
</div> </div>
{#if selectedMember} {#if selectedMember}
<div class="fixed inset-0 z-10 flex flex-col items-center justify-center bg-black/70"> <!-- Modal: backdrop with blur, click-outside and Escape to close -->
<div
bind:this={modalRef}
tabindex="-1"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm outline-none overflow-y-auto"
role="dialog"
aria-modal="true"
aria-labelledby="popup-title"
onclick={(e) => e.target === e.currentTarget && closeModal()}
onkeydown={onBackdropKeydown}
>
<div class="relative flex max-h-[90vh] w-full max-w-4xl flex-col items-center justify-center overflow-hidden">
<CouncilCardPopUp <CouncilCardPopUp
id="popup-title"
name={selectedMember.name} name={selectedMember.name}
position={selectedMember.position} position={selectedMember.position}
email={selectedMember.email} email={selectedMember.email}
positionDescription={selectedMember.positionDescription} positionDescription={selectedMember.positionDescription}
yearProgram={selectedMember.yearProgram} yearProgram={selectedMember.yearProgram}
image={selectedMember.image} image={selectedMember.image}
onClose={closeModal}
/> />
<div transition:fly>
<Button onclick={() => (selectedMember = null)}>Close</Button>
</div> </div>
</div> </div>
{/if} {/if}