diff --git a/apps/web/.dockerignore b/apps/web/.dockerignore new file mode 100644 index 0000000..5d0850b --- /dev/null +++ b/apps/web/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.env +src +public +*.config.js +*.config.ts +index.html diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..8a9f8af --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,38 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# $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 + +# ── Environment ─────────────────────────────────────────────────────────────── +# "production" (droplet) or "qas" (home-hosted, behind Cloudflare Tunnel). +# Set to "qas" only on the QAS Preview build in Vercel's project settings. +VITE_APP_ENV=production + +# ── 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 + +# ── Country detection (phone input) ──────────────────────────────────────────── +# IP → country lookup used to pre-select the phone input's dialing code. +# Defaults to the free hosted instance; self-host for production via +# https://github.com/lineofflight/country (Docker, needs a free MaxMind +# license key) and point this at your own instance instead. +VITE_COUNTRY_API_URL=https://api.country.is diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..bcd6d22 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,49 @@ +# ── 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* +pnpm-debug.log* +yarn-debug.log* +lerna-debug.log* + +# ── OS ──────────────────────────────────────────────────────────────────────── +.DS_Store +Thumbs.db +desktop.ini + +# ── Editors ─────────────────────────────────────────────────────────────────── +.vscode/* +!.vscode/extensions.json +.idea/ +*.swp +*.swo +*.suo +*.ntvs* +*.njsproj +*.sln +.vercel diff --git a/apps/web/.vscode/extensions.json b/apps/web/.vscode/extensions.json new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..a450ee2 --- /dev/null +++ b/apps/web/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/apps/web/LICENSE b/apps/web/LICENSE new file mode 100644 index 0000000..c4662f9 --- /dev/null +++ b/apps/web/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 [Kenneth Obsequio and Russell Obsequio] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..92dc71f --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,177 @@ +# new_starr_app — Frontend + +React + Vite CSR frontend for the STARR LMS platform. + +--- + +## Page Metadata + +Metadata (``, 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> +``` diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 0000000..e94d87e --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "menuColor": "default", + "menuAccent": "subtle", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@coss": "https://coss.com/ui/r/{name}.json" + } +} diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 0000000..ea36dd3 --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + }, +]) diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..42e235c --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,19 @@ +<!doctype html> +<html lang="en"> + <head> + <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" /> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link + href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Fira+Code:wght@400..600&family=Abril+Fatface&display=swap" + rel="stylesheet" + /> + <title>%VITE_APP_NAME% + + +
+ + + diff --git a/apps/web/jsconfig.json b/apps/web/jsconfig.json new file mode 100644 index 0000000..e8b3494 --- /dev/null +++ b/apps/web/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } +} \ No newline at end of file diff --git a/apps/web/lib/bs/.sourcedirs.json b/apps/web/lib/bs/.sourcedirs.json new file mode 100644 index 0000000..01b8a62 --- /dev/null +++ b/apps/web/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/apps/web/lib/bs/build.ninja b/apps/web/lib/bs/build.ninja new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/lib/bs/compiler-info.json b/apps/web/lib/bs/compiler-info.json new file mode 100644 index 0000000..3131cd1 --- /dev/null +++ b/apps/web/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/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.ast b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.ast new file mode 100644 index 0000000..ff57b58 Binary files /dev/null and b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.ast differ diff --git a/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmi b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmi new file mode 100644 index 0000000..6a2b4f0 Binary files /dev/null and b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmi differ diff --git a/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmj b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmj new file mode 100644 index 0000000..615482c Binary files /dev/null and b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmj differ diff --git a/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmt b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmt new file mode 100644 index 0000000..6917c82 Binary files /dev/null and b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.cmt differ diff --git a/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.js b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.js new file mode 100644 index 0000000..debb90b --- /dev/null +++ b/apps/web/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/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.res b/apps/web/lib/bs/src/modules/client/hooks/rescript/AnnotationPanel.res new file mode 100644 index 0000000..6acb6d5 --- /dev/null +++ b/apps/web/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)} +

+
+ +
+