diff --git a/functions/api/volunteer-reference.ts b/functions/api/volunteer-reference.ts new file mode 100644 index 0000000..a691e7a --- /dev/null +++ b/functions/api/volunteer-reference.ts @@ -0,0 +1,348 @@ +type Env = { + TURNSTILE_SECRET_KEY?: string; + RESEND_API_KEY?: string; + CONTACT_FROM_EMAIL?: string; + CONTACT_TO_EMAIL?: string; +}; + +const jsonResponse = (data: Record, status = 200) => + new Response(JSON.stringify(data), { + status, + headers: { "content-type": "application/json" }, + }); + +const fetchWithTimeout = async ( + input: RequestInfo | URL, + init: RequestInit, + timeoutMs: number, + label: string, +) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(input, { ...init, signal: controller.signal }); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + throw new Error(`${label} request failed: ${message}`); + } finally { + clearTimeout(timeout); + } +}; + +const readTextField = (value: string | null) => (value ?? "").trim(); +const toTitleCase = (value: string) => + value + .replace(/_/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); + +const parseFormFields = async (request: Request) => { + const contentType = request.headers.get("content-type") ?? ""; + const data = new Map(); + + const appendValue = (key: string, value: string | null) => { + const normalized = readTextField(value); + if (!normalized) return; + data.set(key, [...(data.get(key) ?? []), normalized]); + }; + + if (contentType.includes("application/x-www-form-urlencoded")) { + const bodyText = await request.text(); + const params = new URLSearchParams(bodyText); + for (const [key, value] of params.entries()) { + appendValue(key, value); + } + return data; + } + + const formData = await request.formData(); + for (const [key, value] of formData.entries()) { + appendValue(key, typeof value === "string" ? value : value.name); + } + return data; +}; + +const formatValue = (values: string[] | undefined) => (values ?? []).join(", "); + +const labelMap: Record = { + applicant_first_names: "Applicant first name(s)", + applicant_last_name: "Applicant last name", + referee_name: "Referee full name", + referee_job_title: "Referee job title or role", + referee_organisation: "Referee organisation", + referee_address: "Referee address", + referee_telephone: "Referee telephone", + referee_email: "Referee email", + relationship_capacity: "Capacity in which referee knows the applicant", + relationship_other: "Relationship (if Other)", + known_duration: "How long referee has known the applicant", + related_to_applicant: "Related to the applicant", + character_assessment: "Character and personal qualities", + reliable_punctual: "Reliable and punctual", + suitable_for_role: "Suitable to work with people with disabilities", + suitability_concerns: "Concerns regarding suitability", + additional_comments: "Additional comments", + declaration_truth: "Declaration of truth and accuracy", + declaration_consent: "Declaration of confidentiality understanding", + declaration_signature: "Signature name", + declaration_date: "Date", +}; + +const requiredFields = [ + "applicant_first_names", + "applicant_last_name", + "referee_name", + "referee_email", + "known_duration", + "character_assessment", + "suitable_for_role", + "declaration_truth", +]; + +const sectionOrder = [ + { + title: "Applicant", + fields: ["applicant_first_names", "applicant_last_name"], + }, + { + title: "Referee Details", + fields: [ + "referee_name", + "referee_job_title", + "referee_organisation", + "referee_address", + "referee_telephone", + "referee_email", + ], + }, + { + title: "Relationship to Applicant", + fields: [ + "relationship_capacity", + "relationship_other", + "known_duration", + "related_to_applicant", + ], + }, + { + title: "Assessment", + fields: [ + "character_assessment", + "reliable_punctual", + "suitable_for_role", + "suitability_concerns", + "additional_comments", + ], + }, + { + title: "Declaration", + fields: [ + "declaration_truth", + "declaration_consent", + "declaration_signature", + "declaration_date", + ], + }, +]; + +const handlePost: PagesFunction = async ({ request, env }) => { + const fields = await parseFormFields(request); + + const missingRequired = requiredFields.filter( + (field) => !formatValue(fields.get(field)), + ); + if (missingRequired.length > 0) { + return jsonResponse( + { + ok: false, + error: "Missing required fields.", + details: missingRequired.map((field) => labelMap[field] ?? field).join(", "), + }, + 400, + ); + } + + if (!env.TURNSTILE_SECRET_KEY) { + return jsonResponse( + { ok: false, error: "Turnstile secret key not configured." }, + 500, + ); + } + + const turnstileToken = formatValue(fields.get("cf-turnstile-response")); + if (!turnstileToken) { + return jsonResponse( + { ok: false, error: "Turnstile verification failed." }, + 400, + ); + } + + const verifyBody = new URLSearchParams({ + secret: env.TURNSTILE_SECRET_KEY, + response: turnstileToken, + remoteip: request.headers.get("CF-Connecting-IP") ?? "", + }); + + const verifyResponse = await fetchWithTimeout( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: verifyBody, + }, + 8000, + "Turnstile", + ); + const verifyResult = (await verifyResponse.json()) as { + success?: boolean; + "error-codes"?: string[]; + messages?: string[]; + hostname?: string; + action?: string; + }; + + if (!verifyResult.success) { + const details = [ + verifyResult["error-codes"]?.length + ? `codes=${verifyResult["error-codes"].join(",")}` + : "", + verifyResult.messages?.length + ? `messages=${verifyResult.messages.join(",")}` + : "", + verifyResult.hostname ? `hostname=${verifyResult.hostname}` : "", + verifyResult.action ? `action=${verifyResult.action}` : "", + ] + .filter(Boolean) + .join(" | ") + .slice(0, 500); + return jsonResponse( + { + ok: false, + error: "Turnstile verification failed.", + details: details || undefined, + }, + 400, + ); + } + + if (!env.RESEND_API_KEY) { + return jsonResponse( + { ok: false, error: "Email provider not configured." }, + 500, + ); + } + + const fromEmail = env.CONTACT_FROM_EMAIL; + if (!fromEmail) { + return jsonResponse( + { ok: false, error: "Sender email not configured." }, + 500, + ); + } + + const toEmail = env.CONTACT_TO_EMAIL ?? "calum@muir.in"; + const applicantName = [ + formatValue(fields.get("applicant_first_names")), + formatValue(fields.get("applicant_last_name")), + ] + .filter(Boolean) + .join(" "); + const refereeName = formatValue(fields.get("referee_name")); + + const bodyText = sectionOrder + .map(({ title, fields: sectionFields }) => { + const lines = sectionFields + .map((field) => { + const value = formatValue(fields.get(field)); + if (!value) return null; + return `${labelMap[field] ?? toTitleCase(field)}: ${value}`; + }) + .filter(Boolean) + .join("\n"); + + return lines ? `${title}\n${lines}` : ""; + }) + .filter(Boolean) + .join("\n\n"); + + const extraFields = [...fields.keys()].filter( + (field) => + field !== "cf-turnstile-response" && + !sectionOrder.some((section) => section.fields.includes(field)), + ); + const extraText = extraFields + .map((field) => { + const value = formatValue(fields.get(field)); + return value ? `${labelMap[field] ?? toTitleCase(field)}: ${value}` : ""; + }) + .filter(Boolean) + .join("\n"); + + const emailPayload = { + from: fromEmail, + to: [toEmail], + reply_to: formatValue(fields.get("referee_email")) || undefined, + subject: `New volunteer reference${applicantName ? ` for ${applicantName}` : ""}${refereeName ? ` from ${refereeName}` : ""}`, + text: [bodyText, extraText ? `Additional Fields\n${extraText}` : ""] + .filter(Boolean) + .join("\n\n"), + }; + + const response = await fetchWithTimeout( + "https://api.resend.com/emails", + { + method: "POST", + headers: { + Authorization: `Bearer ${env.RESEND_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(emailPayload), + }, + 8000, + "Resend", + ); + if (!response.ok) { + const contentType = response.headers.get("content-type") ?? ""; + const errorText = contentType.includes("application/json") + ? JSON.stringify(await response.json().catch(() => ({}))) + : await response.text(); + const errorCode = response.headers.get("x-ms-error-code"); + const details = [ + `${response.status} ${response.statusText}`.trim(), + errorCode ? `code=${errorCode}` : "", + errorText.trim(), + ] + .filter(Boolean) + .join(" | ") + .slice(0, 500); + return jsonResponse( + { + ok: false, + error: "Failed to send email.", + details, + }, + 502, + ); + } + + return jsonResponse({ ok: true }); +}; + +export const onRequest: PagesFunction = async (context) => { + try { + if (context.request.method !== "POST") { + return jsonResponse( + { ok: false, error: "Method not allowed." }, + 405, + ); + } + return await handlePost(context); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + return jsonResponse( + { ok: false, error: "Unhandled exception.", details: message }, + 500, + ); + } +}; diff --git a/package-lock.json b/package-lock.json index 3d8f511..e0bea88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -236,6 +236,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1950,6 +1951,7 @@ "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz", "integrity": "sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==", "license": "MIT", + "peer": true, "dependencies": { "@types/pg": "8.11.6" } @@ -4335,6 +4337,7 @@ "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4346,6 +4349,7 @@ "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" } @@ -4704,6 +4708,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5064,6 +5069,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5085,20 +5091,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bufferutil": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", - "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5671,6 +5663,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -5938,6 +5931,7 @@ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.39.1.tgz", "integrity": "sha512-2bDHlzTY31IDmrYn8i+ZZrxK8IyBD4mPZ7JmZdVDQj2tpBZXs/gxB/1kK5pSvkjxPUMNOVsTnoGkSltgjuJwcA==", "license": "Apache-2.0", + "peer": true, "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", @@ -6099,7 +6093,8 @@ "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", - "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==" + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6194,6 +6189,7 @@ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7254,6 +7250,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -7307,6 +7304,7 @@ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", "devOptional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -9030,6 +9028,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.7.0", @@ -9241,6 +9240,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9564,6 +9564,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9589,6 +9590,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9601,6 +9603,7 @@ "version": "7.55.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz", "integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10082,6 +10085,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10668,6 +10672,7 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -10797,6 +10802,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10883,6 +10889,7 @@ "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -10935,6 +10942,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11478,6 +11486,7 @@ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -12206,6 +12215,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -12266,6 +12276,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/public/sponsors/ffordes_logo.png b/public/sponsors/ffordes_logo.png new file mode 100644 index 0000000..4f16ce5 Binary files /dev/null and b/public/sponsors/ffordes_logo.png differ diff --git a/src/pages/about.astro b/src/pages/about.astro index bc8a8fa..99de298 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -16,7 +16,7 @@ const pageSeo = { const stats = [ { number: "1975", label: "Founded" }, { number: "50+", label: "Years serving the Highlands" }, - { number: "Free", label: "Sessions for all participants" }, + { number: "4", label: "Days a week, Tue–Fri" }, { number: "Tue–Fri", label: "Morning sessions" }, ]; @@ -117,7 +117,7 @@ const testimonials = [ Enriching lives through horses in the Highlands since 1975.

- We are a voluntary organisation based at Sandycroft, Reelig, Kirkhill β€” providing free therapeutic and recreational riding for people with disabilities across the Highlands. + We are a voluntary organisation based at Sandycroft, Reelig, Kirkhill β€” providing therapeutic and recreational riding for people with disabilities across the Highlands.

@@ -152,10 +152,10 @@ const testimonials = [

What we do

- Riding, carriage driving, and so much more. + Riding and so much more.

- We provide free riding and carriage driving sessions for participants with a wide range of disabilities. Working with horses improves balance, coordination, confidence, and provides a uniquely meaningful interaction with animals. + We provide riding sessions for participants with a wide range of disabilities. Working with horses improves balance, coordination, confidence, and provides a uniquely meaningful interaction with animals.

Sessions run Tuesday to Friday mornings from our base at Sandycroft. Each session is supported by trained volunteers and supervised by qualified coaches. diff --git a/src/pages/facebook.astro b/src/pages/facebook.astro index e8b614b..88951f2 100644 --- a/src/pages/facebook.astro +++ b/src/pages/facebook.astro @@ -5,9 +5,7 @@ import SiteFooter from "../components/SiteFooter.astro"; import { navigationItems } from "../lib/navigation"; const facebookPageUrl = - "https://www.facebook.com/profile.php?id=100064729054822"; -const facebookEmbedUrl = - "https://www.facebook.com/plugins/page.php?href=https%3A%2F%2Fwww.facebook.com%2Fprofile.php%3Fid%3D100064729054822&tabs=timeline&width=500&height=900&small_header=false&adapt_container_width=true&hide_cover=false&show_facepile=true"; + "https://www.facebook.com/people/Highland-Group-RDA/100064729054822/"; const pageSeo = { title: "Facebook", @@ -17,88 +15,436 @@ const pageSeo = { "Highland Group RDA Facebook, RDA social media, Highland RDA updates, RDA events Facebook, therapeutic riding community", canonicalPath: "/facebook", }; + +const posts = [ + { + id: 1, + date: "3 days ago", + body: "What a glorious morning at Sandycroft! All four ponies were out, the sun was shining, and our riders had a brilliant session. Huge thanks to our amazing volunteer team who make every Tuesday possible. πŸ΄β˜€οΈ", + img: "/Samantha-and-Connolly-1280.webp", + imgPos: "50% 38%", + likes: 47, + comments: 6, + shares: 3, + }, + { + id: 2, + date: "1 week ago", + body: "We are thrilled to announce that three of our team have just completed their RDA Coach training! Welcome to the qualified ranks β€” and thank you for the hours of study, practice, and dedication. The Highlands are lucky to have you. πŸŽ‰", + img: "/Vols5-2880w-1024x768.avif", + imgPos: "50% 28%", + likes: 132, + comments: 24, + shares: 11, + }, + { + id: 3, + date: "2 weeks ago", + body: "MEET THE PONIES β€” Today we'd like to introduce you to Breagh, one of our most willing and independent ponies. Breagh has been a firm favourite with our riders for years. She loves a good back-scratch. πŸ’š", + img: "/Breagh-bc8e70e8-2880w.webp", + imgPos: "50% 45%", + likes: 218, + comments: 41, + shares: 15, + }, + { + id: 4, + date: "3 weeks ago", + body: "Open Day reminder β€” Sunday 10 May, 11am to 3pm at Sandycroft. Come and meet the ponies, chat to our coaches, and find out how you can get involved. Tea, coffee, and home baking on offer! All welcome.", + img: "/about-960.webp", + imgPos: "50% 35%", + likes: 89, + comments: 18, + shares: 27, + }, + { + id: 5, + date: "1 month ago", + body: "Incredible news β€” our sponsored ride raised over Β£1,200! πŸ™ That's enough to cover hoof trimming and hay for the whole summer. To everyone who donated, sponsored, walked, or cheered us on: thank you. You make this work possible.", + img: null, + imgPos: "", + likes: 165, + comments: 32, + shares: 9, + }, +]; + +const photosGrid = [ + { src: "/Samantha-and-Connolly-1280.webp", pos: "50% 38%" }, + { src: "/alineofponies-1920w.webp", pos: "50% 50%" }, + { src: "/Vols5-2880w-1024x768.avif", pos: "50% 28%" }, + { src: "/about-960.webp", pos: "50% 42%" }, + { src: "/contact-960.webp", pos: "50% 40%" }, + { src: "/Breagh-bc8e70e8-2880w.webp", pos: "50% 50%" }, + { src: "/Harley-6ece9eca-2880w.webp", pos: "50% 50%" }, + { src: "/puzzle-5de6e325-2880w.webp", pos: "50% 50%" }, + { src: "/Wispa-2880w.webp", pos: "50% 20%" }, +]; --- -

+ + + + + +
-
+ + +
+
+
-
- -
-
-

- Social -

-
-

- Facebook updates -

-

- Follow our latest news, events, and photos on - Facebook. -

-
+
+

+ Social +

+
+

+ Follow us on Facebook. +

+

+ The latest from Sandycroft β€” meet the ponies, see our riders in action, and follow our sessions, events, and fundraising. +

- - Donate via JustGiving -
-
-
-
- + +
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+

+ Highland Group RDA +

+

+ Nonprofit organisation Β· Riding for the Disabled +

+
+ +
- - Visit our Facebook page -
- + + +
+
+ + +
+
+

Recent posts

+ + See all on Facebook + +
+ + {posts.map((post) => ( +
+
+
+ +
+
+

Highland Group RDA

+

+ {post.date} · 🌍 +

+
+
+
+

+ {post.body} +

+
+ {post.img && ( +
+ +
+ )} +
+
+
+ + + + + + +
+ {post.likes} +
+
+ {post.comments} comments + {post.shares} shares +
+
+ +
+ ))} + + +
+ + + +
+
+ + +
+
+
+ +
+

+ The posts above are a curated preview of our Facebook activity. For the full feed, visit us directly on Facebook. +

+
+
+
+
+ + diff --git a/src/pages/index.astro b/src/pages/index.astro index 58496a5..b966dfc 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -120,19 +120,29 @@ const ctaCards = [ } .sponsor-logo { - width: 140px; height: 64px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-radius: 6px; display: flex; align-items: center; justify-content: center; - font-family: 'Public Sans', sans-serif; - font-weight: 700; - font-size: 13px; - color: rgba(255, 255, 255, 0.5); - letter-spacing: 0.08em; - text-transform: uppercase; + } + .sponsor-logo img { + max-height: 64px; + max-width: 200px; + object-fit: contain; + filter: brightness(0) invert(1); + opacity: 0.85; + transition: opacity 0.2s ease; + } + .sponsor-logo img:hover { + opacity: 1; + } + .sponsor-cta { + border: 2px dashed rgba(255, 255, 255, 0.25); + border-radius: 8px; + padding: 20px 28px; + text-align: center; + max-width: 380px; + margin: 0 auto; } @media (max-width: 767px) { @@ -233,7 +243,7 @@ const ctaCards = [ What is Riding for the Disabled?

- Across the Highlands, we provide free riding and carriage driving sessions to people with a wide range of physical and mental disabilities β€” connecting them with our ponies in a way that's therapeutic, joyful, and life-changing. + Across the Highlands, we provide riding sessions for people with a wide range of physical and mental disabilities β€” connecting them with our ponies in a way that's therapeutic, joyful, and life-changing.