diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5d0850b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.env +src +public +*.config.js +*.config.ts +index.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..da12a44 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# $APP_NAME — Frontend Environment Variables Template +# Copy to .env and fill in all CHANGE_ME values before building. +# +# Self-hosted setup: +# 1. cp .env.example .env +# 2. Fill in every CHANGE_ME value below +# 3. npm run build (bakes env vars into the dist/ bundle) +# 4. docker build -t $APP_NAME-app . +# (or serve dist/ with any static host — Nginx, Caddy, Vercel, etc.) +# +# NOTE: These are BUILD-TIME variables (VITE_ prefix). Changing them after +# the build has no effect — you must rebuild to pick up new values. +# ═══════════════════════════════════════════════════════════════════════════════ + +# ── API Connection ──────────────────────────────────────────────────────────── +# Point to your backend's public URL — no trailing slash. +# Must match APP_URL / ALLOWED_ORIGINS in the backend .env. +VITE_APP_NAME=starr +VITE_API_URL=https://api.yourdomain.com/api +VITE_APP_URL=https://api.yourdomain.com + +# ── PayPal ──────────────────────────────────────────────────────────────────── +# Client ID is the public key — safe to expose in the browser bundle. +# developer.paypal.com → My Apps & Credentials → Live tab → Client ID +VITE_PAYPAL_CLIENT_ID=CHANGE_ME diff --git a/.gitignore b/.gitignore index 3b0b403..b012a95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,48 @@ -# Logs -logs +# ── Dependencies ────────────────────────────────────────────────────────────── +node_modules/ + +# ── Environment / Secrets ───────────────────────────────────────────────────── +.env +.env.local +.env.*.local +# Keep .env.example — it is safe and documents required variables + +# ── SSL / TLS / Keys ────────────────────────────────────────────────────────── +*.pem +*.key +*.cert +*.crt +*.p12 +*.pfx + +# ── Build / Output ──────────────────────────────────────────────────────────── +dist/ +dist-ssr/ + +# ── Tauri ───────────────────────────────────────────────────────────────────── +src-tauri/target/ +.tauri/ + +# ── Logs ────────────────────────────────────────────────────────────────────── *.log +logs/ npm-debug.log* -yarn-debug.log* -yarn-error.log* pnpm-debug.log* +yarn-debug.log* lerna-debug.log* -node_modules -dist -dist-ssr -*.local +# ── OS ──────────────────────────────────────────────────────────────────────── +.DS_Store +Thumbs.db +desktop.ini -# Editor directories and files +# ── Editors ─────────────────────────────────────────────────────────────────── .vscode/* !.vscode/extensions.json -.idea -.DS_Store +.idea/ +*.swp +*.swo *.suo *.ntvs* *.njsproj *.sln -*.sw? - -.env \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a450ee2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +# Pre-built image — run `npm run build` first so dist/ exists. +FROM nginx:1.27-alpine + +COPY dist/ /usr/share/nginx/html/ +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index a36934d..1df1e69 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,177 @@ -# React + Vite +# new_starr_app — Frontend -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +React + Vite CSR frontend for the STARR LMS platform. -Currently, two official plugins are available: +--- -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) +## Page Metadata -## React Compiler +Metadata (``, Open Graph, Twitter Card) is managed with **`react-helmet-async`**. +The system has two layers: -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +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. -## Expanding the ESLint configuration +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. -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +--- + +### 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/60 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/60 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> +``` diff --git a/components.json b/components.json index 663bf49..e94d87e 100644 --- a/components.json +++ b/components.json @@ -12,6 +12,8 @@ }, "iconLibrary": "lucide", "rtl": false, + "menuColor": "default", + "menuAccent": "subtle", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -19,7 +21,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "menuColor": "default", - "menuAccent": "subtle", - "registries": {} + "registries": { + "@coss": "https://coss.com/ui/r/{name}.json" + } } diff --git a/index.html b/index.html index 53beb49..16f3f16 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,10 @@ <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>my-app + + + + %VITE_APP_NAME%
diff --git a/lib/bs/.sourcedirs.json b/lib/bs/.sourcedirs.json new file mode 100644 index 0000000..01b8a62 --- /dev/null +++ b/lib/bs/.sourcedirs.json @@ -0,0 +1 @@ +{"cmt_scan":[{"also_scan_build_root":true,"build_root":"lib/bs","scan_dirs":["src/modules/client/hooks/rescript","src/modules/client/hooks/rescript/canvas"]}],"dirs":["src/modules/client/hooks/rescript","src/modules/client/hooks/rescript/canvas"],"generated":[],"pkgs":[["@rescript/react","/home/lash/Desktop/projects/new_starr_app/node_modules/.pnpm/@rescript+react@0.15.0_@rescript+runtime@12.3.0_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/@rescript/react"]],"version":2} \ No newline at end of file diff --git a/lib/bs/build.ninja b/lib/bs/build.ninja new file mode 100644 index 0000000..e69de29 diff --git a/lib/bs/compiler-info.json b/lib/bs/compiler-info.json new file mode 100644 index 0000000..3131cd1 --- /dev/null +++ b/lib/bs/compiler-info.json @@ -0,0 +1,8 @@ +{ + "version": "12.3.0", + "bsc_path": "/home/lash/Desktop/projects/new_starr_app/node_modules/.pnpm/@rescript+linux-x64@12.3.0/node_modules/@rescript/linux-x64/bin/bsc.exe", + "bsc_hash": "75e3a59c95cc953608fd3dd6ea6ed68141bba4414a20a52f114261b8a48385fa", + "rescript_config_hash": "d85ec7cb9b3b80783c8953553f7875d1b9d94eb55dcfd9a57b3ba357830c1934", + "runtime_path": "/home/lash/Desktop/projects/new_starr_app/node_modules/.pnpm/@rescript+runtime@12.3.0/node_modules/@rescript/runtime", + "generated_at": "1779801549320" +} \ No newline at end of file diff --git a/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.ast b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.ast new file mode 100644 index 0000000..ff57b58 Binary files /dev/null and b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.ast differ diff --git a/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmi b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmi new file mode 100644 index 0000000..6a2b4f0 Binary files /dev/null and b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmi differ diff --git a/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmj b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmj new file mode 100644 index 0000000..615482c Binary files /dev/null and b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmj differ diff --git a/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmt b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmt new file mode 100644 index 0000000..6917c82 Binary files /dev/null and b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmt differ diff --git a/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.js b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.js new file mode 100644 index 0000000..debb90b --- /dev/null +++ b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.js @@ -0,0 +1,58 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as JsxRuntime from "react/jsx-runtime"; + +function AnnotationPanel(props) { + let onClose = props.onClose; + let onNoteChange = props.onNoteChange; + let note = props.note; + let lesson = props.lesson; + let noteValue = note !== undefined ? note : ""; + return JsxRuntime.jsxs("div", { + children: [ + JsxRuntime.jsxs("div", { + children: [ + JsxRuntime.jsxs("div", { + children: [ + JsxRuntime.jsx("p", { + children: "Annotation", + className: "text-xs text-muted-foreground uppercase tracking-widest font-semibold" + }), + JsxRuntime.jsx("h2", { + children: lesson.title, + className: "text-lg font-bold mt-0.5" + }) + ] + }), + JsxRuntime.jsx("button", { + children: "✕ Close", + className: "text-muted-foreground hover:text-foreground transition-colors text-sm", + onClick: param => onClose() + }) + ], + className: "flex items-center justify-between mb-4" + }), + JsxRuntime.jsx("textarea", { + className: "flex-1 w-full resize-none rounded-lg border bg-background p-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring", + placeholder: "Write your notes for this lesson...", + value: noteValue, + onChange: e => { + let value = e.target.value; + onNoteChange(lesson.id, value); + } + }), + JsxRuntime.jsx("p", { + children: "Auto-saved to your browser", + className: "text-xs text-muted-foreground mt-2" + }) + ], + className: "flex flex-col h-full" + }); +} + +let make = AnnotationPanel; + +export { + make, +} +/* react/jsx-runtime Not a pure module */ diff --git a/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.res b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.res new file mode 100644 index 0000000..6acb6d5 --- /dev/null +++ b/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.res @@ -0,0 +1,43 @@ +@react.component +let make = ( + ~lesson: AnnotationTypes.lesson, + ~note: option, + ~onNoteChange: (string, string) => unit, + ~onClose: unit => unit, +) => { + let noteValue = switch note { + | Some(n) => n + | None => "" + } + +
+
+
+

+ {React.string("Annotation")} +

+

+ {React.string(lesson.title)} +

+
+ +
+