fix: Backend added along with missing pages
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled
This commit is contained in:
26
webos/.eslintrc.cjs
Normal file
26
webos/.eslintrc.cjs
Normal file
@@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2022: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'react-hooks'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
ignorePatterns: ['dist', 'node_modules'],
|
||||
};
|
||||
2436
webos/package-lock.json
generated
2436
webos/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,19 +12,59 @@
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-three/drei": "^10.0.0",
|
||||
"@react-three/fiber": "^9.0.0",
|
||||
"@tanstack/react-query": "^5.51.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^11.3.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^1.14.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.74.0",
|
||||
"react-resizable-panels": "^4.10.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"recharts": "^3.8.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"three": "0.168.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/three": "^0.168.0",
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function ShowroomMode() {
|
||||
const navigate = useNavigate();
|
||||
const [phase, setPhase] = useState<ShowroomPhase>('live');
|
||||
const [elapsed, setElapsed] = useState(0); // seconds
|
||||
const timerRef = useRef<ReturnType<typeof setInterval>>();
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const {
|
||||
isShowroomActive,
|
||||
@@ -43,12 +43,14 @@ export default function ShowroomMode() {
|
||||
// Session timer
|
||||
useEffect(() => {
|
||||
timerRef.current = setInterval(() => setElapsed(s => s + 1), 1000);
|
||||
return () => clearInterval(timerRef.current);
|
||||
return () => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// End session → summary
|
||||
const handleEndSession = () => {
|
||||
clearInterval(timerRef.current);
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
setPhase('summary');
|
||||
setShowroomActive(false);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useConversations } from '../../../shared/hooks/useClient360';
|
||||
import { useConversations } from '@/shared/hooks/useClient360';
|
||||
import styles from './Conversations.module.css';
|
||||
|
||||
/**
|
||||
|
||||
@@ -178,7 +178,7 @@ export default function PropertyEntity() {
|
||||
// ── 3D Model component (R3F) ──────────────────────────────────
|
||||
function PropertyModel({ url }: { url: string }) {
|
||||
const { scene } = useGLTF(url);
|
||||
const meshRef = useRef<any>();
|
||||
const meshRef = useRef<any>(null);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (meshRef.current) {
|
||||
@@ -191,7 +191,7 @@ function PropertyModel({ url }: { url: string }) {
|
||||
|
||||
// Placeholder building geometry when no GLB model available
|
||||
function PlaceholderBuilding() {
|
||||
const ref = useRef<any>();
|
||||
const ref = useRef<any>(null);
|
||||
useFrame((_, delta) => {
|
||||
if (ref.current) ref.current.rotation.y += delta * 0.1;
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ export function AdminGuard({ children }: { children: React.ReactNode }) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
const isAdmin =
|
||||
user?.role === 'ADMIN' || user?.role === 'SALES_DIRECTOR';
|
||||
const role = String(user?.role ?? '').trim().toUpperCase();
|
||||
const isAdmin = role === 'ADMIN' || role === 'SALES_DIRECTOR' || role === 'SUPERADMIN';
|
||||
|
||||
if (!isAdmin) {
|
||||
// Silent redirect — no error screen (security through obscurity)
|
||||
|
||||
@@ -34,7 +34,8 @@ const PILLARS = [
|
||||
|
||||
export function NavRail() {
|
||||
const { user } = useAuthStore();
|
||||
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SALES_DIRECTOR';
|
||||
const role = String(user?.role ?? '').trim().toUpperCase();
|
||||
const isAdmin = role === 'ADMIN' || role === 'SALES_DIRECTOR' || role === 'SUPERADMIN';
|
||||
|
||||
return (
|
||||
<nav className={styles.rail} aria-label="Main navigation">
|
||||
|
||||
@@ -118,14 +118,20 @@ function ChartTooltipContent({
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
}: React.ComponentProps<"div"> & {
|
||||
active?: boolean
|
||||
payload?: any[]
|
||||
label?: any
|
||||
labelFormatter?: (value: any, payload: any[]) => React.ReactNode
|
||||
labelClassName?: string
|
||||
formatter?: (...args: any[]) => React.ReactNode
|
||||
color?: string
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
@@ -258,11 +264,12 @@ function ChartLegendContent({
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
}: React.ComponentProps<"div"> & {
|
||||
payload?: any[]
|
||||
verticalAlign?: "top" | "bottom" | "middle"
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
|
||||
68
webos/src/shared/utils/curveGenerator.ts
Normal file
68
webos/src/shared/utils/curveGenerator.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* curveGenerator.ts
|
||||
* Converts an array of {x, y} data points into a smooth SVG cubic Bezier path string.
|
||||
* Uses a Catmull-Rom → Bezier conversion for monotone-Y curves.
|
||||
*/
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a smooth SVG path string from an array of points using
|
||||
* cubic Bezier curves (Catmull-Rom spline converted to Bezier).
|
||||
*
|
||||
* @param points Array of {x, y} pixel coordinates (already mapped from data).
|
||||
* @param tension Controls curve tightness. 0 = straight lines, 1 = very curvy. Default 0.4.
|
||||
* @returns SVG path `d` attribute string.
|
||||
*/
|
||||
export function generateSmoothPath(points: Point[], tension = 0.4): string {
|
||||
if (points.length === 0) return '';
|
||||
if (points.length === 1) return `M ${points[0].x},${points[0].y}`;
|
||||
|
||||
const d: string[] = [`M ${points[0].x},${points[0].y}`];
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[Math.max(i - 1, 0)];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1];
|
||||
const p3 = points[Math.min(i + 2, points.length - 1)];
|
||||
|
||||
// Catmull-Rom control points
|
||||
const cp1x = p1.x + ((p2.x - p0.x) / 6) * tension * 3;
|
||||
const cp1y = p1.y + ((p2.y - p0.y) / 6) * tension * 3;
|
||||
const cp2x = p2.x - ((p3.x - p1.x) / 6) * tension * 3;
|
||||
const cp2y = p2.y - ((p3.y - p1.y) / 6) * tension * 3;
|
||||
|
||||
d.push(`C ${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(2)},${cp2y.toFixed(2)} ${p2.x},${p2.y}`);
|
||||
}
|
||||
|
||||
return d.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a sentiment score (0–100) to a pixel X coordinate within a container.
|
||||
*
|
||||
* @param score Sentiment score 0–100.
|
||||
* @param width Container pixel width.
|
||||
* @param padding Left/right padding in pixels. Default 40.
|
||||
*/
|
||||
export function sentimentToX(score: number, width: number, padding = 40): number {
|
||||
const usable = width - padding * 2;
|
||||
return padding + (score / 100) * usable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an event index to a pixel Y coordinate within a container.
|
||||
*
|
||||
* @param index Event index (0 = top).
|
||||
* @param total Total number of events.
|
||||
* @param height Container pixel height.
|
||||
* @param padding Top/bottom padding in pixels. Default 40.
|
||||
*/
|
||||
export function indexToY(index: number, total: number, height: number, padding = 40): number {
|
||||
if (total <= 1) return height / 2;
|
||||
const usable = height - padding * 2;
|
||||
return padding + (index / (total - 1)) * usable;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: 'SALES_BROKER' | 'SALES_DIRECTOR' | 'ADMIN';
|
||||
role: 'SALES_BROKER' | 'SALES_DIRECTOR' | 'ADMIN' | 'SUPERADMIN' | string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ export const useAuthStore = create<AuthStore>()(
|
||||
}),
|
||||
{
|
||||
name: 'velocity-auth',
|
||||
partialize: (state) => ({ token: state.token, user: state.user }),
|
||||
partialize: (state) => ({
|
||||
token: state.token,
|
||||
user: state.user,
|
||||
isAuthenticated: Boolean(state.token && state.user),
|
||||
}),
|
||||
}
|
||||
),
|
||||
{ name: 'AuthStore' }
|
||||
|
||||
6
webos/src/vite-env.d.ts
vendored
Normal file
6
webos/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: Record<string, string>;
|
||||
export default classes;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
@@ -15,9 +16,15 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/ui/*": ["./src/shared/ui/*"],
|
||||
"@/hooks/*": ["./src/shared/hooks/*"],
|
||||
"@/lib/*": ["./src/shared/lib/*"],
|
||||
"@/store/*": ["./src/store/*"],
|
||||
"@/types": ["./src/shared/types/index.ts"],
|
||||
"@/types/*": ["./src/shared/types/*"],
|
||||
"@/utils/*": ["./src/shared/utils/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@/components/ui': path.resolve(__dirname, './src/shared/ui'),
|
||||
'@/hooks': path.resolve(__dirname, './src/shared/hooks'),
|
||||
'@/lib': path.resolve(__dirname, './src/shared/lib'),
|
||||
'@/store': path.resolve(__dirname, './src/store'),
|
||||
'@/types': path.resolve(__dirname, './src/shared/types'),
|
||||
'@/utils': path.resolve(__dirname, './src/shared/utils'),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user