Skip to content

Commit 5b89a8c

Browse files
committed
documentation
1 parent fa19e83 commit 5b89a8c

24 files changed

Lines changed: 453 additions & 68 deletions

src/components/cms/EditableImage.jsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,24 @@ import { useEdit } from '../../context/EditContext'
33
import { useContent } from '../../context/ContentContext'
44
import { Image } from 'lucide-react'
55

6+
/**
7+
* A CMS-integrated component for inline image editing.
8+
*
9+
* Displays an image with a hover overlay when site editing is enabled.
10+
* Standard implementation uses a simple URL prompt, but is designed to be
11+
* extended with a full media picker.
12+
*
13+
* @param {Object} props
14+
* @param {string} props.section - The CMS section containing this image data.
15+
* @param {string} props.field - Path to the image URL field within the section.
16+
* @param {string} props.src - Initial image source URL.
17+
* @param {string} props.alt - Accessibility alt text.
18+
* @param {string} [props.className=''] - CSS classes for the image element itself.
19+
* @param {string} [props.containerClassName=''] - CSS classes for the wrapper div.
20+
*/
621
const EditableImage = ({
722
section,
8-
field, // e.g. 'heroImage' or 'features[0].icon' (though icon is not image usually)
23+
field,
924
src,
1025
alt,
1126
className = '',
@@ -20,34 +35,44 @@ const EditableImage = ({
2035
setCurrentSrc(src)
2136
}, [src])
2237

38+
/**
39+
* Triggers the image update flow.
40+
*
41+
* Handles deep cloning and traversal logic identical to EditableText,
42+
* ensuring that image paths at any depth are correctly updated and synced.
43+
*/
2344
const handleEdit = async () => {
24-
// Simple prompt for now. Ideally this would be a media picker.
45+
// UI Interaction: Request new image URL from the user
2546
const newUrl = window.prompt("Enter new image URL:", currentSrc)
47+
2648
if (newUrl && newUrl !== currentSrc) {
49+
// Optimistic update of the local image source
2750
setCurrentSrc(newUrl)
2851

2952
try {
53+
// Step 1: Deep clone the section state
3054
const sectionData = getSection(section)
31-
32-
// Deep clone to avoid mutating state directly
3355
const updatedSection = JSON.parse(JSON.stringify(sectionData))
3456

35-
// Helper to set nested value
36-
// We need to handle nested fields like 'brand.icon' or simple 'heroImage'
57+
// Step 2: Traverse keys to reach the specific image field
3758
const keys = field.split('.')
3859
let target = updatedSection
3960
for (let i = 0; i < keys.length - 1; i++) {
4061
if (!target[keys[i]]) target[keys[i]] = {}
4162
target = target[keys[i]]
4263
}
64+
65+
// Step 3: Set new URL at the target path
4366
target[keys[keys.length - 1]] = newUrl
4467

68+
// Step 4: Commit changes to global state and backend
4569
await updateSection(section, updatedSection)
4670

4771
} catch (err) {
48-
console.error('Failed to update image:', err)
49-
alert('Failed to save image update')
50-
setCurrentSrc(src) // Revert
72+
console.error('CMS: Failed to update image', err)
73+
alert('Failed to save image update. Please check console.')
74+
// Revert UI to previous state on error
75+
setCurrentSrc(src)
5176
}
5277
}
5378
}

src/components/cms/EditableText.jsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ import React, { useState, useEffect } from 'react'
22
import { useEdit } from '../../context/EditContext'
33
import { useContent } from '../../context/ContentContext'
44

5+
/**
6+
* A highly flexible, inline editable text component integrated with the CMS.
7+
*
8+
* This component automatically switches between a display state (plain text)
9+
* and an edit state (input/textarea) based on the global `isEditing` context.
10+
* It handles its own internal state during editing and commits changes to the
11+
* global content state on blur.
12+
*
13+
* @param {Object} props
14+
* @param {string} props.section - The CMS content section name (e.g., 'hero', 'footer').
15+
* @param {string} props.field - The specific field path within the section (e.g., 'title', 'features.0.title').
16+
* @param {string} props.value - The initial fallback value if no CMS data exists.
17+
* @param {boolean} [props.multiline=false] - Whether to use a textarea instead of a text input.
18+
* @param {string} [props.className=''] - Additional CSS classes for the wrapper component.
19+
* @param {React.ElementType} [props.as='span'] - The HTML element or component to render as (e.g., 'h1', 'div').
20+
*/
521
const EditableText = ({
622
section,
723
field,
@@ -21,29 +37,41 @@ const EditableText = ({
2137
setInternalValue(value)
2238
}, [value])
2339

40+
/**
41+
* Commits the edited value back to the CMS backend.
42+
*
43+
* Uses a deep-cloning strategy to preserve data integrity when updating
44+
* nested fields (paths like "array.0.field").
45+
*/
2446
const handleBlur = async () => {
47+
// Skip update if value hasn't changed
2548
if (internalValue === currentValue) return
2649

2750
try {
51+
// Step 1: Fetch current state of the entire section
2852
const sectionData = getSection(section)
29-
// Deep clone to avoid mutating state directly
53+
54+
// Step 2: Deep clone to avoid direct mutation of the global state object
3055
const updatedSection = JSON.parse(JSON.stringify(sectionData))
3156

57+
// Step 3: Traverse the object tree using the dot-notated field path
3258
const keys = field.split('.')
3359
let target = updatedSection
3460
for (let i = 0; i < keys.length - 1; i++) {
61+
// Ensure intermediate objects exist
3562
if (!target[keys[i]]) target[keys[i]] = {}
3663
target = target[keys[i]]
3764
}
65+
66+
// Step 4: Set the new value at the leaf node
3867
target[keys[keys.length - 1]] = internalValue
3968

69+
// Step 5: Persist via the content context (API call + State Sync)
4070
await updateSection(section, updatedSection)
41-
// On success, currentValue will update via props eventually,
42-
// but for immediate feedback we can set it here too if we want,
43-
// though typically we rely on the prop update from parent re-render.
4471
} catch (err) {
45-
console.error('Failed to save', err)
46-
setInternalValue(currentValue) // Revert on error
72+
console.error('CMS: Failed to save changes', err)
73+
// Revert UI to last known good value on failure
74+
setInternalValue(currentValue)
4775
}
4876
}
4977

src/components/landing/LandingCTA.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import React from 'react'
22
import { useNavigate } from 'react-router-dom'
3-
43
import EditableText from '../cms/EditableText'
54

5+
/**
6+
* The final "Call to Action" section for the landing page.
7+
*
8+
* Features a high-contrast design with an animated aurora background
9+
* to draw the user's attention towards the primary conversion button.
10+
*
11+
* @param {Object} props
12+
* @param {Object} props.content - Content object containing title and description.
13+
* @param {string} [props.section='cta_section'] - CMS section identifier.
14+
*/
615
const LandingCTA = ({ content, section = 'cta_section' }) => {
716
const navigate = useNavigate()
817
const ctaContent = content || {}

src/components/landing/LandingFeatures.jsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,38 @@ import { Terminal, Cpu, Globe, Lock, Palette, Zap } from 'lucide-react'
33

44
import EditableText from '../cms/EditableText'
55

6+
/**
7+
* Displays a feature grid on the landing page using a modern Bento layout.
8+
*
9+
* This component is highly dynamic; it accepts a `features` array from the CMS
10+
* and maps them into interactive FeatureCards. It uses `EditableText` extensively
11+
* to allow real-time content updates.
12+
*
13+
* @param {Object} props
14+
* @param {Array} props.features - Array of feature objects (title, description, icon, bg, color).
15+
* @param {string} [props.section='hero'] - The CMS section where these features are stored.
16+
*/
617
const LandingFeatures = ({ features, section = 'hero' }) => {
7-
// Use features from props, or empty array if not provided
18+
// Robustness: Ensure features is always an array to prevent mapping errors
819
const displayFeatures = Array.isArray(features) ? features : []
920

1021
return (
1122
<section id="features" className="py-24 relative overflow-hidden">
1223
<div className="container px-6 mx-auto relative z-10">
1324
<div className="text-center max-w-2xl mx-auto mb-16">
1425
<h2 className="section-title">
15-
<EditableText section="hero" field="features_title" value="Everything you need to " className="text-white" />
16-
<span className="gradient-text-aurora ml-2">
17-
<EditableText section="hero" field="features_highlight" value="scale" />
18-
</span>
26+
<EditableText
27+
section="hero"
28+
field="features_title"
29+
value="Everything you need to "
30+
className="text-white"
31+
/>
32+
<EditableText
33+
section="hero"
34+
field="features_highlight"
35+
value="scale"
36+
className="gradient-text-aurora ml-2"
37+
/>
1938
</h2>
2039
<p className="text-slate-400 text-lg">
2140
<EditableText section="hero" field="features_subtitle" value="Powerful features packaged in a beautiful interface." />
@@ -33,8 +52,21 @@ const LandingFeatures = ({ features, section = 'hero' }) => {
3352
)
3453
}
3554

55+
/**
56+
* An individual card within the Bento grid.
57+
*
58+
* Features micro-animations, glassmorphism styling, and dynamic color/icon injection.
59+
*
60+
* @param {Object} props
61+
* @param {Object} props.feature - The feature data object.
62+
* @param {number} props.index - The index in the array (used for staggered animations).
63+
* @param {string} props.section - Parent section name for CMS updates.
64+
*/
3665
const FeatureCard = ({ feature, index, section }) => {
66+
// Default to 'Star' icon if specific icon name is missing or invalid
3767
const Icon = feature.icon || Star
68+
69+
// Layout Logic: Large features span two columns in the grid
3870
const isLarge = feature.size === 'large'
3971

4072
return (

src/components/landing/LandingHero.jsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,31 @@ import { Link, useNavigate, useLocation } from 'react-router-dom'
66
import { useTranslation } from 'react-i18next'
77
import { navigateContentTarget } from '../../utils/contentNavigation'
88

9+
/**
10+
* The primary hero section for the landing page.
11+
*
12+
* Features:
13+
* - Parallax mouse-tracking effects for depth.
14+
* - Dynamic CMS content integration with multi-layered fallbacks (CMS -> i18n -> Hardcoded).
15+
* - Interactive透视 (perspective) 3D card animation for the dashboard preview.
16+
* - Integrated `EditableText` and `EditableImage` for real-time site editing.
17+
*
18+
* @param {Object} props
19+
* @param {Object} props.content - Hero content from the CMS backend.
20+
*/
921
const LandingHero = ({ content }) => {
1022
const { t } = useTranslation()
1123
const navigate = useNavigate()
1224
const location = useLocation()
25+
26+
// State for the interactive parallax mouse effect
1327
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
1428

1529
useEffect(() => {
30+
/**
31+
* Tracks global mouse movement to update the parallax effect.
32+
* Values are normalized to a small range (0-20) for subtle animation.
33+
*/
1634
const handleMouseMove = (e) => {
1735
setMousePosition({
1836
x: (e.clientX / window.innerWidth) * 20,
@@ -23,7 +41,10 @@ const LandingHero = ({ content }) => {
2341
return () => window.removeEventListener('mousemove', handleMouseMove)
2442
}, [])
2543

26-
// Safe defaults from content or fallbacks
44+
// Logic: Content Fallback Hierarchy
45+
// 1. Check CMS provided content
46+
// 2. Fallback to i18next translation keys
47+
// 3. Last resort: Hardcoded English strings
2748
const titleLine1 = content?.title?.line1 || t('hero.title') || "Publish Stories"
2849
const titleLine2 = content?.title?.line2 || t('hero.subtitle') || "That Matter"
2950
const subtitle = content?.subtitle || t('hero.description')

src/components/landing/LandingStats.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import React from 'react'
2-
32
import EditableText from '../cms/EditableText'
43

4+
/**
5+
* A horizontal statistics bar with glassmorphism styling.
6+
*
7+
* Uses a responsive grid that adjusts from 2 to 4 columns based on screen size.
8+
* Features a custom `divide-x` border styling to separate stat items.
9+
*
10+
* @param {Object} props
11+
* @param {Array} props.stats - Array of stat objects { value, label }.
12+
* @param {string} [props.section='stats'] - CMS section identifier.
13+
*/
514
const LandingStats = ({ stats, section = 'stats' }) => {
6-
// Use stats from props
15+
// Robustness: Ensure stats is an array to avoid crashes if content is missing
716
const displayStats = Array.isArray(stats) ? stats : []
817

918
return (

src/components/layout/Footer.jsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import { navigateContentTarget } from '../../utils/contentNavigation'
66
import { getIconComponent } from '../../utils/iconMap'
77
import { sanitizeExternalUrl } from '../../utils/urlValidation'
88

9+
/**
10+
* Smart icon resolver for contact links.
11+
*
12+
* Attempts to guess the appropriate Lucide icon based on:
13+
* 1. Explicitly defined icon in CMS
14+
* 2. URL protocol (mailto:, tel:)
15+
* 3. Domain detection (github.com)
16+
*/
917
const resolveContactFallbackIcon = (contact) => {
1018
if (!contact) {
1119
return 'Terminal'
@@ -80,6 +88,14 @@ const Footer = () => {
8088
return allItems
8189
}, [staticNavigationItems, dynamicNavigationItems, navigation?.items])
8290

91+
/**
92+
* Normalizes various link target types into a standard browser href.
93+
*
94+
* Handles:
95+
* - `section`: Prepends # for anchor navigation.
96+
* - `route`/`page`: Internal SPA routing paths.
97+
* - `external`: Validates and sanitizes third-party URLs.
98+
*/
8399
const buildTargetHref = useCallback((target) => {
84100
if (!target || typeof target !== 'object') {
85101
return null
@@ -204,11 +220,15 @@ const Footer = () => {
204220
.filter(Boolean)
205221
}, [buildTargetHref, effectiveNavigationItems, footerContent?.quickLinks])
206222

223+
/**
224+
* Centralized click handler for footer links.
225+
*
226+
* Leverages `navigateContentTarget` for SPA-friendly section/route jumps
227+
* and falls back to standard anchor behavior for external/protocol links.
228+
*/
207229
const handleQuickLink = (event, link) => {
208-
// Early return for invalid or missing link data
209230
if (!link) return
210231

211-
// Extract navigation data from link object
212232
const target = link.target
213233

214234
if (target) {
@@ -219,11 +239,11 @@ const Footer = () => {
219239

220240
const href = sanitizeExternalUrl(link.href || link.url)
221241
if (href) {
242+
// Check for protocols that should NOT be intercepted by the SPA router
222243
const isExternal = href.startsWith('http://') || href.startsWith('https://')
223244
const isSpecialProtocol = href.startsWith('mailto:') || href.startsWith('tel:')
224245

225246
if (isExternal || isSpecialProtocol) {
226-
// Allow default anchor behavior
227247
return
228248
}
229249

0 commit comments

Comments
 (0)