Initial implementation of localization
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import { useAnalytics } from "../../hooks";
|
||||
import AskOrganizerView from "./AskOrganizer.view";
|
||||
import type {
|
||||
@@ -35,12 +36,15 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
buttonText = "Ask an organizer",
|
||||
buttonHref = "#",
|
||||
buttonText,
|
||||
buttonHref,
|
||||
className = "",
|
||||
variant = "centered",
|
||||
onContactClick,
|
||||
}) => {
|
||||
const t = useTranslation();
|
||||
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
|
||||
const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref");
|
||||
const { trackEvent, trackCustomEvent } = useAnalytics();
|
||||
|
||||
const resolvedVariant: AskOrganizerVariant = variant ?? "centered";
|
||||
@@ -74,8 +78,8 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
{
|
||||
component: "AskOrganizer",
|
||||
variant: resolvedVariant,
|
||||
buttonText,
|
||||
buttonHref,
|
||||
buttonText: defaultButtonText,
|
||||
buttonHref: defaultButtonHref,
|
||||
},
|
||||
onContactClick as
|
||||
| ((_data: Record<string, unknown>) => void)
|
||||
@@ -92,8 +96,8 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
description={description}
|
||||
buttonText={buttonText}
|
||||
buttonHref={buttonHref}
|
||||
buttonText={defaultButtonText}
|
||||
buttonHref={defaultButtonHref}
|
||||
className={className}
|
||||
sectionPadding={sectionPadding}
|
||||
contentGap={`${contentGap} ${styles.container}`}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import ContentLockup from "../ContentLockup";
|
||||
import Button from "../Button";
|
||||
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
|
||||
@@ -16,11 +19,14 @@ function AskOrganizerView({
|
||||
labelledBy,
|
||||
onContactClick,
|
||||
}: AskOrganizerViewProps) {
|
||||
const t = useTranslation();
|
||||
const ariaLabel = t("askOrganizer.ariaLabel");
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`${sectionPadding} ${className}`}
|
||||
aria-labelledby={labelledBy}
|
||||
aria-label={labelledBy ? undefined : "Ask an organizer"}
|
||||
aria-label={labelledBy ? undefined : ariaLabel}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className={`flex flex-col ${contentGap}`}>
|
||||
@@ -42,7 +48,7 @@ function AskOrganizerView({
|
||||
variant={variant === "inverse" ? "primary" : "default"}
|
||||
className="xl:!px-[var(--spacing-scale-020)] xl:!py-[var(--spacing-scale-012)] xl:!text-[24px] xl:!leading-[28px]"
|
||||
onClick={onContactClick}
|
||||
ariaLabel={`${buttonText} - Contact an organizer for help`}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
|
||||
@@ -1,47 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import FeatureGridView from "./FeatureGrid.view";
|
||||
import type { FeatureGridProps, Feature } from "./FeatureGrid.types";
|
||||
|
||||
const FeatureGridContainer = memo<FeatureGridProps>(
|
||||
({ title, subtitle, className = "" }) => {
|
||||
const t = useTranslation();
|
||||
|
||||
const features: Feature[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
backgroundColor: "bg-[var(--color-surface-default-brand-royal)]",
|
||||
labelLine1: "Decision-making",
|
||||
labelLine2: "support",
|
||||
labelLine1: t("featureGrid.features.decisionMaking.labelLine1"),
|
||||
labelLine2: t("featureGrid.features.decisionMaking.labelLine2"),
|
||||
panelContent: "/assets/Feature_Support.png",
|
||||
ariaLabel: "Decision-making support tools",
|
||||
ariaLabel: t("featureGrid.features.decisionMaking.ariaLabel"),
|
||||
href: "#decision-making",
|
||||
},
|
||||
{
|
||||
backgroundColor: "bg-[#D1FFE2]",
|
||||
labelLine1: "Values alignment",
|
||||
labelLine2: "exercises",
|
||||
labelLine1: t("featureGrid.features.valuesAlignment.labelLine1"),
|
||||
labelLine2: t("featureGrid.features.valuesAlignment.labelLine2"),
|
||||
panelContent: "/assets/Feature_Exercises.png",
|
||||
ariaLabel: "Values alignment exercises",
|
||||
ariaLabel: t("featureGrid.features.valuesAlignment.ariaLabel"),
|
||||
href: "#values-alignment",
|
||||
},
|
||||
{
|
||||
backgroundColor: "bg-[#F4CAFF]",
|
||||
labelLine1: "Membership",
|
||||
labelLine2: "guidance",
|
||||
labelLine1: t("featureGrid.features.membershipGuidance.labelLine1"),
|
||||
labelLine2: t("featureGrid.features.membershipGuidance.labelLine2"),
|
||||
panelContent: "/assets/Feature_Guidance.png",
|
||||
ariaLabel: "Membership guidance resources",
|
||||
ariaLabel: t("featureGrid.features.membershipGuidance.ariaLabel"),
|
||||
href: "#membership-guidance",
|
||||
},
|
||||
{
|
||||
backgroundColor: "bg-[#CBDDFF]",
|
||||
labelLine1: "Conflict resolution",
|
||||
labelLine2: "tools",
|
||||
labelLine1: t("featureGrid.features.conflictResolution.labelLine1"),
|
||||
labelLine2: t("featureGrid.features.conflictResolution.labelLine2"),
|
||||
panelContent: "/assets/Feature_Tools.png",
|
||||
ariaLabel: "Conflict resolution tools",
|
||||
ariaLabel: t("featureGrid.features.conflictResolution.ariaLabel"),
|
||||
href: "#conflict-resolution",
|
||||
},
|
||||
],
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
const labelledBy = title ? "feature-grid-headline" : undefined;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import ContentLockup from "../ContentLockup";
|
||||
import MiniCard from "../MiniCard";
|
||||
import type { FeatureGridViewProps } from "./FeatureGrid.types";
|
||||
@@ -9,11 +12,16 @@ function FeatureGridView({
|
||||
features,
|
||||
labelledBy,
|
||||
}: FeatureGridViewProps) {
|
||||
const t = useTranslation();
|
||||
const ariaLabel = t("featureGrid.ariaLabel");
|
||||
const linkText = t("featureGrid.linkText");
|
||||
const linkHref = t("featureGrid.linkHref");
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`p-0 lg:p-[var(--spacing-scale-064)] ${className}`}
|
||||
aria-labelledby={labelledBy}
|
||||
aria-label={labelledBy ? undefined : "Feature tools and services"}
|
||||
aria-label={labelledBy ? undefined : ariaLabel}
|
||||
>
|
||||
<div className="py-[var(--spacing-scale-032)] px-[var(--spacing-scale-020)] md:pt-[var(--spacing-scale-076)] md:pb-[var(--spacing-scale-048)] lg:pb-[var(--spacing-scale-076)] md:px-[var(--spacing-scale-048)] bg-[#171717] rounded-[var(--radius-measures-radius-xlarge)] focus-within:ring-2 focus-within:ring-[var(--color-surface-default-brand-royal)] focus-within:ring-offset-2">
|
||||
<div className="w-full mx-auto gap-[var(--spacing-scale-048)] lg:flex lg:items-start lg:gap-[var(--spacing-scale-048)] [container-type:inline-size]">
|
||||
@@ -23,8 +31,8 @@ function FeatureGridView({
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
variant="feature"
|
||||
linkText="Learn more"
|
||||
linkHref="#"
|
||||
linkText={linkText}
|
||||
linkHref={linkHref}
|
||||
titleId={labelledBy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+26
-21
@@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../contexts/MessagesContext";
|
||||
import Link from "next/link";
|
||||
import Logo from "./Logo";
|
||||
import Separator from "./Separator";
|
||||
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
||||
|
||||
const Footer = memo(() => {
|
||||
const t = useTranslation("footer");
|
||||
|
||||
// Schema markup for organization information
|
||||
const schemaData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "Media Economies Design Lab",
|
||||
email: "medlab@colorado.edu",
|
||||
url: "https://communityrule.com",
|
||||
name: t("organization.name"),
|
||||
email: t("organization.email"),
|
||||
url: t("organization.url"),
|
||||
sameAs: [
|
||||
"https://bsky.app/profile/medlabboulder",
|
||||
"https://gitlab.com/medlabboulder",
|
||||
t("social.bluesky.url"),
|
||||
t("social.gitlab.url"),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -55,22 +60,22 @@ const Footer = memo(() => {
|
||||
{/* Contact info */}
|
||||
<div className="flex flex-col items-start gap-[var(--spacing-measures-spacing-016,16px)]">
|
||||
<div className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal">
|
||||
Media Economies Design Lab
|
||||
{t("organization.name")}
|
||||
</div>
|
||||
<a
|
||||
href="mailto:medlab@colorado.edu"
|
||||
href={`mailto:${t("organization.email")}`}
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
medlab@colorado.edu
|
||||
{t("organization.email")}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Social media links */}
|
||||
<div className="flex flex-col items-start gap-[var(--spacing-measures-spacing-016,16px)]">
|
||||
<a
|
||||
href="#"
|
||||
href={t("social.bluesky.url")}
|
||||
className="flex items-center gap-[var(--spacing-measures-spacing-06,6px)] hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer group"
|
||||
aria-label="Follow us on Bluesky"
|
||||
aria-label={t("social.bluesky.ariaLabel")}
|
||||
>
|
||||
<img
|
||||
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
|
||||
@@ -80,13 +85,13 @@ const Footer = memo(() => {
|
||||
className="flex-shrink-0 group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
<div className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal">
|
||||
medlabboulder
|
||||
{t("social.bluesky.handle")}
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
href={t("social.gitlab.url")}
|
||||
className="flex items-center gap-[var(--spacing-measures-spacing-06,6px)] hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer group"
|
||||
aria-label="Follow us on GitLab"
|
||||
aria-label={t("social.gitlab.ariaLabel")}
|
||||
>
|
||||
<img
|
||||
src={getAssetPath(ASSETS.GITLAB_ICON)}
|
||||
@@ -96,7 +101,7 @@ const Footer = memo(() => {
|
||||
className="flex-shrink-0 grayscale group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
<div className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal">
|
||||
medlabboulder
|
||||
{t("social.gitlab.handle")}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,19 +113,19 @@ const Footer = memo(() => {
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
Use cases
|
||||
{t("navigation.useCases")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/learn"
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
Learn
|
||||
{t("navigation.learn")}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
About
|
||||
{t("navigation.about")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,25 +138,25 @@ const Footer = memo(() => {
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6 hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
Privacy Policy
|
||||
{t("legal.privacyPolicy")}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6 hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
Terms of Service
|
||||
{t("legal.termsOfService")}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6 hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
Cookies Settings
|
||||
{t("legal.cookiesSettings")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6">
|
||||
© All right reserved
|
||||
{t("copyright")}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../contexts/MessagesContext";
|
||||
import ContentLockup from "./ContentLockup";
|
||||
import HeroDecor from "./HeroDecor";
|
||||
import { getAssetPath } from "../../lib/assetUtils";
|
||||
@@ -15,6 +16,9 @@ interface HeroBannerProps {
|
||||
|
||||
const HeroBanner = memo<HeroBannerProps>(
|
||||
({ title, subtitle, description, ctaText, ctaHref }) => {
|
||||
const t = useTranslation();
|
||||
const imageAlt = t("heroBanner.imageAlt");
|
||||
|
||||
return (
|
||||
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-010)]">
|
||||
@@ -44,7 +48,7 @@ const HeroBanner = memo<HeroBannerProps>(
|
||||
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
|
||||
<img
|
||||
src={getAssetPath("assets/HeroImage.png")}
|
||||
alt="Hero illustration"
|
||||
alt={imageAlt}
|
||||
className="w-full h-auto"
|
||||
loading="eager"
|
||||
fetchPriority="high"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import LanguageSwitcherView from "./LanguageSwitcher.view";
|
||||
import type { LanguageSwitcherProps } from "./LanguageSwitcher.types";
|
||||
|
||||
const LanguageSwitcherContainer = memo<LanguageSwitcherProps>(
|
||||
({ className }) => {
|
||||
// Future: Add language switching logic here
|
||||
// For now, this is just a UI component
|
||||
|
||||
return <LanguageSwitcherView className={className} />;
|
||||
},
|
||||
);
|
||||
|
||||
LanguageSwitcherContainer.displayName = "LanguageSwitcher";
|
||||
|
||||
export default LanguageSwitcherContainer;
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface LanguageSwitcherProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type { LanguageSwitcherProps, Language } from "./LanguageSwitcher.types";
|
||||
|
||||
const AVAILABLE_LANGUAGES: Language[] = [
|
||||
{
|
||||
code: "en",
|
||||
name: "English",
|
||||
nativeName: "English",
|
||||
},
|
||||
];
|
||||
|
||||
function LanguageSwitcherView({ className = "" }: LanguageSwitcherProps) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<label htmlFor="language-select" className="sr-only">
|
||||
Select language
|
||||
</label>
|
||||
<select
|
||||
id="language-select"
|
||||
className="bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] font-inter text-sm leading-5 font-normal border border-[var(--color-surface-default-secondary)] rounded-[var(--radius-measures-radius-small)] px-[var(--spacing-scale-012)] py-[var(--spacing-scale-008)] focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 cursor-pointer"
|
||||
aria-label="Select language"
|
||||
disabled
|
||||
>
|
||||
{AVAILABLE_LANGUAGES.map((language) => (
|
||||
<option key={language.code} value={language.code}>
|
||||
{language.nativeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[var(--color-content-default-secondary)] font-inter text-xs leading-4 font-normal mt-[var(--spacing-scale-008)]">
|
||||
Language switching functionality coming soon
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LanguageSwitcherView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./LanguageSwitcher.container";
|
||||
export type { LanguageSwitcherProps, Language } from "./LanguageSwitcher.types";
|
||||
@@ -1,3 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import SectionHeader from "../SectionHeader";
|
||||
import NumberedCard from "../NumberedCard";
|
||||
import Button from "../Button";
|
||||
@@ -9,6 +12,8 @@ function NumberedCardsView({
|
||||
cards,
|
||||
schemaJson,
|
||||
}: NumberedCardsViewProps) {
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
@@ -23,7 +28,7 @@ function NumberedCardsView({
|
||||
<SectionHeader
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
titleLg="How CommunityRule helps"
|
||||
titleLg={t("numberedCards.titleLg")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -45,13 +50,13 @@ function NumberedCardsView({
|
||||
{/* Default button for xsm and sm breakpoints */}
|
||||
<div className="block lg:hidden">
|
||||
<Button variant="default" size="large">
|
||||
Create CommunityRule
|
||||
{t("numberedCards.buttons.createCommunityRule")}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Outlined button for lg and xlg breakpoints */}
|
||||
<div className="hidden lg:block">
|
||||
<Button variant="outlined" size="large">
|
||||
See how it works
|
||||
{t("numberedCards.buttons.seeHowItWorks")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user