mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
git-subtree-dir: apps/web git-subtree-mainline:eaa6d2276agit-subtree-split:459af9d46a
178 lines
5.4 KiB
Markdown
178 lines
5.4 KiB
Markdown
# new_starr_app — Frontend
|
||
|
||
React + Vite CSR frontend for the STARR LMS platform.
|
||
|
||
---
|
||
|
||
## Page Metadata
|
||
|
||
Metadata (`<title>`, Open Graph, Twitter Card) is managed with **`react-helmet-async`**.
|
||
The system has two layers:
|
||
|
||
1. **Global fallback** — a `<Helmet>` in `App.jsx` always renders default tags. Any page that does not supply its own metadata falls back to this.
|
||
2. **Per-page override** — each page renders a `<PageMeta />` component anywhere in its JSX tree. react-helmet-async lets the deepest/last `<Helmet>` win, so the page always overrides the fallback.
|
||
|
||
This approach fixes the `useNavigate()` stale-title bug: when you navigate away from a page, its `<PageMeta>` unmounts and the global fallback immediately takes over.
|
||
|
||
---
|
||
|
||
### The component
|
||
|
||
```jsx
|
||
// src/contexts/MetadataContext.jsx
|
||
import { PageMeta } from '@/contexts/MetadataContext'
|
||
```
|
||
|
||
**Props**
|
||
|
||
| Prop | Type | Default |
|
||
|---|---|---|
|
||
| `title` | `string \| undefined` | `'STARR \| Philproperties'` |
|
||
| `description` | `string \| undefined` | app default description |
|
||
| `keywords` | `string \| undefined` | app default keywords |
|
||
| `ogImage` | `string \| undefined` | app default OG image URL |
|
||
| `ogType` | `string` | `'website'` |
|
||
|
||
Every prop is optional. Any omitted prop falls back to the app-level default.
|
||
Passing `title={undefined}` explicitly is the same as omitting it — the fallback title renders.
|
||
|
||
---
|
||
|
||
### Static pages
|
||
|
||
Pages with no data dependency just pass a hardcoded string.
|
||
|
||
```jsx
|
||
import { PageMeta } from '@/contexts/MetadataContext'
|
||
|
||
export default function CourseList() {
|
||
return (
|
||
<section className="bg-muted h-full">
|
||
<PageMeta
|
||
title="Courses - STARR"
|
||
description="Browse and manage your training courses."
|
||
/>
|
||
{/* rest of page */}
|
||
</section>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Dynamic pages — data from context
|
||
|
||
When the page title comes from an API response, pass the derived string.
|
||
Use `undefined` (not a hardcoded fallback string) while the data is loading — this lets the global default render during the loading state and then automatically updates when data arrives.
|
||
|
||
```jsx
|
||
import { PageMeta } from '@/contexts/MetadataContext'
|
||
import { useCourses } from '@/contexts/AdminCoursesContext'
|
||
|
||
export default function ViewCourse() {
|
||
const { course } = useCourses()
|
||
|
||
return (
|
||
<section className="bg-muted/60 min-h-full">
|
||
{/* undefined while course is null → global default renders */}
|
||
<PageMeta
|
||
title={course ? `${course.title} - STARR` : undefined}
|
||
description={course?.description}
|
||
/>
|
||
{/* rest of page */}
|
||
</section>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Dynamic pages — data in local state
|
||
|
||
Same pattern when the title comes from local `useState` instead of context.
|
||
|
||
```jsx
|
||
import { PageMeta } from '@/contexts/MetadataContext'
|
||
|
||
export default function EditUnit() {
|
||
const [unitTitle, setUnitTitle] = useState('')
|
||
// ... fetch and setUnitTitle on load
|
||
|
||
return (
|
||
<section className="bg-muted h-full">
|
||
<PageMeta title={unitTitle ? `Edit: ${unitTitle} - STARR` : undefined} />
|
||
{/* rest of page */}
|
||
</section>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Placement rules
|
||
|
||
- Drop `<PageMeta>` as the **first child** of the root element in the page's main `return`.
|
||
- Do **not** add it to early/loading returns (spinner-only returns). The global fallback covers those.
|
||
- Do **not** wrap the entire page in `<PageMeta>` — it is an inline element, not a wrapper.
|
||
|
||
```jsx
|
||
// CORRECT — inline inside root element
|
||
return (
|
||
<section>
|
||
<PageMeta title="..." />
|
||
<div>...</div>
|
||
</section>
|
||
)
|
||
|
||
// WRONG — do not use as a wrapper
|
||
return (
|
||
<PageMeta title="...">
|
||
<section>...</section>
|
||
</PageMeta>
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
### Title convention
|
||
|
||
| Page type | Pattern | Example |
|
||
|---|---|---|
|
||
| List | `{Entity} - STARR` | `Courses - STARR` |
|
||
| Archived list | `Archived {Entity} - STARR` | `Archived Courses - STARR` |
|
||
| Create | `Add {Entity} - STARR` | `Add Course - STARR` |
|
||
| View (dynamic) | `{name} - STARR` | `Intro to Sales - STARR` |
|
||
| Edit (dynamic) | `Edit: {name} - STARR` | `Edit: Intro to Sales - STARR` |
|
||
| Sub-list | `{SubEntity} – {parentName} - STARR` | `Units – Intro to Sales - STARR` |
|
||
| Nested action | `{Action} – {parentName} - STARR` | `Page Builder – Lesson 1 - STARR` |
|
||
|
||
---
|
||
|
||
### Adding metadata to a new page — checklist
|
||
|
||
1. Import `PageMeta` from `@/contexts/MetadataContext`.
|
||
2. Identify what data (if any) drives the title — context state, local state, or nothing.
|
||
3. Place `<PageMeta title="..." />` as the first child of the root element in the main `return`.
|
||
4. Use the title convention above.
|
||
5. Pass `description` if the page has meaningful content to describe (e.g. a course description).
|
||
6. Pass `title={undefined}` (or omit `title`) while data is still loading.
|
||
|
||
---
|
||
|
||
### MetadataProvider (legacy)
|
||
|
||
`Login.jsx` and `Register.jsx` still use the old wrapper pattern via `MetadataProvider`. This is a backward-compat shim that internally renders `<PageMeta {...value} />` and then its children. It works but should not be used for new pages — use the inline `<PageMeta />` pattern instead.
|
||
|
||
```jsx
|
||
// Old pattern (Login / Register only — do not copy for new pages)
|
||
<MetadataProvider value={{ title: 'Login - Philproperties', description: '...' }}>
|
||
<LoginContent />
|
||
</MetadataProvider>
|
||
|
||
// New pattern (use this for all new pages)
|
||
<section>
|
||
<PageMeta title="Login - Philproperties" description="..." />
|
||
<LoginContent />
|
||
</section>
|
||
```
|