feat: complete Phase 1 foundation scaffold
Some checks failed
CI / Lint (push) Has been cancelled
CI / TypeCheck (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/api/Dockerfile name:api]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/cms/Dockerfile name:cms]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/web/Dockerfile name:web]) (push) Has been cancelled
CI / Build (push) Has been cancelled
Deploy / Deploy (push) Has been cancelled
Some checks failed
CI / Lint (push) Has been cancelled
CI / TypeCheck (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/api/Dockerfile name:api]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/cms/Dockerfile name:cms]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/web/Dockerfile name:web]) (push) Has been cancelled
CI / Build (push) Has been cancelled
Deploy / Deploy (push) Has been cancelled
- Monorepo: Turborepo + pnpm workspaces with 7 apps/packages - Backend: NestJS scaffold with 9 modules (auth, tenant, curriculum, simulation, portfolio, ai-coach, analytics, classroom, gamification) - Frontend: React 18 + Vite + TailwindCSS + shadcn/ui with 10 pages, 14 UI primitives, 3 Zustand stores - Database: Prisma schema with 19 models, 13 enums, seed script, multi-tenant ready - Docker: Dev, production, and Portainer Compose files with Traefik reverse proxy - Configuration: .env.example (47 vars), Zod validation, frontend-safe env exposure - i18n: English + Greek locale files (10 files), ICU MessageFormat, react-i18next - Shared packages: @investplay/types, @investplay/ui, @investplay/i18n, @investplay/utils - CI/CD: GitHub Actions (lint, typecheck, test, build, deploy, PR checks) - Documentation: CONTEXT.md, ARCHITECTURE.md (10 Mermaid diagrams), API.md, LOCALIZATION.md, DEVELOPMENT-ROADMAP.md - Infrastructure: Dockerfiles (dev + prod), nginx configs, backup/healthcheck scripts - Security: JWT auth guards, role-based access, rate limiting, Helmet, CORS, PII sanitization
This commit is contained in:
23
packages/i18n/package.json
Normal file
23
packages/i18n/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@investplay/i18n",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "InvestPlay internationalization package",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\" --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "^24.2.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"intl-messageformat": "^10.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
"eslint": "^8.57.0",
|
||||
"rimraf": "^6.0.0"
|
||||
}
|
||||
}
|
||||
53
packages/i18n/src/index.ts
Normal file
53
packages/i18n/src/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import i18next, { type InitOptions } from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import enCommon from "./locales/en/common.json" with { type: "json" };
|
||||
import enFinance from "./locales/en/finance.json" with { type: "json" };
|
||||
import enGamification from "./locales/en/gamification.json" with { type: "json" };
|
||||
import enClassroom from "./locales/en/classroom.json" with { type: "json" };
|
||||
import enAiCoach from "./locales/en/ai-coach.json" with { type: "json" };
|
||||
import elCommon from "./locales/el/common.json" with { type: "json" };
|
||||
import elFinance from "./locales/el/finance.json" with { type: "json" };
|
||||
import elGamification from "./locales/el/gamification.json" with { type: "json" };
|
||||
import elClassroom from "./locales/el/classroom.json" with { type: "json" };
|
||||
import elAiCoach from "./locales/el/ai-coach.json" with { type: "json" };
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
common: enCommon,
|
||||
finance: enFinance,
|
||||
gamification: enGamification,
|
||||
classroom: enClassroom,
|
||||
aiCoach: enAiCoach,
|
||||
},
|
||||
el: {
|
||||
common: elCommon,
|
||||
finance: elFinance,
|
||||
gamification: elGamification,
|
||||
classroom: elClassroom,
|
||||
aiCoach: elAiCoach,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type LocaleResource = typeof resources;
|
||||
|
||||
const defaultOptions: InitOptions = {
|
||||
resources,
|
||||
fallbackLng: "en",
|
||||
defaultNS: "common",
|
||||
ns: ["common", "finance", "gamification", "classroom", "aiCoach"],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
returnNull: false,
|
||||
returnEmptyString: false,
|
||||
};
|
||||
|
||||
export function initI18n(options?: Partial<InitOptions>): typeof i18next {
|
||||
const instance = i18next.createInstance();
|
||||
instance.use(initReactI18next);
|
||||
instance.init({ ...defaultOptions, ...options });
|
||||
return instance;
|
||||
}
|
||||
|
||||
export { useTranslation } from "react-i18next";
|
||||
export type { LocaleResource };
|
||||
34
packages/i18n/src/locales/el/ai-coach.json
Normal file
34
packages/i18n/src/locales/el/ai-coach.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"chat": {
|
||||
"placeholder": "Ρωτήστε με οτιδήποτε για τα οικονομικά...",
|
||||
"greeting": "Γεια σας! Είμαι ο AI Coach σας. Μπορώ να σας βοηθήσω να κατανοήσετε οικονομικές έννοιες, να αναλύσετε το χαρτοφυλάκιό σας και να σας καθοδηγήσω σε προσομοιώσεις. Τι θα θέλατε να μάθετε σήμερα;",
|
||||
"quotaExhausted": "Φτάσατε στο ημερήσιο όριο μηνυμάτων για τον AI Coach. Αναβαθμίστε το πρόγραμμά σας ή δοκιμάστε ξανά αύριο.",
|
||||
"thinking": "Σκέφτομαι...",
|
||||
"errorFallback": "Αντιμετωπίζω δυσκολία στην επεξεργασία του αιτήματός σας. Παρακαλώ δοκιμάστε ξανά.",
|
||||
"send": "Αποστολή",
|
||||
"clear": "Εκκαθάριση συνομιλίας",
|
||||
"close": "Κλείσιμο συνομιλίας",
|
||||
"minimize": "Ελαχιστοποίηση"
|
||||
},
|
||||
"context": {
|
||||
"lesson": "Αυτή τη στιγμή βρίσκεστε στο μάθημα \"{{lesson}}\". Θα προσαρμόσω τις απαντήσεις μου για να σας βοηθήσω με αυτό το θέμα.",
|
||||
"simulation": "Βλέπω ότι εκτελείτε μια προσομοίωση {{simulation}}. Επιτρέψτε μου να σας βοηθήσω να λάβετε τεκμηριωμένες αποφάσεις.",
|
||||
"portfolio": "Ορίστε μια γρήγορη ανάλυση της τρέχουσας κατανομής του χαρτοφυλακίου σας."
|
||||
},
|
||||
"limits": {
|
||||
"dailyMessages": "Ημερήσια μηνύματα: {{used}} / {{limit}}",
|
||||
"tokenUsage": "Tokens που χρησιμοποιήθηκαν: {{used}} / {{limit}}",
|
||||
"upgrade": "Χρειάζεστε περισσότερα; Επικοινωνήστε με τον διαχειριστή σας."
|
||||
},
|
||||
"topics": {
|
||||
"basics": "Οικονομικά Βασικά",
|
||||
"investing": "Επενδύσεις",
|
||||
"budgeting": "Προϋπολογισμός",
|
||||
"debt": "Διαχείριση Χρέους",
|
||||
"retirement": "Συνταξιοδοτικός Προγραμματισμός",
|
||||
"taxes": "Φόροι",
|
||||
"insurance": "Ασφάλειες",
|
||||
"realEstate": "Ακίνητα",
|
||||
"crypto": "Κρυπτονομίσματα"
|
||||
}
|
||||
}
|
||||
66
packages/i18n/src/locales/el/classroom.json
Normal file
66
packages/i18n/src/locales/el/classroom.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "Πίνακας Ελέγχου Τάξης",
|
||||
"overview": "Επισκόπηση",
|
||||
"totalStudents": "Σύνολο Μαθητών",
|
||||
"averageScore": "Μέση Βαθμολογία",
|
||||
"completionRate": "Ποσοστό Ολοκλήρωσης",
|
||||
"reports": "Αναφορές",
|
||||
"recentActivity": "Πρόσφατη Δραστηριότητα",
|
||||
"noActivity": "Καμία πρόσφατη δραστηριότητα"
|
||||
},
|
||||
"assignments": {
|
||||
"title": "Εργασίες",
|
||||
"create": "Δημιουργία Εργασίας",
|
||||
"edit": "Επεξεργασία Εργασίας",
|
||||
"delete": "Διαγραφή Εργασίας",
|
||||
"dueDate": "Ημερομηνία Παράδοσης",
|
||||
"requiredScore": "Απαιτούμενη Βαθμολογία",
|
||||
"maxAttempts": "Μέγιστες Προσπάθειες",
|
||||
"submissions": "Υποβολές",
|
||||
"noAssignments": "Δεν έχουν δημιουργηθεί εργασίες ακόμα",
|
||||
"status": {
|
||||
"open": "Ανοικτή",
|
||||
"closed": "Κλειστή",
|
||||
"draft": "Πρόχειρο"
|
||||
}
|
||||
},
|
||||
"cohorts": {
|
||||
"title": "Τμήματα",
|
||||
"create": "Δημιουργία Τμήματος",
|
||||
"join": "Εγγραφή σε Τάξη",
|
||||
"inviteCode": "Κωδικός Πρόσκλησης",
|
||||
"students": "Μαθητές",
|
||||
"noCohorts": "Δεν υπάρχουν τμήματα ακόμα",
|
||||
"emptyClass": "Δεν υπάρχουν μαθητές σε αυτή την τάξη ακόμα"
|
||||
},
|
||||
"reports": {
|
||||
"title": "Αναφορές",
|
||||
"generate": "Δημιουργία Αναφοράς",
|
||||
"export": "Εξαγωγή",
|
||||
"compositeScore": "Σύνθετη Βαθμολογία",
|
||||
"roi": "Απόδοση Επένδυσης",
|
||||
"riskManagement": "Διαχείριση Κινδύνου",
|
||||
"esgAlignment": "ESG Συμμόρφωση",
|
||||
"quizScore": "Βαθμολογία Κουίζ",
|
||||
"lessonCompletion": "Ολοκλήρωση Μαθημάτων",
|
||||
"studentProgress": "Πρόοδος Μαθητή",
|
||||
"classAverage": "Μέσος Όρος Τάξης"
|
||||
},
|
||||
"teacher": {
|
||||
"actions": {
|
||||
"createAssignment": "Νέα Εργασία",
|
||||
"viewSubmissions": "Προβολή Υποβολών",
|
||||
"gradeAssignment": "Βαθμολόγηση Εργασίας",
|
||||
"manageStudents": "Διαχείριση Μαθητών",
|
||||
"sendAnnouncement": "Αποστολή Ανακοίνωσης",
|
||||
"viewReports": "Προβολή Αναφορών"
|
||||
},
|
||||
"announcement": {
|
||||
"title": "Ανακοίνωση",
|
||||
"placeholder": "Γράψτε μια ανακοίνωση για την τάξη σας...",
|
||||
"send": "Αποστολή Ανακοίνωσης",
|
||||
"sent": "Η ανακοίνωση στάλθηκε με επιτυχία"
|
||||
}
|
||||
}
|
||||
}
|
||||
103
packages/i18n/src/locales/el/common.json
Normal file
103
packages/i18n/src/locales/el/common.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"nav": {
|
||||
"dashboard": "Πίνακας Ελέγχου",
|
||||
"learn": "Μάθηση",
|
||||
"play": "Παιχνίδι",
|
||||
"practice": "Εξάσκηση",
|
||||
"teach": "Διδασκαλία",
|
||||
"classroom": "Τάξη",
|
||||
"leaderboard": "Βαθμολογία",
|
||||
"profile": "Προφίλ",
|
||||
"settings": "Ρυθμίσεις",
|
||||
"admin": "Διαχείριση"
|
||||
},
|
||||
"actions": {
|
||||
"next": "Επόμενο",
|
||||
"back": "Πίσω",
|
||||
"submit": "Υποβολή",
|
||||
"cancel": "Ακύρωση",
|
||||
"save": "Αποθήκευση",
|
||||
"delete": "Διαγραφή",
|
||||
"edit": "Επεξεργασία",
|
||||
"view": "Προβολή",
|
||||
"confirm": "Επιβεβαίωση",
|
||||
"retry": "Δοκιμή ξανά",
|
||||
"close": "Κλείσιμο",
|
||||
"continue": "Συνέχεια",
|
||||
"start": "Έναρξη",
|
||||
"finish": "Ολοκλήρωση",
|
||||
"skip": "Παράλειψη",
|
||||
"search": "Αναζήτηση",
|
||||
"filter": "Φίλτρο",
|
||||
"sort": "Ταξινόμηση",
|
||||
"refresh": "Ανανέωση",
|
||||
"download": "Λήψη",
|
||||
"upload": "Μεταφόρτωση",
|
||||
"share": "Κοινοποίηση",
|
||||
"copy": "Αντιγραφή",
|
||||
"undo": "Αναίρεση",
|
||||
"redo": "Επανάληψη"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "Κάτι πήγε στραβά. Παρακαλώ δοκιμάστε ξανά.",
|
||||
"network": "Σφάλμα δικτύου. Ελέγξτε τη σύνδεσή σας.",
|
||||
"unauthorized": "Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.",
|
||||
"forbidden": "Δεν έχετε άδεια να εκτελέσετε αυτή την ενέργεια.",
|
||||
"notFound": "Η σελίδα που ζητήσατε δεν υπάρχει.",
|
||||
"validation": "Ελέγξτε τα στοιχεία σας και δοκιμάστε ξανά.",
|
||||
"timeout": "Το αίτημα έληξε. Παρακαλώ δοκιμάστε ξανά.",
|
||||
"serverError": "Σφάλμα διακομιστή. Η ομάδα μας έχει ειδοποιηθεί.",
|
||||
"rateLimited": "Πάρα πολλά αιτήματα. Παρακαλώ περιμένετε λίγο.",
|
||||
"sessionExpired": "Η συνεδρία σας έληξε. Παρακαλώ συνδεθείτε ξανά."
|
||||
},
|
||||
"auth": {
|
||||
"login": "Σύνδεση",
|
||||
"register": "Δημιουργία Λογαριασμού",
|
||||
"logout": "Αποσύνδεση",
|
||||
"forgotPassword": "Ξεχάσατε τον Κωδικό;",
|
||||
"resetPassword": "Επαναφορά Κωδικού",
|
||||
"changePassword": "Αλλαγή Κωδικού",
|
||||
"email": "Διεύθυνση email",
|
||||
"password": "Κωδικός",
|
||||
"confirmPassword": "Επιβεβαίωση κωδικού",
|
||||
"rememberMe": "Να με θυμάσαι",
|
||||
"noAccount": "Δεν έχετε λογαριασμό;",
|
||||
"hasAccount": "Έχετε ήδη λογαριασμό;",
|
||||
"signUp": "Εγγραφή",
|
||||
"signIn": "Σύνδεση",
|
||||
"welcomeBack": "Καλώς ήρθατε πίσω",
|
||||
"createAccount": "Δημιουργήστε τον λογαριασμό σας",
|
||||
"termsAgree": "Συμφωνώ με τους Όρους Χρήσης και την Πολιτική Απορρήτου"
|
||||
},
|
||||
"empty": {
|
||||
"noData": "Δεν υπάρχουν διαθέσιμα δεδομένα",
|
||||
"noResults": "Δεν βρέθηκαν αποτελέσματα",
|
||||
"comingSoon": "Σύντομα διαθέσιμο",
|
||||
"noLessons": "Δεν υπάρχουν μαθήματα ακόμα",
|
||||
"noSimulations": "Δεν υπάρχουν διαθέσιμες προσομοιώσεις",
|
||||
"noAssignments": "Δεν υπάρχουν εργασίες ακόμα",
|
||||
"noStudents": "Δεν είναι εγγεγραμμένοι μαθητές",
|
||||
"noMessages": "Δεν υπάρχουν μηνύματα ακόμα"
|
||||
},
|
||||
"time": {
|
||||
"today": "Σήμερα",
|
||||
"yesterday": "Χθες",
|
||||
"thisWeek": "Αυτή την εβδομάδα",
|
||||
"thisMonth": "Αυτό τον μήνα",
|
||||
"minutes": "{{count}} λεπτό",
|
||||
"minutes_plural": "{{count}} λεπτά",
|
||||
"hours": "{{count}} ώρα",
|
||||
"hours_plural": "{{count}} ώρες",
|
||||
"days": "{{count}} ημέρα",
|
||||
"days_plural": "{{count}} ημέρες"
|
||||
},
|
||||
"theme": {
|
||||
"light": "Φωτεινό",
|
||||
"dark": "Σκοτεινό",
|
||||
"system": "Συστήματος"
|
||||
},
|
||||
"language": {
|
||||
"en": "English",
|
||||
"el": "Ελληνικά"
|
||||
}
|
||||
}
|
||||
105
packages/i18n/src/locales/el/finance.json
Normal file
105
packages/i18n/src/locales/el/finance.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"assetClasses": {
|
||||
"stocks": "Μετοχές",
|
||||
"bonds": "Ομόλογα",
|
||||
"etfs": "ETF",
|
||||
"mutualFunds": "Αμοιβαία Κεφάλαια",
|
||||
"crypto": "Κρυπτονομίσματα",
|
||||
"realEstate": "Ακίνητα",
|
||||
"commodities": "Εμπορεύματα",
|
||||
"cash": "Μετρητά",
|
||||
"derivatives": "Παράγωγα",
|
||||
"forex": "Συνάλλαγμα"
|
||||
},
|
||||
"investmentTypes": {
|
||||
"growth": "Επένδυση Ανάπτυξης",
|
||||
"value": "Επένδυση Αξίας",
|
||||
"dividend": "Επένδυση Μερισμάτων",
|
||||
"index": "Επένδυση Δείκτη",
|
||||
"esg": "ESG Επένδυση",
|
||||
"dayTrading": "Ημερήσια Διαπραγμάτευση",
|
||||
"swing": "Swing Trading",
|
||||
"dca": "Μέσος Όρος Κόστους"
|
||||
},
|
||||
"banking": {
|
||||
"savingsAccount": "Λογαριασμός Ταμιευτηρίου",
|
||||
"checkingAccount": "Τρεχούμενος Λογαριασμός",
|
||||
"fixedDeposit": "Προθεσμιακή Κατάθεση",
|
||||
"interestRate": "Επιτόκιο",
|
||||
"apr": "Επιτόκιο (APR)",
|
||||
"apy": "Ετήσια Απόδοση (APY)",
|
||||
"compoundInterest": "Ανατοκισμός",
|
||||
"principal": "Κεφάλαιο",
|
||||
"maturity": "Λήξη"
|
||||
},
|
||||
"budgeting": {
|
||||
"income": "Εισόδημα",
|
||||
"expenses": "Έξοδα",
|
||||
"savings": "Αποταμίευση",
|
||||
"investments": "Επενδύσεις",
|
||||
"needs": "Ανάγκες",
|
||||
"wants": "Επιθυμίες",
|
||||
"emergencyFund": "Ταμείο Έκτακτης Ανάγκης",
|
||||
"trackExpenses": "Παρακολούθηση Εξόδων",
|
||||
"setBudget": "Ορισμός Προϋπολογισμού",
|
||||
"reviewSpending": "Ανασκόπηση Δαπανών"
|
||||
},
|
||||
"debt": {
|
||||
"creditCard": "Πιστωτική Κάρτα",
|
||||
"studentLoan": "Φοιτητικό Δάνειο",
|
||||
"mortgage": "Στεγαστικό Δάνειο",
|
||||
"personalLoan": "Προσωπικό Δάνειο",
|
||||
"interestRate": "Επιτόκιο",
|
||||
"minimumPayment": "Ελάχιστη Πληρωμή",
|
||||
"balance": "Υπόλοιπο",
|
||||
"debtFree": "Χωρίς Χρέη",
|
||||
"consolidation": "Συγχώνευση Χρεών",
|
||||
"snowball": "Μέθοδος Χιονόμπαλας",
|
||||
"avalanche": "Μέθοδος Χιονοστιβάδας"
|
||||
},
|
||||
"inflation": {
|
||||
"inflationRate": "Ποσοστό Πληθωρισμού",
|
||||
"purchasingPower": "Αγοραστική Δύναμη",
|
||||
"cpi": "Δείκτης Τιμών Καταναλωτή",
|
||||
"realReturn": "Πραγματική Απόδοση",
|
||||
"nominalReturn": "Ονομαστική Απόδοση",
|
||||
"hedge": "Αντιστάθμιση Πληθωρισμού"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Χαμηλού Κινδύνου",
|
||||
"medium": "Μέτριου Κινδύνου",
|
||||
"high": "Υψηλού Κινδύνου",
|
||||
"volatility": "Μεταβλητότητα",
|
||||
"diversification": "Διαφοροποίηση",
|
||||
"correlation": "Συσχέτιση",
|
||||
"beta": "Beta",
|
||||
"alpha": "Alpha",
|
||||
"sharpeRatio": "Δείκτης Sharpe",
|
||||
"maxDrawdown": "Μέγιστη Υποχώρηση",
|
||||
"riskTolerance": "Ανοχή Κινδύνου"
|
||||
},
|
||||
"portfolio": {
|
||||
"allocation": "Κατανομή Περιουσιακών Στοιχείων",
|
||||
"rebalance": "Εξισορρόπηση",
|
||||
"performance": "Απόδοση",
|
||||
"returns": "Αποδόσεις",
|
||||
"gain": "Κέρδος",
|
||||
"loss": "Ζημία",
|
||||
"totalValue": "Συνολική Αξία",
|
||||
"profitLoss": "Κέρδη / Ζημίες",
|
||||
"roe": "Απόδοση Επένδυσης",
|
||||
"annualizedReturn": "Ετήσια Απόδοση"
|
||||
},
|
||||
"market": {
|
||||
"bullMarket": "Αγορά Ανοδική (Bull Market)",
|
||||
"bearMarket": "Πτωτική Αγορά (Bear Market)",
|
||||
"correction": "Διόρθωση",
|
||||
"crash": "Κατάρρευση Αγοράς",
|
||||
"recession": "Ύφεση",
|
||||
"recovery": "Ανάκαμψη",
|
||||
"volatility": "Μεταβλητότητα Αγοράς",
|
||||
"liquidity": "Ρευστότητα",
|
||||
"spread": "Διαφορά Αγοράς-Πώλησης",
|
||||
"volume": "Όγκος Συναλλαγών"
|
||||
}
|
||||
}
|
||||
62
packages/i18n/src/locales/el/gamification.json
Normal file
62
packages/i18n/src/locales/el/gamification.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"xp": {
|
||||
"label": "ΠΠ",
|
||||
"earned": "Κερδίσατε {{count}} ΠΠ",
|
||||
"needed": "{{count}} ΠΠ για το επόμενο επίπεδο",
|
||||
"reason": {
|
||||
"lessonCompleted": "Ολοκλήρωση μαθήματος",
|
||||
"quizPassed": "Επιτυχία κουίζ",
|
||||
"streakMaintained": "Διατήρηση σερί",
|
||||
"challengeCompleted": "Ολοκλήρωση πρόκλησης",
|
||||
"simulationCompleted": "Ολοκλήρωση προσομοίωσης",
|
||||
"diversification": "Διαφοροποίηση χαρτοφυλακίου",
|
||||
"riskManagement": "Σωστή διαχείριση κινδύνου",
|
||||
"hypeAvoided": "Αποφυγή διαφημιστικής εκστρατείας",
|
||||
"debtPaidOff": "Εξόφληση χρέους"
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"label": "Επίπεδο",
|
||||
"current": "Επίπεδο {{level}}",
|
||||
"progress": "Επίπεδο {{current}} — {{progress}}% για το Επίπεδο {{next}}"
|
||||
},
|
||||
"badges": {
|
||||
"title": "Σήματα",
|
||||
"unlocked": "Ξεκλείδωμα Σήματος!",
|
||||
"locked": "Κλειδωμένο",
|
||||
"progress": "{{count}} από {{total}} σήματα",
|
||||
"categories": {
|
||||
"milestone": "Ορόσημα",
|
||||
"skill": "Δεξιότητες",
|
||||
"behavior": "Συμπεριφορές",
|
||||
"special": "Ειδικά"
|
||||
}
|
||||
},
|
||||
"achievements": {
|
||||
"title": "Επιτεύγματα",
|
||||
"recent": "Πρόσφατα Επιτεύγματα",
|
||||
"all": "Όλα τα Επιτεύγματα",
|
||||
"empty": "Ολοκληρώστε μαθήματα για να κερδίσετε επιτεύγματα"
|
||||
},
|
||||
"streaks": {
|
||||
"label": "Σερί",
|
||||
"current": "Σερί {{count}} ημερών",
|
||||
"longest": "Μεγαλύτερο: {{count}} ημέρες",
|
||||
"days": "{{count}} ημέρα",
|
||||
"days_plural": "{{count}} ημέρες",
|
||||
"maintain": "Ολοκληρώστε ένα μάθημα σήμερα για να διατηρήσετε το σερί σας!",
|
||||
"lost": "Το σερί χάθηκε. Ξεκινήστε ξανά αύριο!"
|
||||
},
|
||||
"leaderboard": {
|
||||
"title": "Βαθμολογία",
|
||||
"rank": "Θέση",
|
||||
"player": "Παίκτης",
|
||||
"xp": "ΠΠ",
|
||||
"level": "Επίπεδο",
|
||||
"badges": "Σήματα",
|
||||
"yourRank": "Η Θέση σας",
|
||||
"empty": "Δεν υπάρχουν ακόμα κατατάξεις. Ξεκινήστε να μαθαίνετε!",
|
||||
"classroom": "Βαθμολογία Τάξης",
|
||||
"global": "Γενική Βαθμολογία"
|
||||
}
|
||||
}
|
||||
34
packages/i18n/src/locales/en/ai-coach.json
Normal file
34
packages/i18n/src/locales/en/ai-coach.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"chat": {
|
||||
"placeholder": "Ask me anything about finance...",
|
||||
"greeting": "Hi! I'm your AI Coach. I can help you understand financial concepts, analyze your portfolio, and guide you through simulations. What would you like to learn today?",
|
||||
"quotaExhausted": "You've reached your daily limit for AI Coach messages. Upgrade your plan or try again tomorrow.",
|
||||
"thinking": "Thinking...",
|
||||
"errorFallback": "I'm having trouble processing your request. Please try again.",
|
||||
"send": "Send",
|
||||
"clear": "Clear chat",
|
||||
"close": "Close chat",
|
||||
"minimize": "Minimize"
|
||||
},
|
||||
"context": {
|
||||
"lesson": "You are currently on lesson \"{{lesson}}\". I'll tailor my responses to help you with this topic.",
|
||||
"simulation": "I can see you're running a {{simulation}} simulation. Let me help you make informed decisions.",
|
||||
"portfolio": "Here's a quick analysis of your current portfolio allocation."
|
||||
},
|
||||
"limits": {
|
||||
"dailyMessages": "Daily messages: {{used}} / {{limit}}",
|
||||
"tokenUsage": "Tokens used: {{used}} / {{limit}}",
|
||||
"upgrade": "Need more? Contact your administrator."
|
||||
},
|
||||
"topics": {
|
||||
"basics": "Financial Basics",
|
||||
"investing": "Investing",
|
||||
"budgeting": "Budgeting",
|
||||
"debt": "Debt Management",
|
||||
"retirement": "Retirement Planning",
|
||||
"taxes": "Taxes",
|
||||
"insurance": "Insurance",
|
||||
"realEstate": "Real Estate",
|
||||
"crypto": "Cryptocurrency"
|
||||
}
|
||||
}
|
||||
66
packages/i18n/src/locales/en/classroom.json
Normal file
66
packages/i18n/src/locales/en/classroom.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "Classroom Dashboard",
|
||||
"overview": "Overview",
|
||||
"totalStudents": "Total Students",
|
||||
"averageScore": "Average Score",
|
||||
"completionRate": "Completion Rate",
|
||||
"reports": "Reports",
|
||||
"recentActivity": "Recent Activity",
|
||||
"noActivity": "No recent activity"
|
||||
},
|
||||
"assignments": {
|
||||
"title": "Assignments",
|
||||
"create": "Create Assignment",
|
||||
"edit": "Edit Assignment",
|
||||
"delete": "Delete Assignment",
|
||||
"dueDate": "Due Date",
|
||||
"requiredScore": "Required Score",
|
||||
"maxAttempts": "Max Attempts",
|
||||
"submissions": "Submissions",
|
||||
"noAssignments": "No assignments created yet",
|
||||
"status": {
|
||||
"open": "Open",
|
||||
"closed": "Closed",
|
||||
"draft": "Draft"
|
||||
}
|
||||
},
|
||||
"cohorts": {
|
||||
"title": "Cohorts",
|
||||
"create": "Create Cohort",
|
||||
"join": "Join Class",
|
||||
"inviteCode": "Invite Code",
|
||||
"students": "Students",
|
||||
"noCohorts": "No cohorts yet",
|
||||
"emptyClass": "No students in this class yet"
|
||||
},
|
||||
"reports": {
|
||||
"title": "Reports",
|
||||
"generate": "Generate Report",
|
||||
"export": "Export",
|
||||
"compositeScore": "Composite Score",
|
||||
"roi": "ROI",
|
||||
"riskManagement": "Risk Management",
|
||||
"esgAlignment": "ESG Alignment",
|
||||
"quizScore": "Quiz Score",
|
||||
"lessonCompletion": "Lesson Completion",
|
||||
"studentProgress": "Student Progress",
|
||||
"classAverage": "Class Average"
|
||||
},
|
||||
"teacher": {
|
||||
"actions": {
|
||||
"createAssignment": "New Assignment",
|
||||
"viewSubmissions": "View Submissions",
|
||||
"gradeAssignment": "Grade Assignment",
|
||||
"manageStudents": "Manage Students",
|
||||
"sendAnnouncement": "Send Announcement",
|
||||
"viewReports": "View Reports"
|
||||
},
|
||||
"announcement": {
|
||||
"title": "Announcement",
|
||||
"placeholder": "Write an announcement to your class...",
|
||||
"send": "Send Announcement",
|
||||
"sent": "Announcement sent successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
103
packages/i18n/src/locales/en/common.json
Normal file
103
packages/i18n/src/locales/en/common.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"learn": "Learn",
|
||||
"play": "Play",
|
||||
"practice": "Practice",
|
||||
"teach": "Teach",
|
||||
"classroom": "Classroom",
|
||||
"leaderboard": "Leaderboard",
|
||||
"profile": "Profile",
|
||||
"settings": "Settings",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"actions": {
|
||||
"next": "Next",
|
||||
"back": "Back",
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"view": "View",
|
||||
"confirm": "Confirm",
|
||||
"retry": "Retry",
|
||||
"close": "Close",
|
||||
"continue": "Continue",
|
||||
"start": "Start",
|
||||
"finish": "Finish",
|
||||
"skip": "Skip",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"sort": "Sort",
|
||||
"refresh": "Refresh",
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"share": "Share",
|
||||
"copy": "Copy",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "Something went wrong. Please try again.",
|
||||
"network": "Network error. Please check your connection.",
|
||||
"unauthorized": "You need to sign in to access this page.",
|
||||
"forbidden": "You do not have permission to perform this action.",
|
||||
"notFound": "The page you are looking for does not exist.",
|
||||
"validation": "Please check your input and try again.",
|
||||
"timeout": "The request timed out. Please try again.",
|
||||
"serverError": "Server error. Our team has been notified.",
|
||||
"rateLimited": "Too many requests. Please wait a moment.",
|
||||
"sessionExpired": "Your session has expired. Please sign in again."
|
||||
},
|
||||
"auth": {
|
||||
"login": "Sign In",
|
||||
"register": "Create Account",
|
||||
"logout": "Sign Out",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"resetPassword": "Reset Password",
|
||||
"changePassword": "Change Password",
|
||||
"email": "Email address",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm password",
|
||||
"rememberMe": "Remember me",
|
||||
"noAccount": "Don't have an account?",
|
||||
"hasAccount": "Already have an account?",
|
||||
"signUp": "Sign Up",
|
||||
"signIn": "Sign In",
|
||||
"welcomeBack": "Welcome back",
|
||||
"createAccount": "Create your account",
|
||||
"termsAgree": "I agree to the Terms of Service and Privacy Policy"
|
||||
},
|
||||
"empty": {
|
||||
"noData": "No data available",
|
||||
"noResults": "No results found",
|
||||
"comingSoon": "Coming soon",
|
||||
"noLessons": "No lessons available yet",
|
||||
"noSimulations": "No simulations available",
|
||||
"noAssignments": "No assignments yet",
|
||||
"noStudents": "No students enrolled",
|
||||
"noMessages": "No messages yet"
|
||||
},
|
||||
"time": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"thisWeek": "This week",
|
||||
"thisMonth": "This month",
|
||||
"minutes": "{{count}} min",
|
||||
"minutes_plural": "{{count}} mins",
|
||||
"hours": "{{count}} hour",
|
||||
"hours_plural": "{{count}} hours",
|
||||
"days": "{{count}} day",
|
||||
"days_plural": "{{count}} days"
|
||||
},
|
||||
"theme": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"language": {
|
||||
"en": "English",
|
||||
"el": "Ελληνικά"
|
||||
}
|
||||
}
|
||||
105
packages/i18n/src/locales/en/finance.json
Normal file
105
packages/i18n/src/locales/en/finance.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"assetClasses": {
|
||||
"stocks": "Stocks",
|
||||
"bonds": "Bonds",
|
||||
"etfs": "ETFs",
|
||||
"mutualFunds": "Mutual Funds",
|
||||
"crypto": "Cryptocurrency",
|
||||
"realEstate": "Real Estate",
|
||||
"commodities": "Commodities",
|
||||
"cash": "Cash",
|
||||
"derivatives": "Derivatives",
|
||||
"forex": "Foreign Exchange"
|
||||
},
|
||||
"investmentTypes": {
|
||||
"growth": "Growth Investing",
|
||||
"value": "Value Investing",
|
||||
"dividend": "Dividend Investing",
|
||||
"index": "Index Investing",
|
||||
"esg": "ESG Investing",
|
||||
"dayTrading": "Day Trading",
|
||||
"swing": "Swing Trading",
|
||||
"dca": "Dollar-Cost Averaging"
|
||||
},
|
||||
"banking": {
|
||||
"savingsAccount": "Savings Account",
|
||||
"checkingAccount": "Checking Account",
|
||||
"fixedDeposit": "Fixed Deposit",
|
||||
"interestRate": "Interest Rate",
|
||||
"apr": "APR",
|
||||
"apy": "APY",
|
||||
"compoundInterest": "Compound Interest",
|
||||
"principal": "Principal",
|
||||
"maturity": "Maturity"
|
||||
},
|
||||
"budgeting": {
|
||||
"income": "Income",
|
||||
"expenses": "Expenses",
|
||||
"savings": "Savings",
|
||||
"investments": "Investments",
|
||||
"needs": "Needs",
|
||||
"wants": "Wants",
|
||||
"emergencyFund": "Emergency Fund",
|
||||
"trackExpenses": "Track Expenses",
|
||||
"setBudget": "Set Budget",
|
||||
"reviewSpending": "Review Spending"
|
||||
},
|
||||
"debt": {
|
||||
"creditCard": "Credit Card",
|
||||
"studentLoan": "Student Loan",
|
||||
"mortgage": "Mortgage",
|
||||
"personalLoan": "Personal Loan",
|
||||
"interestRate": "Interest Rate",
|
||||
"minimumPayment": "Minimum Payment",
|
||||
"balance": "Balance",
|
||||
"debtFree": "Debt Free",
|
||||
"consolidation": "Debt Consolidation",
|
||||
"snowball": "Debt Snowball",
|
||||
"avalanche": "Debt Avalanche"
|
||||
},
|
||||
"inflation": {
|
||||
"inflationRate": "Inflation Rate",
|
||||
"purchasingPower": "Purchasing Power",
|
||||
"cpi": "Consumer Price Index",
|
||||
"realReturn": "Real Return",
|
||||
"nominalReturn": "Nominal Return",
|
||||
"hedge": "Inflation Hedge"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Low Risk",
|
||||
"medium": "Medium Risk",
|
||||
"high": "High Risk",
|
||||
"volatility": "Volatility",
|
||||
"diversification": "Diversification",
|
||||
"correlation": "Correlation",
|
||||
"beta": "Beta",
|
||||
"alpha": "Alpha",
|
||||
"sharpeRatio": "Sharpe Ratio",
|
||||
"maxDrawdown": "Max Drawdown",
|
||||
"riskTolerance": "Risk Tolerance"
|
||||
},
|
||||
"portfolio": {
|
||||
"allocation": "Asset Allocation",
|
||||
"rebalance": "Rebalance",
|
||||
"performance": "Performance",
|
||||
"returns": "Returns",
|
||||
"gain": "Gain",
|
||||
"loss": "Loss",
|
||||
"totalValue": "Total Value",
|
||||
"profitLoss": "Profit / Loss",
|
||||
"roe": "Return on Investment",
|
||||
"annualizedReturn": "Annualized Return"
|
||||
},
|
||||
"market": {
|
||||
"bullMarket": "Bull Market",
|
||||
"bearMarket": "Bear Market",
|
||||
"correction": "Correction",
|
||||
"crash": "Market Crash",
|
||||
"recession": "Recession",
|
||||
"recovery": "Recovery",
|
||||
"volatility": "Market Volatility",
|
||||
"liquidity": "Liquidity",
|
||||
"spread": "Bid-Ask Spread",
|
||||
"volume": "Trading Volume"
|
||||
}
|
||||
}
|
||||
62
packages/i18n/src/locales/en/gamification.json
Normal file
62
packages/i18n/src/locales/en/gamification.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"xp": {
|
||||
"label": "XP",
|
||||
"earned": "{{count}} XP earned",
|
||||
"needed": "{{count}} XP to next level",
|
||||
"reason": {
|
||||
"lessonCompleted": "Lesson completed",
|
||||
"quizPassed": "Quiz passed",
|
||||
"streakMaintained": "Streak maintained",
|
||||
"challengeCompleted": "Challenge completed",
|
||||
"simulationCompleted": "Simulation completed",
|
||||
"diversification": "Portfolio diversified",
|
||||
"riskManagement": "Good risk management",
|
||||
"hypeAvoided": "Avoided market hype",
|
||||
"debtPaidOff": "Debt paid off"
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"label": "Level",
|
||||
"current": "Level {{level}}",
|
||||
"progress": "Level {{current}} — {{progress}}% to Level {{next}}"
|
||||
},
|
||||
"badges": {
|
||||
"title": "Badges",
|
||||
"unlocked": "Badge Unlocked!",
|
||||
"locked": "Locked",
|
||||
"progress": "{{count}} of {{total}} badges",
|
||||
"categories": {
|
||||
"milestone": "Milestones",
|
||||
"skill": "Skills",
|
||||
"behavior": "Behaviors",
|
||||
"special": "Special"
|
||||
}
|
||||
},
|
||||
"achievements": {
|
||||
"title": "Achievements",
|
||||
"recent": "Recent Achievements",
|
||||
"all": "All Achievements",
|
||||
"empty": "Complete lessons to earn achievements"
|
||||
},
|
||||
"streaks": {
|
||||
"label": "Streak",
|
||||
"current": "{{count}}-day streak",
|
||||
"longest": "Longest: {{count}} days",
|
||||
"days": "{{count}} day",
|
||||
"days_plural": "{{count}} days",
|
||||
"maintain": "Complete a lesson today to keep your streak!",
|
||||
"lost": "Streak lost. Start again tomorrow!"
|
||||
},
|
||||
"leaderboard": {
|
||||
"title": "Leaderboard",
|
||||
"rank": "Rank",
|
||||
"player": "Player",
|
||||
"xp": "XP",
|
||||
"level": "Level",
|
||||
"badges": "Badges",
|
||||
"yourRank": "Your Rank",
|
||||
"empty": "No rankings yet. Start learning!",
|
||||
"classroom": "Classroom Leaderboard",
|
||||
"global": "Global Leaderboard"
|
||||
}
|
||||
}
|
||||
12
packages/i18n/tsconfig.json
Normal file
12
packages/i18n/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"resolveJsonModule": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.json"],
|
||||
"exclude": ["node_modules", "dist", ".turbo"]
|
||||
}
|
||||
18
packages/types/package.json
Normal file
18
packages/types/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@investplay/types",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "InvestPlay shared type definitions",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\" --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
"eslint": "^8.57.0",
|
||||
"rimraf": "^6.0.0"
|
||||
}
|
||||
}
|
||||
38
packages/types/src/ai.ts
Normal file
38
packages/types/src/ai.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface AICoachMessage {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
metadata: {
|
||||
tokensUsed: number;
|
||||
processingTimeMs: number;
|
||||
model: string;
|
||||
} | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AICoachContext {
|
||||
userId: string;
|
||||
lessonId: string | null;
|
||||
simulationType: string | null;
|
||||
portfolio: Record<string, unknown> | null;
|
||||
recentDecisions: string[];
|
||||
userLevel: number;
|
||||
preferredLocale: string;
|
||||
}
|
||||
|
||||
export interface AIModeConfig {
|
||||
mode: "MANAGED" | "BYOK" | "DISABLED";
|
||||
apiEndpoint: string | null;
|
||||
modelName: string | null;
|
||||
maxTokensPerMessage: number;
|
||||
temperature: number;
|
||||
}
|
||||
|
||||
export interface AITokenBudget {
|
||||
tenantId: string;
|
||||
monthlyLimit: number;
|
||||
usedThisMonth: number;
|
||||
remaining: number;
|
||||
resetDate: string;
|
||||
}
|
||||
22
packages/types/src/analytics.ts
Normal file
22
packages/types/src/analytics.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export enum AnalyticsEventType {
|
||||
LESSON_STARTED = "LESSON_STARTED",
|
||||
LESSON_COMPLETED = "LESSON_COMPLETED",
|
||||
QUIZ_ATTEMPTED = "QUIZ_ATTEMPTED",
|
||||
SIMULATION_JOINED = "SIMULATION_JOINED",
|
||||
TRADE_EXECUTED = "TRADE_EXECUTED",
|
||||
RISK_WARNING_SHOWN = "RISK_WARNING_SHOWN",
|
||||
RISK_WARNING_IGNORED = "RISK_WARNING_IGNORED",
|
||||
PANIC_SELL_DETECTED = "PANIC_SELL_DETECTED",
|
||||
HYPE_TRAP_TRIGGERED = "HYPE_TRAP_TRIGGERED",
|
||||
AI_COACH_CONSULTED = "AI_COACH_CONSULTED",
|
||||
}
|
||||
|
||||
export interface AnalyticsEvent {
|
||||
id: string;
|
||||
eventType: AnalyticsEventType;
|
||||
userId: string;
|
||||
tenantId: string;
|
||||
sessionId: string | null;
|
||||
payload: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
}
|
||||
76
packages/types/src/classroom.ts
Normal file
76
packages/types/src/classroom.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export interface Classroom {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
tenantId: string;
|
||||
teacherId: string;
|
||||
inviteCode: string;
|
||||
students: ClassroomStudent[];
|
||||
assignments: Assignment[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ClassroomStudent {
|
||||
id: string;
|
||||
classroomId: string;
|
||||
userId: string;
|
||||
joinedAt: string;
|
||||
studentName: string;
|
||||
studentEmail: string;
|
||||
}
|
||||
|
||||
export interface Assignment {
|
||||
id: string;
|
||||
classroomId: string;
|
||||
lessonId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
dueDate: string | null;
|
||||
requiredScore: number;
|
||||
maxAttempts: number;
|
||||
submissions: AssignmentSubmission[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AssignmentSubmission {
|
||||
id: string;
|
||||
assignmentId: string;
|
||||
userId: string;
|
||||
score: number | null;
|
||||
completed: boolean;
|
||||
attempts: number;
|
||||
submittedAt: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TeacherReport {
|
||||
classroomId: string;
|
||||
classroomName: string;
|
||||
totalStudents: number;
|
||||
averageScore: number;
|
||||
completionRate: number;
|
||||
studentReports: StudentReport[];
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
export interface StudentReport {
|
||||
userId: string;
|
||||
studentName: string;
|
||||
compositeScore: CompositeScore;
|
||||
lessonsCompleted: number;
|
||||
averageQuizScore: number;
|
||||
simulationsPlayed: number;
|
||||
totalXp: number;
|
||||
lastActivity: string | null;
|
||||
}
|
||||
|
||||
export interface CompositeScore {
|
||||
overall: number;
|
||||
roi: number;
|
||||
riskManagement: number;
|
||||
esgAlignment: number;
|
||||
quizScore: number;
|
||||
lessonCompletion: number;
|
||||
}
|
||||
70
packages/types/src/curriculum.ts
Normal file
70
packages/types/src/curriculum.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export enum BlockType {
|
||||
HERO = "HERO",
|
||||
TEXT = "TEXT",
|
||||
MEDIA = "MEDIA",
|
||||
QUIZ = "QUIZ",
|
||||
CALCULATOR = "CALCULATOR",
|
||||
BUDGET_GAME = "BUDGET_GAME",
|
||||
BLIND_SIMULATION = "BLIND_SIMULATION",
|
||||
DRAG_DROP = "DRAG_DROP",
|
||||
REFLECTION = "REFLECTION",
|
||||
TEACHER_NOTE = "TEACHER_NOTE",
|
||||
ASSIGNMENT = "ASSIGNMENT",
|
||||
SUMMARY = "SUMMARY",
|
||||
}
|
||||
|
||||
export enum CurriculumTrack {
|
||||
SURVIVAL_BASICS = "SURVIVAL_BASICS",
|
||||
CREDIT_DEBT = "CREDIT_DEBT",
|
||||
WEALTH_BUILDER = "WEALTH_BUILDER",
|
||||
ANTI_HYPE = "ANTI_HYPE",
|
||||
MACROECONOMICS = "MACROECONOMICS",
|
||||
}
|
||||
|
||||
export interface LessonBlock {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
order: number;
|
||||
content: Record<string, unknown>;
|
||||
config: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
track: CurriculumTrack;
|
||||
moduleId: string;
|
||||
order: number;
|
||||
estimatedMinutes: number;
|
||||
xpReward: number;
|
||||
blocks: LessonBlock[];
|
||||
tags: string[];
|
||||
prerequisites: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Module {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
track: CurriculumTrack;
|
||||
order: number;
|
||||
lessons: Lesson[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserProgress {
|
||||
userId: string;
|
||||
lessonId: string;
|
||||
completed: boolean;
|
||||
score: number | null;
|
||||
timeSpentSeconds: number;
|
||||
attempts: number;
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
60
packages/types/src/gamification.ts
Normal file
60
packages/types/src/gamification.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export enum XPReason {
|
||||
LESSON_COMPLETED = "LESSON_COMPLETED",
|
||||
QUIZ_PASSED = "QUIZ_PASSED",
|
||||
STREAK_MAINTAINED = "STREAK_MAINTAINED",
|
||||
CHALLENGE_COMPLETED = "CHALLENGE_COMPLETED",
|
||||
SIMULATION_COMPLETED = "SIMULATION_COMPLETED",
|
||||
DIVERSIFICATION = "DIVERSIFICATION",
|
||||
RISK_MANAGEMENT = "RISK_MANAGEMENT",
|
||||
HYPE_AVOIDED = "HYPE_AVOIDED",
|
||||
DEBT_PAID_OFF = "DEBT_PAID_OFF",
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
iconUrl: string;
|
||||
category: "milestone" | "skill" | "behavior" | "special";
|
||||
requiredXp: number | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Achievement {
|
||||
id: string;
|
||||
badgeId: string;
|
||||
userId: string;
|
||||
unlockedAt: string;
|
||||
badge: Badge;
|
||||
}
|
||||
|
||||
export interface UserBadge {
|
||||
id: string;
|
||||
userId: string;
|
||||
badgeId: string;
|
||||
badge: Badge;
|
||||
earnedAt: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface LeaderboardEntry {
|
||||
userId: string;
|
||||
rank: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatarUrl: string | null;
|
||||
xp: number;
|
||||
level: number;
|
||||
badges: number;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
export interface XPEvent {
|
||||
userId: string;
|
||||
amount: number;
|
||||
reason: XPReason;
|
||||
referenceId: string;
|
||||
referenceType: "lesson" | "quiz" | "simulation" | "challenge" | "streak";
|
||||
createdAt: string;
|
||||
}
|
||||
40
packages/types/src/index.ts
Normal file
40
packages/types/src/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export type { User, UserRole } from "./user.js";
|
||||
export type { Tenant, TenantFeatureFlags, AIMode } from "./tenant.js";
|
||||
export type {
|
||||
BlockType,
|
||||
CurriculumTrack,
|
||||
LessonBlock,
|
||||
Lesson,
|
||||
Module,
|
||||
UserProgress,
|
||||
} from "./curriculum.js";
|
||||
export type {
|
||||
SimulationType,
|
||||
SimulationTick,
|
||||
Portfolio,
|
||||
Trade,
|
||||
SimulationSession,
|
||||
} from "./simulation.js";
|
||||
export type {
|
||||
Badge,
|
||||
Achievement,
|
||||
UserBadge,
|
||||
LeaderboardEntry,
|
||||
XPEvent,
|
||||
XPReason,
|
||||
} from "./gamification.js";
|
||||
export type {
|
||||
Classroom,
|
||||
ClassroomStudent,
|
||||
Assignment,
|
||||
AssignmentSubmission,
|
||||
TeacherReport,
|
||||
CompositeScore,
|
||||
} from "./classroom.js";
|
||||
export type { AnalyticsEvent, AnalyticsEventType } from "./analytics.js";
|
||||
export type {
|
||||
AICoachMessage,
|
||||
AICoachContext,
|
||||
AIModeConfig,
|
||||
AITokenBudget,
|
||||
} from "./ai.js";
|
||||
91
packages/types/src/simulation.ts
Normal file
91
packages/types/src/simulation.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export enum SimulationType {
|
||||
BUDGET_CHALLENGE = "BUDGET_CHALLENGE",
|
||||
EMERGENCY_FUND = "EMERGENCY_FUND",
|
||||
COST_OF_LIVING = "COST_OF_LIVING",
|
||||
BLIND_ASSET = "BLIND_ASSET",
|
||||
MARKET_CRASH = "MARKET_CRASH",
|
||||
RENT_VS_TRANSPORT = "RENT_VS_TRANSPORT",
|
||||
SAVINGS_HABIT = "SAVINGS_HABIT",
|
||||
ESG_PORTFOLIO = "ESG_PORTFOLIO",
|
||||
INTEREST_APY = "INTEREST_APY",
|
||||
DEBT_REPAYMENT = "DEBT_REPAYMENT",
|
||||
}
|
||||
|
||||
export interface SimulationTick {
|
||||
tick: number;
|
||||
label: string;
|
||||
values: Record<string, number>;
|
||||
events: SimulationEvent[];
|
||||
}
|
||||
|
||||
export interface SimulationEvent {
|
||||
type: string;
|
||||
description: string;
|
||||
impact: Record<string, number>;
|
||||
choice?: {
|
||||
options: SimulationChoice[];
|
||||
chosenOptionId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SimulationChoice {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
impacts: Record<string, number>;
|
||||
isTrap?: boolean;
|
||||
trapReveal?: string;
|
||||
}
|
||||
|
||||
export interface Portfolio {
|
||||
cash: number;
|
||||
investments: Investment[];
|
||||
debt: Debt[];
|
||||
monthlyIncome: number;
|
||||
monthlyExpenses: number;
|
||||
}
|
||||
|
||||
export interface Investment {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "stock" | "bond" | "etf" | "crypto" | "real_estate" | "commodity" | "cash";
|
||||
amount: number;
|
||||
value: number;
|
||||
risk: "low" | "medium" | "high";
|
||||
esgScore: number | null;
|
||||
}
|
||||
|
||||
export interface Debt {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "credit_card" | "student_loan" | "mortgage" | "personal_loan";
|
||||
principal: number;
|
||||
interestRate: number;
|
||||
minimumPayment: number;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
assetName: string;
|
||||
type: "buy" | "sell";
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalValue: number;
|
||||
timestamp: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SimulationSession {
|
||||
id: string;
|
||||
userId: string;
|
||||
simulationType: SimulationType;
|
||||
ticks: SimulationTick[];
|
||||
portfolio: Portfolio;
|
||||
trades: Trade[];
|
||||
score: number;
|
||||
decisions: number;
|
||||
completed: boolean;
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
32
packages/types/src/tenant.ts
Normal file
32
packages/types/src/tenant.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export enum AIMode {
|
||||
MANAGED = "MANAGED",
|
||||
BYOK = "BYOK",
|
||||
DISABLED = "DISABLED",
|
||||
}
|
||||
|
||||
export interface TenantFeatureFlags {
|
||||
aiCoach: boolean;
|
||||
classroom: boolean;
|
||||
simulations: boolean;
|
||||
gamification: boolean;
|
||||
tenantDashboard: boolean;
|
||||
researchProgram: boolean;
|
||||
}
|
||||
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
domain: string | null;
|
||||
branding: {
|
||||
logoUrl: string | null;
|
||||
primaryColor: string | null;
|
||||
accentColor: string | null;
|
||||
faviconUrl: string | null;
|
||||
} | null;
|
||||
features: TenantFeatureFlags;
|
||||
aiMode: AIMode;
|
||||
monthlyTokenBudget: number;
|
||||
defaultLocale: string;
|
||||
excludeFromResearch: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
28
packages/types/src/user.ts
Normal file
28
packages/types/src/user.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export enum UserRole {
|
||||
STUDENT = "STUDENT",
|
||||
TEACHER = "TEACHER",
|
||||
TENANT_ADMIN = "TENANT_ADMIN",
|
||||
PLATFORM_ADMIN = "PLATFORM_ADMIN",
|
||||
CONTENT_MANAGER = "CONTENT_MANAGER",
|
||||
RESEARCHER = "RESEARCHER",
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
role: UserRole;
|
||||
locale: string;
|
||||
tenantId: string;
|
||||
xp: number;
|
||||
level: number;
|
||||
streaks: {
|
||||
current: number;
|
||||
longest: number;
|
||||
lastActivityDate: string | null;
|
||||
};
|
||||
avatarUrl: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
9
packages/types/tsconfig.json
Normal file
9
packages/types/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", ".turbo"]
|
||||
}
|
||||
41
packages/ui/package.json
Normal file
41
packages/ui/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@investplay/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "InvestPlay shared UI component library",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\" --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"tailwindcss": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"@radix-ui/react-dialog": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"lucide-react": "^0.490.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"typescript": "^5.8.0",
|
||||
"eslint": "^8.57.0",
|
||||
"rimraf": "^6.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/ui/src/index.ts
Normal file
1
packages/ui/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { cn, formatCurrency, formatXP, formatPercentage } from "./lib/utils.js";
|
||||
50
packages/ui/src/lib/utils.ts
Normal file
50
packages/ui/src/lib/utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currency = "EUR",
|
||||
locale = "en",
|
||||
): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
} catch {
|
||||
return `${currency} ${amount.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatXP(xp: number, locale = "en"): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(xp);
|
||||
} catch {
|
||||
return `${xp}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPercentage(
|
||||
value: number,
|
||||
locale = "en",
|
||||
decimals = 1,
|
||||
): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "percent",
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value / 100);
|
||||
} catch {
|
||||
return `${value.toFixed(decimals)}%`;
|
||||
}
|
||||
}
|
||||
12
packages/ui/tsconfig.json
Normal file
12
packages/ui/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist", ".turbo"]
|
||||
}
|
||||
23
packages/utils/package.json
Normal file
23
packages/utils/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@investplay/utils",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "InvestPlay shared utility functions",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\" --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^3.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"rimraf": "^6.0.0"
|
||||
}
|
||||
}
|
||||
34
packages/utils/src/constants.ts
Normal file
34
packages/utils/src/constants.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const SUPPORTED_LOCALES = ["en", "el"] as const;
|
||||
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||
|
||||
export const DEFAULT_LOCALE: SupportedLocale = "en";
|
||||
|
||||
export const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
EUR: "€",
|
||||
USD: "$",
|
||||
GBP: "£",
|
||||
JPY: "¥",
|
||||
CNY: "¥",
|
||||
BTC: "₿",
|
||||
ETH: "Ξ",
|
||||
};
|
||||
|
||||
export const AGE_BRACKETS = [
|
||||
{ min: 13, max: 15, label: "13-15" },
|
||||
{ min: 16, max: 18, label: "16-18" },
|
||||
{ min: 19, max: 24, label: "19-24" },
|
||||
{ min: 25, max: 35, label: "25-35" },
|
||||
{ min: 36, max: 50, label: "36-50" },
|
||||
{ min: 51, max: 99, label: "51+" },
|
||||
] as const;
|
||||
|
||||
export const LESSON_TIME_MIN = 5;
|
||||
export const LESSON_TIME_MAX = 45;
|
||||
|
||||
export const MAX_DAILY_AI_MESSAGES = 50;
|
||||
|
||||
export const MAX_MONTHLY_TOKENS = {
|
||||
managed: 1_000_000,
|
||||
byok: 10_000_000,
|
||||
disabled: 0,
|
||||
} as const;
|
||||
52
packages/utils/src/env.ts
Normal file
52
packages/utils/src/env.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Parse a string environment variable into a boolean.
|
||||
* Accepts: "true", "1", "yes" (case-insensitive) → true; everything else → false.
|
||||
*/
|
||||
export function parseEnvBoolean(value: string | undefined, defaultValue = false): boolean {
|
||||
if (value === undefined || value === null) return defaultValue;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "true" || normalized === "1" || normalized === "yes";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string environment variable into a number with optional bounds.
|
||||
*/
|
||||
export function parseEnvNumber(
|
||||
value: string | undefined,
|
||||
defaultValue: number,
|
||||
options?: { min?: number; max?: number },
|
||||
): number {
|
||||
if (value === undefined || value === null || value.trim() === "") return defaultValue;
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) return defaultValue;
|
||||
if (options?.min !== undefined && parsed < options.min) return options.min;
|
||||
if (options?.max !== undefined && parsed > options.max) return options.max;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string environment variable into one of the allowed enum values.
|
||||
* Returns the default value if the input doesn't match any allowed value.
|
||||
*/
|
||||
export function parseEnvEnum<T extends string>(
|
||||
value: string | undefined,
|
||||
allowedValues: readonly T[],
|
||||
defaultValue: T,
|
||||
): T {
|
||||
if (value === undefined || value === null) return defaultValue;
|
||||
const normalized = value.trim() as T;
|
||||
if (allowedValues.includes(normalized)) return normalized;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a comma-separated string environment variable into an array of strings.
|
||||
* Trims each entry and filters out empty strings.
|
||||
*/
|
||||
export function parseEnvArray(value: string | undefined, defaultValue: string[] = []): string[] {
|
||||
if (value === undefined || value === null || value.trim() === "") return defaultValue;
|
||||
return value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
}
|
||||
95
packages/utils/src/formatting.ts
Normal file
95
packages/utils/src/formatting.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
locale = "en",
|
||||
currency = "EUR",
|
||||
): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
} catch {
|
||||
return `${currency} ${amount.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(
|
||||
date: Date | string,
|
||||
locale = "en",
|
||||
options?: Intl.DateTimeFormatOptions,
|
||||
): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
...options,
|
||||
}).format(d);
|
||||
} catch {
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
|
||||
export function formatRelativeTime(
|
||||
date: Date | string,
|
||||
locale = "en",
|
||||
): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
try {
|
||||
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
||||
|
||||
if (diffSeconds < 60) return rtf.format(-diffSeconds, "second");
|
||||
if (diffMinutes < 60) return rtf.format(-diffMinutes, "minute");
|
||||
if (diffHours < 24) return rtf.format(-diffHours, "hour");
|
||||
if (diffDays < 30) return rtf.format(-diffDays, "day");
|
||||
return formatDate(d, locale);
|
||||
} catch {
|
||||
if (diffDays > 0) return `${diffDays}d ago`;
|
||||
if (diffHours > 0) return `${diffHours}h ago`;
|
||||
if (diffMinutes > 0) return `${diffMinutes}m ago`;
|
||||
return "just now";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatNumber(
|
||||
value: number,
|
||||
locale = "en",
|
||||
options?: Intl.NumberFormatOptions,
|
||||
): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, options).format(value);
|
||||
} catch {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPercentage(
|
||||
value: number,
|
||||
locale = "en",
|
||||
decimals = 1,
|
||||
): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "percent",
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value / 100);
|
||||
} catch {
|
||||
return `${value.toFixed(decimals)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
export function truncateText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength).trimEnd() + "...";
|
||||
}
|
||||
26
packages/utils/src/index.ts
Normal file
26
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export {
|
||||
emailSchema,
|
||||
passwordSchema,
|
||||
tenantNameSchema,
|
||||
localeSchema,
|
||||
dailyMessageLimitSchema,
|
||||
simulationTradeSchema,
|
||||
} from "./validation.js";
|
||||
export {
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
formatNumber,
|
||||
formatPercentage,
|
||||
truncateText,
|
||||
} from "./formatting.js";
|
||||
export {
|
||||
SUPPORTED_LOCALES,
|
||||
DEFAULT_LOCALE,
|
||||
CURRENCY_SYMBOLS,
|
||||
AGE_BRACKETS,
|
||||
LESSON_TIME_MIN,
|
||||
LESSON_TIME_MAX,
|
||||
MAX_DAILY_AI_MESSAGES,
|
||||
MAX_MONTHLY_TOKENS,
|
||||
} from "./constants.js";
|
||||
44
packages/utils/src/validation.ts
Normal file
44
packages/utils/src/validation.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const emailSchema = z
|
||||
.string()
|
||||
.email("Invalid email address")
|
||||
.max(255, "Email must be under 255 characters")
|
||||
.transform((v) => v.toLowerCase().trim());
|
||||
|
||||
export const passwordSchema = z
|
||||
.string()
|
||||
.min(8, "Password must be at least 8 characters")
|
||||
.max(128, "Password must be under 128 characters")
|
||||
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
|
||||
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
|
||||
.regex(/[0-9]/, "Password must contain at least one number")
|
||||
.regex(
|
||||
/[^A-Za-z0-9]/,
|
||||
"Password must contain at least one special character",
|
||||
);
|
||||
|
||||
export const tenantNameSchema = z
|
||||
.string()
|
||||
.min(2, "Tenant name must be at least 2 characters")
|
||||
.max(100, "Tenant name must be under 100 characters")
|
||||
.regex(
|
||||
/^[a-zA-Z0-9\s\-'&.]+$/,
|
||||
"Tenant name contains invalid characters",
|
||||
);
|
||||
|
||||
export const localeSchema = z.enum(["en", "el"]);
|
||||
|
||||
export const dailyMessageLimitSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, "Minimum 1 message per day")
|
||||
.max(500, "Maximum 500 messages per day");
|
||||
|
||||
export const simulationTradeSchema = z.object({
|
||||
assetName: z.string().min(1, "Asset name is required"),
|
||||
type: z.enum(["buy", "sell"]),
|
||||
quantity: z.number().positive("Quantity must be positive"),
|
||||
price: z.number().positive("Price must be positive"),
|
||||
reason: z.string().max(500, "Reason must be under 500 characters").optional(),
|
||||
});
|
||||
11
packages/utils/tsconfig.json
Normal file
11
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", ".turbo"]
|
||||
}
|
||||
Reference in New Issue
Block a user