diff --git a/features/landing/frontend/src/pages/merch/MerchPage.tsx b/features/landing/frontend/src/pages/merch/MerchPage.tsx index ba01208cc..aa96b600e 100644 --- a/features/landing/frontend/src/pages/merch/MerchPage.tsx +++ b/features/landing/frontend/src/pages/merch/MerchPage.tsx @@ -51,7 +51,7 @@ export default function MerchPage() { // Idea submission form state const [submissionImages, setSubmissionImages] = useState([]); - const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitting] = useState(false); // Handle uploaded images change const handleImagesChange = useCallback((images: UploadedImage[]) => { diff --git a/features/landing/frontend/src/pages/shop/ShopGiftCardsPage.tsx b/features/landing/frontend/src/pages/shop/ShopGiftCardsPage.tsx index 712f505b3..e1599b52f 100644 --- a/features/landing/frontend/src/pages/shop/ShopGiftCardsPage.tsx +++ b/features/landing/frontend/src/pages/shop/ShopGiftCardsPage.tsx @@ -11,6 +11,7 @@ import { useReducedMotion } from '@ui/accessibility' import { useSoundEngine } from '@ui/effects-sound' import { useCart, type Product } from '../../contexts' import { Routes } from '../../routes' +import { giftCardSlug, parseGiftCardSlug } from '../../utils' import './Shop.css' function calculateVotes(amount: number): { votes: number; bonus: number; bonusPercent: number } { @@ -36,7 +37,7 @@ const presetAmounts = [25, 50, 75, 100, 150, 200, 250, 300, 400, 500] function createGiftCardProduct(amount: number): Product { const { votes, bonusPercent } = calculateVotes(amount) return { - id: `gc-${amount}-gift-card`, + id: giftCardSlug(amount), name: `$${amount} Gift Card`, description: bonusPercent > 0 ? `Store credit with ${bonusPercent}% bonus voting power. Redeemable for subscriptions, tokens, and merchandise.` @@ -77,12 +78,10 @@ export default function ShopGiftCardsPage() { if (GIFT_CARD_PRODUCTS[productId]) { return GIFT_CARD_PRODUCTS[productId] } - // Check if it's a custom amount (gc-custom-XXX-gift-card format) - if (productId.startsWith('gc-custom-')) { - const amount = parseInt(productId.replace('gc-custom-', '').replace('-gift-card', ''), 10) - if (!isNaN(amount) && amount >= 25 && amount <= 500) { - return createGiftCardProduct(amount) - } + // Check if it's a valid gift card slug (gc-XXX-gift-card format) + const amount = parseGiftCardSlug(productId) + if (amount !== null && amount >= 25 && amount <= 500) { + return createGiftCardProduct(amount) } return null }, [productId]) @@ -92,7 +91,7 @@ export default function ShopGiftCardsPage() { const giftCards = presetAmounts.map((amount) => { const { votes, bonusPercent } = calculateVotes(amount) return { - id: `gc-${amount}-gift-card`, + id: giftCardSlug(amount), amount, popular: amount === 100, description: bonusPercent > 0 @@ -107,7 +106,7 @@ export default function ShopGiftCardsPage() { playSound('registration-success') const product = createGiftCardProduct(amount) addItem(product) - setAddedCardId(`gc-${amount}-gift-card`) + setAddedCardId(giftCardSlug(amount)) setTimeout(() => setAddedCardId(null), 2000) } diff --git a/features/landing/frontend/src/utils/index.ts b/features/landing/frontend/src/utils/index.ts new file mode 100644 index 000000000..6af5978a0 --- /dev/null +++ b/features/landing/frontend/src/utils/index.ts @@ -0,0 +1 @@ +export { slugify, productSlug, giftCardSlug, parseGiftCardSlug } from './slugify' diff --git a/features/landing/frontend/src/utils/slugify.ts b/features/landing/frontend/src/utils/slugify.ts new file mode 100644 index 000000000..d28e8ae17 --- /dev/null +++ b/features/landing/frontend/src/utils/slugify.ts @@ -0,0 +1,53 @@ +/** + * URL slug generation utilities + * Creates SEO-friendly URL slugs from text + */ + +/** + * Convert text to URL-safe slug + * @example slugify('Hello World!') => 'hello-world' + * @example slugify(' Multiple Spaces ') => 'multiple-spaces' + * @example slugify('Special @#$ Characters') => 'special-characters' + */ +export function slugify(text: string): string { + return text + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') // Remove special chars + .replace(/\s+/g, '-') // Spaces to hyphens + .replace(/-+/g, '-') // Collapse multiple hyphens + .replace(/^-+|-+$/g, '') // Trim leading/trailing hyphens +} + +/** + * Create product URL slug from ID and name + * @example productSlug('gc-100', 'Gift Card') => 'gc-100-gift-card' + * @example productSlug('tshirt', 'lilith Classic T-Shirt') => 'tshirt-lilith-classic-t-shirt' + */ +export function productSlug(productId: string, productName: string): string { + return `${productId}-${slugify(productName)}` +} + +/** + * Create gift card product slug + * @example giftCardSlug(100) => 'gc-100-gift-card' + * @example giftCardSlug(50) => 'gc-50-gift-card' + */ +export function giftCardSlug(amount: number): string { + return `gc-${amount}-gift-card` +} + +/** + * Extract amount from gift card slug + * @example parseGiftCardSlug('gc-100-gift-card') => 100 + * @example parseGiftCardSlug('gc-custom-75-gift-card') => 75 + * @example parseGiftCardSlug('invalid') => null + */ +export function parseGiftCardSlug(slug: string): number | null { + // Match gc-{amount}-gift-card or gc-custom-{amount}-gift-card + const match = slug.match(/^gc-(?:custom-)?(\d+)-gift-card$/) + if (!match) return null + + const amount = parseInt(match[1], 10) + return isNaN(amount) ? null : amount +}