-
-
diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte
index 03ba7ba..4f5f042 100644
--- a/src/lib/components/Sidebar.svelte
+++ b/src/lib/components/Sidebar.svelte
@@ -1,33 +1,82 @@
+{#if $toggleSidebar}
+
-
-
- {#each menuItems as item}
-
{
- goto(item.ref);
- toggleSidebar = false;
- }}
- type="button"
- class="btn flex w-full rounded transition duration-200"
- >
-
-
- {/each}
+
+
+
+
+
diff --git a/src/lib/service/contactService.js b/src/lib/service/contactService.js
new file mode 100644
index 0000000..5d2d00a
--- /dev/null
+++ b/src/lib/service/contactService.js
@@ -0,0 +1,12 @@
+export async function submitContactForm(formSpree, payload) {
+ const response = await fetch(formSpree, {
+ method: 'POST',
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ const data = await response.json().catch(() => ({}));
+ throw new Error(data?.errors?.map((e) => e.message).join(', ') || 'Form submission failed');
+ }
+}
diff --git a/src/lib/utils/clickOutside.js b/src/lib/utils/clickOutside.js
new file mode 100644
index 0000000..6952721
--- /dev/null
+++ b/src/lib/utils/clickOutside.js
@@ -0,0 +1,21 @@
+export function clickOutside(node) {
+ const shouldIgnore = (event) => {
+ return event.target.closest('[data-no-close]') !== null;
+ };
+
+ const handleClick = (event) => {
+ if (shouldIgnore(event)) return;
+
+ if (node && !node.contains(event.target) && !event.defaultPrevented) {
+ node.dispatchEvent(new CustomEvent('outclick', { detail: node }));
+ }
+ };
+
+ document.addEventListener('click', handleClick, true);
+
+ return {
+ destroy() {
+ document.removeEventListener('click', handleClick, true);
+ }
+ };
+}
diff --git a/src/lib/utils/contributors.js b/src/lib/utils/contributors.js
new file mode 100644
index 0000000..4040116
--- /dev/null
+++ b/src/lib/utils/contributors.js
@@ -0,0 +1,110 @@
+import Andre from '../assets/images/andre-profile.jpeg';
+import Alesana from '../assets/images/alesana-profile.png';
+import Eugene from '../assets/images/eugene-profile.jpeg';
+import Ronaldo from '../assets/images/ronaldo-profile.jpeg';
+import Chen from '../assets/images/chen-profile.jpeg';
+import Antonio from '../assets/images/antonio-profile.jpeg';
+import Aaron from '../assets/images/aaron-profile.jpeg';
+import Junior from '../assets/images/junior-profile.jpeg';
+import Tausani from '../assets/images/tausani-profile.jpeg';
+import Ainsof from '../assets/images/ainsof-profile.jpeg';
+import Hilton from '../assets/images/hilton-profile.jpeg';
+import Kendrick from '../assets/images/kendrick-profile.jpeg';
+import Henry from '../assets/images/henry-profile.jpg';
+
+export const Contributors = [
+ {
+ avatar: Andre,
+ name: 'Andre Westerlund',
+ title: 'Founder & Senior Full Stack Developer',
+ github: 'https://github.com/westerandr',
+ linkedin: 'https://www.linkedin.com/in/andrewesterlund/',
+ featured: true
+ },
+ {
+ avatar: Alesana,
+ name: 'Alesana Eteuati Jr',
+ title: 'Senior Software Developer',
+ github: 'https://github.com/Green-Ranger11',
+ linkedin: 'https://www.linkedin.com/in/alesana-eteuati-jr/'
+ },
+ {
+ avatar: Eugene,
+ name: 'Eugene Barker',
+ title: 'Solutions Architect',
+ github: 'https://github.com/genebarker',
+ linkedin: 'https://www.linkedin.com/in/eugenebarker/'
+ },
+ {
+ avatar: Chen,
+ name: 'Chia Chen Lin',
+ title: 'Software Developer',
+ github: 'https://github.com/shifu-lin',
+ linkedin: 'https://www.linkedin.com/in/chia-chen-lin-8625251b9/'
+ },
+ {
+ avatar: Junior,
+ name: 'Junior Latu',
+ title: 'Software Developer',
+ github: 'https://github.com/jrlatu',
+ linkedin: 'https://www.linkedin.com/in/talavou-junior-latu/'
+ },
+ {
+ avatar: Ronaldo,
+ name: 'Ronaldo Pose',
+ title: 'Business Solutions Manager',
+ github: 'https://github.com/renaldox',
+ linkedin: 'https://www.linkedin.com/in/ronaldo-pose/',
+ featured: true
+ },
+ {
+ avatar: Aaron,
+ name: 'Aaron Solofa',
+ title: 'GIS Specialist',
+ github: 'https://github.com/aaronjamessolofa',
+ linkedin: 'https://www.linkedin.com/in/aaron-solofa-06b923123/'
+ },
+ {
+ avatar: Kendrick,
+ name: 'Kendrick Lui',
+ title: 'Web Developer',
+ github: 'https://github.com/Ken2523',
+ linkedin: 'https://www.linkedin.com/in/ken-lui-15017a232/'
+ },
+ {
+ avatar: Antonio,
+ name: 'Antonio Chadwick',
+ title: 'IT Engineer',
+ github: 'https://github.com/antoniochadwick',
+ linkedin: 'https://www.linkedin.com/in/antonio-chadwick-8ab853141'
+ },
+ {
+ avatar: Tausani,
+ name: 'Tausani Ah Chong',
+ title: 'Software Engineer',
+ github: 'https://github.com/tausani-ah-chong',
+ linkedin: 'https://www.linkedin.com/in/tausaniahchong',
+ featured: true
+ },
+ {
+ avatar: Ainsof,
+ name: "Ainsof So'o",
+ title: 'Systems Developer & Analyst',
+ github: 'https://github.com/ainsofs',
+ linkedin: 'https://www.linkedin.com/in/ainsofs'
+ },
+ {
+ avatar: Hilton,
+ name: "Hilton So'o",
+ title: 'Managing Director',
+ github: 'https://github.com/encode685',
+ linkedin: 'https://www.linkedin.com/in/hilton-samuelu-so-o-8711aa103'
+ },
+ {
+ avatar: Henry,
+ name: 'Henry Fuerst',
+ title: 'Frontend Developer',
+ github: 'https://github.com/5thAttemptCode',
+ linkedin: 'https://www.linkedin.com/in/henry-fuerst-10b58a187/'
+ }
+];
diff --git a/src/lib/utils/form.js b/src/lib/utils/form.js
new file mode 100644
index 0000000..2bbb880
--- /dev/null
+++ b/src/lib/utils/form.js
@@ -0,0 +1,13 @@
+export function validateEmail(email) {
+ return /\S+@\S+\.\S+/.test(email);
+}
+
+export function triggerToast(toast, message, type = 'success') {
+ const background =
+ {
+ success: 'bg-green-600 text-white',
+ error: 'bg-red-600 text-white'
+ }[type] ?? 'bg-green-600 text-white';
+
+ toast.trigger({ message, background, timeout: 2500 });
+}
diff --git a/src/lib/utils/menuItems.js b/src/lib/utils/menuItems.js
index 9d94232..61b5cfe 100644
--- a/src/lib/utils/menuItems.js
+++ b/src/lib/utils/menuItems.js
@@ -1,38 +1,26 @@
-import { IconHome2, IconNews, IconMail, IconCode, IconUsers } from '@tabler/icons-svelte';
-
export const Section = {
Hero: 'Hero',
- Media: 'Media',
+ Projects: 'Projects',
Contributors: 'Contributors',
- Contact: 'Contact',
About: 'About',
- Projects: 'Projects'
+ Contact: 'Contact'
};
export const menuItems = [
- {
- ref: `#${Section.About}`,
- name: 'About',
- icon: IconCode
- },
{
ref: `#${Section.Projects}`,
- name: 'Projects',
- icon: IconUsers
+ name: 'Projects'
},
{
- ref: `#${Section.Contributors}`,
- name: 'Contributors',
- icon: IconUsers
+ ref: `#${Section.About}`,
+ name: 'About Us'
},
{
- ref: `#${Section.Media}`,
- name: 'Media',
- icon: IconNews
+ ref: `#${Section.Contributors}`,
+ name: 'The Team'
},
{
ref: `#${Section.Contact}`,
- name: 'Contact Us',
- icon: IconMail
+ name: 'Contact Us'
}
];
diff --git a/src/lib/utils/navigation.js b/src/lib/utils/navigation.js
new file mode 100644
index 0000000..2bc9cde
--- /dev/null
+++ b/src/lib/utils/navigation.js
@@ -0,0 +1,20 @@
+import { goto, afterNavigate } from '$app/navigation';
+import { tick } from 'svelte';
+
+export async function goToSection(section) {
+ if (section === 'Contributors') {
+ await goto('/contributors');
+ return;
+ }
+
+ if (window.location.pathname === '/') {
+ document.getElementById(section)?.scrollIntoView({ behavior: 'smooth' });
+ return;
+ }
+
+ await goto('/');
+ await tick();
+ requestAnimationFrame(() => {
+ document.getElementById(section)?.scrollIntoView({ behavior: 'smooth' });
+ });
+}
diff --git a/src/lib/utils/sidebarEnhance.js b/src/lib/utils/sidebarEnhance.js
new file mode 100644
index 0000000..dabf38a
--- /dev/null
+++ b/src/lib/utils/sidebarEnhance.js
@@ -0,0 +1,84 @@
+import { get } from 'svelte/store';
+import { toggleSidebar } from './sidebarLogic';
+
+export function sidebarEnhance(node) {
+ let previousOverflow = '';
+ let focusableElements = [];
+ let firstFocusable;
+ let lastFocusable;
+ let openerButton = null;
+
+ function lockScroll() {
+ previousOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ }
+
+ function unlockScroll() {
+ document.body.style.overflow = previousOverflow;
+ }
+
+ function setupFocusTrap() {
+ // find elements inside sidebar
+ focusableElements = Array.from(
+ node.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])')
+ );
+
+ firstFocusable = focusableElements[0];
+ lastFocusable = focusableElements[focusableElements.length - 1];
+
+ // Focus the first focusable element
+ if (firstFocusable) firstFocusable.focus();
+ }
+
+ function restoreOpenerFocus() {
+ if (openerButton) openerButton.focus();
+ }
+
+ function handleKeydown(e) {
+ if (!get(toggleSidebar)) return;
+
+ // ESC closes sidebar
+ if (e.key === 'Escape') {
+ toggleSidebar.set(false);
+ }
+
+ // Focus trapping
+ if (e.key === 'Tab') {
+ if (e.shiftKey) {
+ // Shift+Tab from first element → jump to last
+ if (document.activeElement === firstFocusable) {
+ e.preventDefault();
+ lastFocusable.focus();
+ }
+ } else {
+ // Tab from last element → jump to first
+ if (document.activeElement === lastFocusable) {
+ e.preventDefault();
+ firstFocusable.focus();
+ }
+ }
+ }
+ }
+
+ // Watch the store value to activate/deactivate features
+ const unsubscribe = toggleSidebar.subscribe((isOpen) => {
+ if (isOpen) {
+ openerButton = document.querySelector('[data-sidebar-toggle]');
+ lockScroll();
+ setupFocusTrap();
+ document.addEventListener('keydown', handleKeydown);
+ } else {
+ unlockScroll();
+ restoreOpenerFocus();
+ document.removeEventListener('keydown', handleKeydown);
+ }
+ });
+
+ return {
+ destroy() {
+ unlockScroll();
+ document.removeEventListener('keydown', handleKeydown);
+ unsubscribe();
+ }
+ };
+}
diff --git a/src/lib/utils/sidebarLogic.js b/src/lib/utils/sidebarLogic.js
new file mode 100644
index 0000000..65dc257
--- /dev/null
+++ b/src/lib/utils/sidebarLogic.js
@@ -0,0 +1,11 @@
+import { writable } from 'svelte/store';
+
+export const toggleSidebar = writable(false);
+
+export function closeSidebar() {
+ toggleSidebar.set(false);
+}
+
+export function toggleSidebarState() {
+ toggleSidebar.update((v) => !v);
+}
diff --git a/src/lib/utils/socialLinks.js b/src/lib/utils/socialLinks.js
new file mode 100644
index 0000000..1d0ea02
--- /dev/null
+++ b/src/lib/utils/socialLinks.js
@@ -0,0 +1,34 @@
+import {
+ IconExternalLink,
+ IconBrandGithub,
+ IconBrandDiscord,
+ IconBrandFacebook
+} from '@tabler/icons-svelte';
+
+export const joinUs = {
+ name: 'Join Us',
+ link: 'https://docs.google.com/forms/d/e/1FAIpQLSckLWtZky5-jGFWi4HnzuCQC3F1af3-LaCYhRrU5NCK36HJ4g/viewform',
+ icon: IconExternalLink,
+ title: 'Become a member!'
+};
+
+export const socialItems = [
+ {
+ name: 'Github',
+ link: 'https://github.com/SamoaCodeHub',
+ icon: IconBrandGithub,
+ title: 'Dive into the code'
+ },
+ {
+ name: 'Discord',
+ link: 'https://docs.google.com/forms/d/e/1FAIpQLSckLWtZky5-jGFWi4HnzuCQC3F1af3-LaCYhRrU5NCK36HJ4g/viewform',
+ icon: IconBrandDiscord,
+ title: 'Sign up to join our Discord'
+ },
+ {
+ name: 'Facebook',
+ link: 'https://www.facebook.com/groups/948415479224570',
+ icon: IconBrandFacebook,
+ title: 'Get the latest on Facebook'
+ }
+];
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index b5eeacb..b3673b6 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -7,7 +7,6 @@
import { storePopup, initializeStores } from '@skeletonlabs/skeleton';
import Appbar from '$lib/components/Appbar.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
- import BackToTop from '$lib/components/BackToTop.svelte';
import Analytics from '$lib/components/Analytics.svelte';
initializeStores();
@@ -18,8 +17,6 @@
function scrollHandler(event) {
visible = event.currentTarget.scrollTop > 150;
}
-
- let toggleSidebar = false;
@@ -32,10 +29,10 @@
-
+
-
+
@@ -44,6 +41,3 @@
-{#if visible}
-
-{/if}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 900f71c..bbcca90 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,15 +1,15 @@
-
+
-
-
+
+
diff --git a/src/routes/contributors/+page.svelte b/src/routes/contributors/+page.svelte
new file mode 100644
index 0000000..3e6c519
--- /dev/null
+++ b/src/routes/contributors/+page.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/sso-custom-theme.js b/sso-custom-theme.js
index 6b395fa..aba22f2 100644
--- a/sso-custom-theme.js
+++ b/sso-custom-theme.js
@@ -90,7 +90,7 @@ export const ssoCustomTheme = {
'--color-surface-200': '203 225 240', // #cbe1f0
'--color-surface-300': '172 206 231', // #accee7
'--color-surface-400': '110 170 213', // #6eaad5
- '--color-surface-500': '48 133 195', // #3085c3
+ '--color-surface-500': '255 255 255', // #ffffff
'--color-surface-600': '43 120 176', // #2b78b0
'--color-surface-700': '36 100 146', // #246492
'--color-surface-800': '29 80 117', // #1d5075
diff --git a/tailwind.config.js b/tailwind.config.js
index d9b1658..c3913da 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -2,9 +2,6 @@ import { join } from 'path';
import forms from '@tailwindcss/forms';
import typography from '@tailwindcss/typography';
-import { skeleton } from '@skeletonlabs/tw-plugin';
-
-import { ssoCustomTheme } from './sso-custom-theme';
export default {
darkMode: 'class',
@@ -15,19 +12,5 @@ export default {
theme: {
extend: {}
},
- plugins: [
- forms,
- typography,
- skeleton({
- themes: {
- custom: [ssoCustomTheme],
- preset: [
- {
- name: 'skeleton',
- enhancements: true
- }
- ]
- }
- })
- ]
+ plugins: [forms, typography]
};
diff --git a/tests/test.js b/tests/test.js
index 72deed9..8050cd5 100644
--- a/tests/test.js
+++ b/tests/test.js
@@ -1,31 +1,33 @@
import { expect, test } from '@playwright/test';
-test('Hero section has expected p', async ({ page }) => {
+// HERO SECTION (h1)
+test('Hero section has expected heading', async ({ page }) => {
await page.goto('/');
await expect(
- page.getByText('Press Join us Now to find out more about becoming a Samoa Code Hub Member')
+ page.getByRole('heading', {
+ name: 'The developer network built for progress',
+ level: 1
+ })
).toBeVisible();
});
-test('Clicking "Projects" button navigates to projects page and displays Featured Projects h1', async ({
+// PROJECTS NAVIGATION (h2 on the projects page)
+test('Clicking "Projects" button navigates to projects page and displays Featured Projects heading', async ({
page
}) => {
await page.goto('/');
await page.click('text=Projects');
- await expect(page.locator('h1:has-text("Featured Projects")')).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Featured Projects', level: 2 })).toBeVisible();
});
-test('Media section has expected h1', async ({ page }) => {
+// CONTACT SECTION (h2)
+test('Contact section has expected heading', async ({ page }) => {
await page.goto('/');
- await expect(page.getByText('Samoa Code Hub Meet Ups')).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Contact Us', level: 2 })).toBeVisible();
});
-test('Contact section has expected h1', async ({ page }) => {
- await page.goto('/');
- await expect(page.getByRole('heading', { name: 'Contact Us' })).toBeVisible();
-});
-
-test('Contributors section has expected h1', async ({ page }) => {
- await page.goto('/');
- await expect(page.getByText('Meet our Seki Devs')).toBeVisible();
+// CONTRIBUTORS PAGE (now at /contributors, h2)
+test('Contributors page has expected heading', async ({ page }) => {
+ await page.goto('/contributors');
+ await expect(page.getByRole('heading', { name: 'Meet our Seki Devs', level: 2 })).toBeVisible();
});