Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,44 @@
/* Light shades - for backgrounds and cards */
--color-ecsess-50: #e8ffd9;
--color-ecsess-100: #cce7ba;
--color-ecsess-150: #bae9a5;
--color-ecsess-200: #a9d0a0;

/* Mid-light shades - for borders and hover states */
--color-ecsess-250: #9cc295;
--color-ecsess-300: #8fb98a;
--color-ecsess-350: #7daa7a;
--color-ecsess-400: #6a9a6a;

/* Mid shades - for accents and interactive elements */
--color-ecsess-450: #62925a;
--color-ecsess-500: #5a8b5a;
--color-ecsess-550: #4c7a4f;
--color-ecsess-600: #3f6a3f;
--color-ecsess-650: #306032;

/* Mid-dark shades - for text on light backgrounds */
--color-ecsess-700: #2d5a2d;
--color-ecsess-700: #2f4d29;
--color-ecsess-750: #1c4a1e;
--color-ecsess-800: #0a3d2a;

/* Dark shades - for text and backgrounds */
--color-ecsess-850: #083525;
--color-ecsess-900: #062c20;
--color-ecsess-950: #031c15;

/* Black variants for UI elements */
--color-ecsess-black: #1f1f1f;
--color-ecsess-black-hover: #161917;

--animate-wiggle: wiggle 0.3s ease-in-out infinite;
--animate-wiggle: wiggle 0.5s ease-in-out 1;
@keyframes wiggle {
0%,
100% {
transform: rotateY(-3deg);
transform: rotateY(-2.4deg);
}
50% {
transform: rotateY(3deg);
transform: rotateY(2.4deg);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/RichText.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
let { value } = $props();
</script>

<div class="flex flex-col justify-center-safe">
<div class="typography flex flex-col justify-center-safe">
<PortableText {value} />
</div>
157 changes: 71 additions & 86 deletions src/components/homepage/AffiliatedGroups.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { Globe, Instagram, Wrench, Zap, CodeXml, Podcast } from '@lucide/svelte';
import { Globe, Instagram, Wrench, Users, CodeXml, Cpu } from '@lucide/svelte';

// All icons from @lucide/svelte share the same component type; reuse one for typing
type IconComponent = typeof Wrench;
Expand All @@ -17,16 +17,11 @@
{
name: 'Code.Jam()',
description:
"McGill Engineering's largest annual hackathon. A 48-hour programming competition where students create innovative solutions to real-world problems.",
"McGill Engineering's largest annual hackathon, a 36-hour programming competition where students create innovative projects!",
website: 'https://codejam.mcgilleus.ca/',
instagram: 'https://www.instagram.com/mcgillcodejam/',
icon: CodeXml,
features: [
'Biggest Hackathon in Engineering',
'Great prizes',
'Cool swags and merch',
'Networking opportunities'
]
features: ['Biggest Hackathon in Engineering', 'Great prizes', 'Networking opportunities']
},
{
name: 'The Factory',
Expand All @@ -35,122 +30,112 @@
website: 'https://factory.mcgilleus.ca/',
instagram: 'https://www.instagram.com/thefactory_mcgill/',
icon: Wrench,
features: ['Student-run Lab Space', '3D Printing', 'Equipment Rental', 'Hardware Workshops']
features: ['Student-run Lab Space', '3D Printing', 'Hardware Workshops']
},
{
name: 'ECSESSBits',
description:
'First Year Council of the McGill Electrical, Computer, Software Engineering Student Society.',
website: '',
instagram: 'https://www.instagram.com/ecsessbits/',
icon: Users,
features: ['First Year Council', 'Fun Events', 'Study Sessions']
},
{
name: 'IEEE McGill',
description:
'One of the largest IEEE student branches in Eastern Canada, offering professional development, networking, and industry connections.',
website: 'https://ieee.mcgilleus.ca/',
instagram: 'https://www.instagram.com/ieeemcgill/',
icon: Zap,
features: [
'Technical Talks',
'Arduino Workshops',
'IEEEXtreme Competition',
'Networking Events'
]
icon: Cpu,
features: ['Technical Talks', 'Arduino Workshops', 'Networking Events']
}
];
</script>

<div class="container mx-auto px-4">
<!-- Section Header -->
<div class="mb-12 text-center">
<h2 id="affiliated-clubs-title" class="text-ecsess-100 mb-4 text-4xl font-bold md:text-5xl">
<div class="my-12 text-center">
<h2 id="affiliated-clubs-title" class="text-ecsess-100 mb-2 text-4xl font-bold md:text-5xl">
Subcommittees & Affiliated Groups
</h2>
<p class="text-ecsess-200 mx-auto max-w-2xl text-lg">
<p class="text-ecsess-200 mx-auto max-w-2xl text-base">
Explore opportunities to enhance your skills, build innovative projects, and connect with the
engineering community through our subcommittees and affiliated groups.
</p>
</div>

<!-- Clubs Grid -->
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
<!-- Clubs Grid: 2x2 on large screens -->
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
{#each groups as group, i (group.name)}
{@const Icon = group.icon}
<article
class="group bg-ecsess-950 shadow-ecsess-800 relative flex flex-col overflow-hidden rounded-2xl shadow-xl transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
class="bg-ecsess-950 border-ecsess-800 flex flex-col overflow-hidden rounded-lg border text-left"
aria-labelledby={`group-${i}-title`}
>
<!-- Decorative gradient bar -->
<div class="from-ecsess-400 via-ecsess-500 to-ecsess-600 h-2 bg-gradient-to-r"></div>

<div class="flex flex-1 flex-col p-6">
<!-- Icon and Name -->
<div class="mb-4 flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div
class="group-hover:bg-ecsess-500 bg-ecsess-800 flex h-14 w-14 items-center justify-center rounded-xl shadow-md transition-all duration-300 group-hover:scale-110"
>
<Icon
class="text-ecsess-300 h-7 w-7 transition-colors group-hover:text-white"
strokeWidth={2.5}
aria-hidden="true"
focusable="false"
/>
</div>
<h3 id={`group-${i}-title`} class="text-ecsess-50 text-2xl font-bold">
{group.name}
</h3>
</div>
<!-- Action Buttons -->
<div class="flex gap-2">
<a
href={group.website}
target="_blank"
rel="noopener noreferrer external"
aria-label={`Visit ${group.name} website`}
class="bg-ecsess-800 hover:bg-ecsess-500 text-ecsess-100 hover:text-ecsess-50 flex h-10 w-10 items-center justify-center rounded-lg shadow-md transition-all hover:shadow-lg active:scale-95"
>
<Globe class="h-5 w-5" strokeWidth={2.5} aria-hidden="true" focusable="false" />
</a>
{#if group.instagram}
<a
href={group.instagram}
target="_blank"
rel="noopener noreferrer external"
aria-label={`Follow ${group.name} on Instagram`}
class="bg-ecsess-800 hover:bg-ecsess-500 text-ecsess-100 hover:text-ecsess-50 flex h-10 w-10 items-center justify-center rounded-lg shadow-md transition-all hover:shadow-lg active:scale-95"
>
<Instagram
class="h-5 w-5"
strokeWidth={2.5}
aria-hidden="true"
focusable="false"
/>
</a>
{/if}
<div class="flex flex-1 flex-col p-7 md:p-8">
<!-- Header: icon + name -->
<header class="mb-5 flex items-center justify-start gap-4">
<div
class="bg-ecsess-800 flex h-14 w-14 shrink-0 items-center justify-center rounded-xl"
>
<Icon
class="text-ecsess-300 size-7"
strokeWidth={2.5}
aria-hidden="true"
focusable="false"
/>
</div>
</div>
<h3 id={`group-${i}-title`} class="text-ecsess-50 text-2xl font-bold">
{group.name}
</h3>
</header>

<!-- Description -->
<p class="text-ecsess-100 mb-6 flex-1 text-base leading-relaxed">
<p class="text-ecsess-200 mb-5 text-base leading-relaxed md:text-lg">
{group.description}
</p>

<!-- Features -->
<ul class="mb-6 space-y-2" role="list">
<ul class="mb-5 list-none space-y-2 ps-0 text-base md:text-lg" role="list">
{#each group.features as feature (feature)}
<li class="flex items-center gap-2 pl-3">
<div class="bg-ecsess-500 h-1.5 w-1.5 rounded-full" aria-hidden="true"></div>
<span class="text-ecsess-100 text-base font-semibold">
{feature}
</span>
<li class="flex items-center gap-2">
<span class="bg-ecsess-500 h-1.5 w-1.5 shrink-0 rounded-full" aria-hidden="true"
></span>
<span class="text-ecsess-100 font-medium">{feature}</span>
</li>
{/each}
</ul>

<!-- Links -->
<div class="border-ecsess-800 mt-auto flex flex-wrap items-center gap-3 border-t pt-5">
{#if group.instagram}
<a
href={group.instagram}
target="_blank"
rel="noopener noreferrer external"
aria-label={`Follow ${group.name} on Instagram`}
class="text-ecsess-300 hover:text-ecsess-100 border-ecsess-700 bg-ecsess-900/50 hover:bg-ecsess-800/80 inline-flex items-center gap-2 rounded-md border px-4 py-2 text-base"
>
<Instagram class="size-5" strokeWidth={2.5} aria-hidden="true" focusable="false" />
<span>Instagram</span>
</a>
{/if}
{#if group.website}
<a
href={group.website}
target="_blank"
rel="noopener noreferrer external"
aria-label={`Visit ${group.name} website`}
class="text-ecsess-300 hover:text-ecsess-100 border-ecsess-700 bg-ecsess-900/50 hover:bg-ecsess-800/80 inline-flex items-center gap-2 rounded-md border px-4 py-2 text-base"
>
<Globe class="size-5" strokeWidth={2.5} aria-hidden="true" focusable="false" />
<span>Website</span>
</a>
{/if}
</div>
</div>
</article>
{/each}
</div>

<!-- Bottom CTA -->
<div class="mt-12 text-center">
<p class="text-ecsess-300 text-sm">
Want to get involved? Visit their websites and social media pages to learn about upcoming
events and how to join!
</p>
</div>
</div>
47 changes: 47 additions & 0 deletions src/components/homepage/Sponsors.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import type { Sponsors } from '$lib/schemas';
import Link from 'components/Link.svelte';
import Button from 'components/Button.svelte';

let { sponsors } = $props<{ sponsors: Sponsors[] }>();
</script>

<div class="container mx-auto px-4">
<!-- Section Header -->
<div class="my-12 text-center">
<h2 id="sponsors-title" class="text-ecsess-100 mb-2 text-4xl font-bold md:text-5xl">
Our Sponsors
</h2>
<p class="text-ecsess-200 mx-auto max-w-2xl text-base">
We're grateful to our sponsors for their continued support of ECSESS and our community.
</p>
<div class="mt-6">
<Link href="/sponsor">
<Button>Become a Sponsor</Button>
</Link>
</div>
</div>

<!-- Sponsors Flex -->
{#if sponsors && sponsors.length > 0}
<div class="flex flex-wrap justify-center gap-6 lg:gap-8">
{#each sponsors as sponsor}
<Link href={sponsor.url} external>
<div
class="bg-ecsess-950 border-ecsess-800 hover:border-ecsess-700 group flex h-32 w-sm items-center justify-center rounded-lg border p-4 transition-all hover:shadow-lg"
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

w-sm is not a standard Tailwind width utility (likely meant max-w-sm or a fixed w-*). As-is, it will be ignored and the sponsor card width will collapse to content. Use a valid width utility (or define a custom width token) to make the layout deterministic.

Suggested change
class="bg-ecsess-950 border-ecsess-800 hover:border-ecsess-700 group flex h-32 w-sm items-center justify-center rounded-lg border p-4 transition-all hover:shadow-lg"
class="bg-ecsess-950 border-ecsess-800 hover:border-ecsess-700 group flex h-32 w-64 items-center justify-center rounded-lg border p-4 transition-all hover:shadow-lg"

Copilot uses AI. Check for mistakes.
>
<img
src={sponsor.logo}
alt="{sponsor.name} Logo"
class="max-h-20 w-40 object-contain opacity-90 transition-opacity group-hover:opacity-100"
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sponsor.url is coming directly from the CMS and is passed as href to Link without any validation of the URL scheme. If an attacker can control a sponsors entry in the CMS (or if that data is otherwise compromised), they could set url to a javascript: or other dangerous URI and trigger XSS in the context of your site when a user clicks the sponsor card. To harden this, ensure you validate or sanitize sponsor.url on the server or before rendering so only safe schemes like https/http are allowed, and ignore or replace anything else.

Copilot uses AI. Check for mistakes.
/>
</div>
</Link>
{/each}
</div>
{:else}
<div class="text-ecsess-300 py-12 text-center">
<p>No sponsors at this time.</p>
</div>
{/if}
</div>
8 changes: 8 additions & 0 deletions src/components/layout/PageThumbnail.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
let { thumbnail = '' } = $props();
</script>

<svelte:head>
<meta property="og:image" content={thumbnail} />
<meta property="twitter:image" content={thumbnail} />
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageThumbnail always emits og:image / twitter:image meta tags even when thumbnail is an empty string. If the thumbnail is missing, it’s better to omit these tags or provide a non-empty default URL to avoid emitting empty SEO metadata.

Suggested change
<meta property="og:image" content={thumbnail} />
<meta property="twitter:image" content={thumbnail} />
{#if thumbnail}
<meta property="og:image" content={thumbnail} />
<meta property="twitter:image" content={thumbnail} />
{/if}

Copilot uses AI. Check for mistakes.
</svelte:head>
5 changes: 0 additions & 5 deletions src/components/layout/SeoMetaTags.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
description = 'Meet the student council, get access to academic and technical resources, registration for events, and much more!',
canonical = 'https://ecsess.mcgilleus.ca'
} = $props();

let thumbnail =
'https://cdn.sanity.io/images/vmtsvpe2/production/5d68504038cc692805dc5e51af83adedfefde442-5304x3443.jpg?h=628&fm=webp';
</script>

<svelte:head>
Expand All @@ -22,12 +19,10 @@
<meta property="og:url" content={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={thumbnail} />

<!-- X (Twitter) -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonical} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={thumbnail} />
</svelte:head>
6 changes: 3 additions & 3 deletions src/components/officehour/OHBlock.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
</script>

<div
class="bg-ecsess-100 text-ecsess-900 hover:bg-ecsess-200 border-ecsess-300 grid h-full place-content-center rounded-md border text-center shadow-md transition-all hover:shadow-lg"
class="bg-ecsess-100 text-ecsess-900 hover:bg-ecsess-200 grid h-full place-content-center rounded-md border-transparent text-center shadow-md transition-all hover:shadow-lg"
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

border-transparent has no effect here because border (border-width) was removed from the class list. If the intent is no border, drop border-transparent; if the intent is an invisible border to prevent layout shift on hover, add border back.

Suggested change
class="bg-ecsess-100 text-ecsess-900 hover:bg-ecsess-200 grid h-full place-content-center rounded-md border-transparent text-center shadow-md transition-all hover:shadow-lg"
class="bg-ecsess-100 text-ecsess-900 hover:bg-ecsess-200 grid h-full place-content-center rounded-md text-center shadow-md transition-all hover:shadow-lg"

Copilot uses AI. Check for mistakes.
>
<p class="text-base font-extrabold lg:text-lg">
<p class="text-base leading-tight font-semibold">
{officeHour.member.name.split(' ')[0]}
</p>

{#if !isShortBlock}
<p class="text-ecsess-700 text-xs italic">
<p class="text-ecsess-700 mt-0.5 text-[11px] leading-tight opacity-90">
{shortenPosition(officeHour.member.position)}
</p>
{/if}
Expand Down
10 changes: 7 additions & 3 deletions src/components/officehour/OHSchedule.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,14 @@
</script>

<div class="overflow-x-auto">
<div class="mx-auto max-w-7xl min-w-[800px]">
<div class="border-ecsess-500 bg-ecsess-900 mx-auto max-w-7xl min-w-[800px] border-t pt-2">
<!-- Header row -->
<div class="mb-2 grid gap-0" style:grid-template-columns="80px repeat(5, 1fr)">
<div class="text-ecsess-50 px-2 text-center text-base font-semibold">Time</div>
<div
class="text-ecsess-50 bg-ecsess-900 sticky left-0 z-20 px-2 text-center text-base font-semibold"
>
Time
</div>
{#each DAYS as day}
<div class="text-ecsess-50 px-2 text-center text-base font-semibold md:text-lg">
{day}
Expand All @@ -135,7 +139,7 @@

<!-- Time column (only for first day) -->
{#if dayIndex === 0}
<div class="border-ecsess-500 relative border-b-2">
<div class="border-ecsess-500 bg-ecsess-900 sticky left-0 z-20 border-b-2">
{#each timeSlots as timeSlot}
{@const isHourMark = timeSlot % 60 === 0}
<div
Expand Down
Loading
Loading