Skip to content

Commit e7f742d

Browse files
committed
adjust app cards
1 parent d5e6c14 commit e7f742d

3 files changed

Lines changed: 8 additions & 307 deletions

File tree

dev-dist/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
8282
"revision": "3ca0b8505b4bec776b69afdba2768812"
8383
}, {
8484
"url": "index.html",
85-
"revision": "0.4fs9ilpf6t8"
85+
"revision": "0.30lktn5ur2s"
8686
}], {});
8787
workbox.cleanupOutdatedCaches();
8888
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

src/components/dashboard/AppCard3D.tsx

Lines changed: 7 additions & 298 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
1-
import React, { useEffect, useRef } from "react";
1+
import React from "react";
22
import { Link } from "react-router-dom";
33
import { ArrowRight } from "lucide-react";
44
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
55
import { AnimatedButton } from "@/components/ui/animated-button";
6-
import * as THREE from "three";
7-
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
86
import { cn } from "@/lib/utils";
97
import { AnimatedIcon } from "@/components/ui/animated-icon";
108
import { LucideIcon } from "lucide-react";
11-
import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js";
129

1310
type Props = {
1411
title: string;
1512
description: string;
1613
to: string;
17-
modelPath?: string;
1814
icon?: LucideIcon;
1915
colorClass?: string;
20-
preset?: "lowpoly-bot" | "pyramid" | "nodes-graph" | "workshop-tools" | "picture-frame" | "old-book" | "airplane" | "technician";
2116
};
2217

23-
export const AppCard3D: React.FC<Props> = ({ title, description, to, modelPath, icon, colorClass = "bg-primary", preset }) => {
24-
const canvasRef = useRef<HTMLCanvasElement | null>(null);
25-
const containerRef = useRef<HTMLDivElement | null>(null);
18+
export const AppCard3D: React.FC<Props> = ({ title, description, to, icon, colorClass = "bg-primary" }) => {
2619
const Line = ({ className = "" }) => (
2720
<div
2821
className={cn(
@@ -40,301 +33,17 @@ export const AppCard3D: React.FC<Props> = ({ title, description, to, modelPath,
4033
</>
4134
);
4235

43-
useEffect(() => {
44-
const canvas = canvasRef.current;
45-
const container = containerRef.current;
46-
if (!canvas || !container) return;
47-
48-
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
49-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
50-
renderer.setClearColor(0x000000, 0);
51-
52-
const scene = new THREE.Scene();
53-
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
54-
camera.position.set(2, 1.5, 3);
55-
camera.lookAt(0, 0, 0);
56-
57-
const hemi = new THREE.HemisphereLight(0xffffff, 0x202020, 0.8);
58-
scene.add(hemi);
59-
const dir = new THREE.DirectionalLight(0xffffff, 0.7);
60-
dir.position.set(3, 5, 2);
61-
scene.add(dir);
62-
const dir2 = new THREE.DirectionalLight(0xffffff, 0.3);
63-
dir2.position.set(-4, -3, -2);
64-
scene.add(dir2);
65-
66-
const group = new THREE.Group();
67-
scene.add(group);
68-
69-
let loaded: THREE.Object3D | null = null;
70-
71-
const centerAndScale = (obj: THREE.Object3D) => {
72-
const pivot = new THREE.Group();
73-
pivot.add(obj);
74-
const box = new THREE.Box3().setFromObject(pivot);
75-
const size = new THREE.Vector3();
76-
box.getSize(size);
77-
const maxDim = Math.max(size.x, size.y, size.z) || 1;
78-
const scale = 1.8 / maxDim;
79-
pivot.scale.setScalar(scale);
80-
const box2 = new THREE.Box3().setFromObject(pivot);
81-
const center = new THREE.Vector3();
82-
box2.getCenter(center);
83-
pivot.position.set(-center.x, -center.y, -center.z);
84-
return pivot;
85-
};
86-
87-
if (preset === "lowpoly-bot") {
88-
const bot = new THREE.Group();
89-
const matBody = new THREE.MeshPhysicalMaterial({ color: 0x7c3aed, metalness: 0.35, roughness: 0.35, clearcoat: 0.25 });
90-
const matAccent = new THREE.MeshPhysicalMaterial({ color: 0x22d3ee, metalness: 0.4, roughness: 0.28, clearcoat: 0.15, emissive: 0x0b9edb, emissiveIntensity: 0.2 });
91-
const matDark = new THREE.MeshPhysicalMaterial({ color: 0x1f2937, metalness: 0.15, roughness: 0.7 });
92-
93-
const body = new THREE.Mesh(new RoundedBoxGeometry(1.2, 0.9, 0.6, 4, 0.18), matBody);
94-
body.position.set(0, -0.1, 0);
95-
bot.add(body);
96-
97-
const head = new THREE.Mesh(new RoundedBoxGeometry(0.9, 0.6, 0.6, 4, 0.16), matBody);
98-
head.position.set(0, 0.6, 0);
99-
bot.add(head);
100-
101-
const eyeL = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.02, 8), matAccent);
102-
eyeL.rotation.x = Math.PI / 2;
103-
eyeL.position.set(-0.22, 0.65, 0.32);
104-
const eyeR = eyeL.clone();
105-
eyeR.position.x = 0.22;
106-
bot.add(eyeL, eyeR);
107-
108-
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 0.3, 6), matDark);
109-
stem.position.set(0, 0.95, 0);
110-
const tip = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), matAccent);
111-
tip.position.set(0, 1.12, 0);
112-
bot.add(stem, tip);
113-
114-
const armL = new THREE.Mesh(new RoundedBoxGeometry(0.2, 0.6, 0.2, 2, 0.08), matBody);
115-
armL.position.set(-0.8, 0.1, 0);
116-
const armR = armL.clone();
117-
armR.position.x = 0.8;
118-
bot.add(armL, armR);
119-
120-
const visor = new THREE.Mesh(new RoundedBoxGeometry(0.5, 0.22, 0.05, 2, 0.05), matDark);
121-
visor.position.set(0, 0.65, 0.33);
122-
bot.add(visor);
123-
124-
const pivot = centerAndScale(bot);
125-
loaded = pivot;
126-
group.add(pivot);
127-
} else if (preset === "pyramid") {
128-
const g = new THREE.Group();
129-
const matSand = new THREE.MeshPhysicalMaterial({ color: 0xC8A76F, roughness: 0.65, metalness: 0.06 });
130-
const levels = 4;
131-
for (let i = 0; i < levels; i++) {
132-
const s = 1.4 - i * 0.3;
133-
const h = 0.22 + i * 0.02;
134-
const step = new THREE.Mesh(new RoundedBoxGeometry(s, h, s, 2, 0.04), matSand);
135-
step.position.y = -0.5 + i * h + i * 0.05;
136-
g.add(step);
137-
}
138-
const cap = new THREE.Mesh(new THREE.ConeGeometry(0.35, 0.35, 4), matSand);
139-
cap.rotation.y = Math.PI / 4;
140-
cap.position.y = 0.15;
141-
g.add(cap);
142-
const base = new THREE.Mesh(new RoundedBoxGeometry(1.8, 0.08, 1.8, 3, 0.05), new THREE.MeshPhysicalMaterial({ color: 0x9E7F4F, roughness: 0.8 }));
143-
base.position.y = -0.54;
144-
g.add(base);
145-
const pivot = centerAndScale(g);
146-
loaded = pivot; group.add(pivot);
147-
} else if (preset === "nodes-graph") {
148-
const g = new THREE.Group();
149-
const matNode = new THREE.MeshPhysicalMaterial({ color: 0x4F46E5, roughness: 0.35, metalness: 0.25, clearcoat: 0.1 });
150-
const matEdge = new THREE.MeshPhysicalMaterial({ color: 0x22D3EE, roughness: 0.25, metalness: 0.25 });
151-
const positions = [
152-
new THREE.Vector3(-0.9, 0.5, 0),
153-
new THREE.Vector3(0.9, 0.5, 0.1),
154-
new THREE.Vector3(-0.6, -0.6, -0.1),
155-
new THREE.Vector3(0.6, -0.6, 0),
156-
new THREE.Vector3(0, 0.1, 0.6),
157-
new THREE.Vector3(0.2, -0.1, -0.6),
158-
new THREE.Vector3(-0.2, 0.3, -0.4),
159-
new THREE.Vector3(0.4, 0.2, 0.4),
160-
];
161-
positions.forEach(p => {
162-
const n = new THREE.Mesh(new THREE.SphereGeometry(0.16, 20, 20), matNode);
163-
n.position.copy(p);
164-
g.add(n);
165-
});
166-
const connectCurve = (a: THREE.Vector3, b: THREE.Vector3, off: THREE.Vector3) => {
167-
const curve = new THREE.QuadraticBezierCurve3(a, new THREE.Vector3().addVectors(a, b).multiplyScalar(0.5).add(off), b);
168-
const tube = new THREE.Mesh(new THREE.TubeGeometry(curve, 20, 0.035, 8, false), matEdge);
169-
g.add(tube);
170-
};
171-
connectCurve(positions[0], positions[2], new THREE.Vector3(0, 0.2, 0));
172-
connectCurve(positions[1], positions[3], new THREE.Vector3(0, 0.25, 0.15));
173-
connectCurve(positions[0], positions[4], new THREE.Vector3(0.1, 0.15, 0));
174-
connectCurve(positions[1], positions[4], new THREE.Vector3(-0.1, 0.15, 0));
175-
connectCurve(positions[2], positions[3], new THREE.Vector3(0, -0.15, 0));
176-
connectCurve(positions[6], positions[7], new THREE.Vector3(0.1, -0.05, 0.1));
177-
const pivot = centerAndScale(g);
178-
loaded = pivot; group.add(pivot);
179-
} else if (preset === "workshop-tools") {
180-
const g = new THREE.Group();
181-
const matMetal = new THREE.MeshPhysicalMaterial({ color: 0x9CA3AF, roughness: 0.4, metalness: 0.7, clearcoat: 0.1 });
182-
const matWood = new THREE.MeshPhysicalMaterial({ color: 0x8B5E3C, roughness: 0.65, metalness: 0.12 });
183-
const handle = new THREE.Mesh(new RoundedBoxGeometry(0.15, 0.7, 0.15, 3, 0.05), matWood);
184-
handle.position.set(-0.4, 0.0, 0);
185-
const head = new THREE.Mesh(new RoundedBoxGeometry(0.45, 0.18, 0.18, 3, 0.06), matMetal);
186-
head.position.set(-0.4, 0.35, 0);
187-
g.add(handle, head);
188-
const jaw = new THREE.Mesh(new RoundedBoxGeometry(0.45, 0.12, 0.18, 3, 0.06), matMetal);
189-
jaw.position.set(0.35, 0.18, 0);
190-
const shaft = new THREE.Mesh(new RoundedBoxGeometry(0.12, 0.6, 0.12, 3, 0.05), matMetal);
191-
shaft.position.set(0.35, -0.15, 0);
192-
g.add(jaw, shaft);
193-
const screwdriverHandle = new THREE.Mesh(new RoundedBoxGeometry(0.12, 0.35, 0.12, 3, 0.05), new THREE.MeshPhysicalMaterial({ color: 0xEF4444, roughness: 0.5, metalness: 0.2 }));
194-
screwdriverHandle.position.set(0.05, -0.1, 0.25);
195-
const screwdriverShaft = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.35, 12), matMetal);
196-
screwdriverShaft.position.set(0.05, 0.15, 0.25);
197-
g.add(screwdriverHandle, screwdriverShaft);
198-
const pivot = centerAndScale(g);
199-
loaded = pivot; group.add(pivot);
200-
} else if (preset === "picture-frame") {
201-
const g = new THREE.Group();
202-
const matFrame = new THREE.MeshPhysicalMaterial({ color: 0xA78BFA, roughness: 0.45, metalness: 0.25, clearcoat: 0.15 });
203-
const frame = new THREE.Mesh(new RoundedBoxGeometry(1.3, 1.0, 0.12, 4, 0.12), matFrame);
204-
const inner = new THREE.Mesh(new RoundedBoxGeometry(0.9, 0.6, 0.02, 3, 0.03), new THREE.MeshPhysicalMaterial({ color: 0x0f172a, roughness: 0.85 }));
205-
inner.position.z = 0.06;
206-
g.add(frame, inner);
207-
const matBorder = new THREE.MeshPhysicalMaterial({ color: 0xffffff, roughness: 0.9 });
208-
const mat1 = new THREE.Mesh(new RoundedBoxGeometry(1.0, 0.8, 0.015, 2, 0.02), matBorder);
209-
mat1.position.z = 0.055;
210-
g.add(mat1);
211-
const pivot = centerAndScale(g);
212-
loaded = pivot; group.add(pivot);
213-
} else if (preset === "old-book") {
214-
const g = new THREE.Group();
215-
const cover = new THREE.Mesh(new RoundedBoxGeometry(1.1, 0.7, 0.2, 4, 0.08), new THREE.MeshPhysicalMaterial({ color: 0x6B4E2E, roughness: 0.7, metalness: 0.08 }));
216-
const pages = new THREE.Mesh(new RoundedBoxGeometry(1.02, 0.62, 0.18, 2, 0.05), new THREE.MeshPhysicalMaterial({ color: 0xF2E9D8, roughness: 0.95 }));
217-
pages.position.set(0, 0, 0.01);
218-
g.add(cover, pages);
219-
const strap = new THREE.Mesh(new RoundedBoxGeometry(1.15, 0.08, 0.05, 2, 0.02), new THREE.MeshPhysicalMaterial({ color: 0x3f3f46, roughness: 0.6 }));
220-
strap.position.set(0, 0, 0.12);
221-
g.add(strap);
222-
const pivot = centerAndScale(g);
223-
loaded = pivot; group.add(pivot);
224-
} else if (preset === "airplane") {
225-
const g = new THREE.Group();
226-
const mat = new THREE.MeshPhysicalMaterial({ color: 0x60A5FA, metalness: 0.45, roughness: 0.35, clearcoat: 0.2 });
227-
const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.22, 1.1, 8, 16), mat);
228-
body.rotation.z = Math.PI / 2;
229-
const wing = new THREE.Mesh(new RoundedBoxGeometry(1.0, 0.06, 0.3, 3, 0.06), mat);
230-
const tail = new THREE.Mesh(new RoundedBoxGeometry(0.3, 0.06, 0.22, 3, 0.05), mat);
231-
wing.position.set(0, 0, 0);
232-
tail.position.set(-0.45, 0.18, 0);
233-
const finV = new THREE.Mesh(new RoundedBoxGeometry(0.18, 0.25, 0.06, 2, 0.04), mat);
234-
finV.position.set(-0.5, 0.22, 0);
235-
const finH = new THREE.Mesh(new RoundedBoxGeometry(0.25, 0.06, 0.15, 2, 0.04), mat);
236-
finH.position.set(-0.52, 0.1, 0);
237-
const propHub = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.1, 12), mat);
238-
propHub.rotation.x = Math.PI / 2;
239-
propHub.position.set(0.55, 0, 0);
240-
const blade1 = new THREE.Mesh(new RoundedBoxGeometry(0.02, 0.35, 0.08, 2, 0.01), mat);
241-
blade1.position.set(0.55, 0.18, 0);
242-
const blade2 = blade1.clone();
243-
blade2.position.set(0.55, -0.18, 0);
244-
g.add(body, wing, tail, finV, finH, propHub, blade1, blade2);
245-
const pivot = centerAndScale(g);
246-
loaded = pivot; group.add(pivot);
247-
} else if (preset === "technician") {
248-
const g = new THREE.Group();
249-
const matSuit = new THREE.MeshPhysicalMaterial({ color: 0x2563EB, roughness: 0.45, metalness: 0.25 });
250-
const matSkin = new THREE.MeshPhysicalMaterial({ color: 0xF2C7A5, roughness: 0.65, metalness: 0.08 });
251-
const matTool = new THREE.MeshPhysicalMaterial({ color: 0x9CA3AF, roughness: 0.5, metalness: 0.7 });
252-
const torso = new THREE.Mesh(new RoundedBoxGeometry(0.6, 0.8, 0.3, 3, 0.1), matSuit);
253-
const head = new THREE.Mesh(new THREE.SphereGeometry(0.2, 12, 12), matSkin);
254-
head.position.set(0, 0.6, 0);
255-
const legL = new THREE.Mesh(new RoundedBoxGeometry(0.18, 0.6, 0.18, 2, 0.06), matSuit);
256-
legL.position.set(-0.15, -0.6, 0);
257-
const legR = legL.clone(); legR.position.x = 0.15;
258-
const armL = new THREE.Mesh(new RoundedBoxGeometry(0.16, 0.5, 0.16, 2, 0.06), matSuit);
259-
armL.position.set(-0.45, 0.0, 0);
260-
const armR = armL.clone(); armR.position.x = 0.45;
261-
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 12, 0, Math.PI * 2, 0, Math.PI / 2), matSuit);
262-
helmet.position.set(0, 0.7, 0);
263-
const belt = new THREE.Mesh(new RoundedBoxGeometry(0.62, 0.12, 0.32, 2, 0.04), new THREE.MeshPhysicalMaterial({ color: 0x1f2937, roughness: 0.6 }));
264-
belt.position.set(0, -0.1, 0);
265-
const tablet = new THREE.Mesh(new RoundedBoxGeometry(0.35, 0.22, 0.02, 2, 0.03), matTool);
266-
tablet.position.set(0.5, 0.05, 0.15);
267-
g.add(torso, head, legL, legR, armL, armR, helmet, belt, tablet);
268-
const pivot = centerAndScale(g);
269-
loaded = pivot; group.add(pivot);
270-
} else {
271-
const loader = new GLTFLoader();
272-
const slug = (title || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
273-
const src = modelPath || `/${slug}.glb`;
274-
loader.load(
275-
src,
276-
(gltf) => {
277-
const pivot = centerAndScale(gltf.scene);
278-
loaded = pivot;
279-
group.add(pivot);
280-
},
281-
undefined,
282-
() => {
283-
const geo = new THREE.TorusKnotGeometry(0.6, 0.2, 120, 16);
284-
const mat = new THREE.MeshStandardMaterial({ color: 0x8b5cf6, metalness: 0.5, roughness: 0.3 });
285-
loaded = new THREE.Mesh(geo, mat);
286-
group.add(loaded as THREE.Object3D);
287-
}
288-
);
289-
}
290-
291-
const resize = () => {
292-
const w = container.clientWidth;
293-
const h = w; // keep square
294-
renderer.setSize(w, h, false);
295-
camera.aspect = w / h;
296-
camera.updateProjectionMatrix();
297-
};
298-
resize();
299-
const ro = new ResizeObserver(resize);
300-
ro.observe(container);
301-
302-
let raf = 0;
303-
const tick = () => {
304-
if (group) {
305-
group.rotation.y += 0.01;
306-
}
307-
renderer.render(scene, camera);
308-
raf = requestAnimationFrame(tick);
309-
};
310-
raf = requestAnimationFrame(tick);
311-
312-
return () => {
313-
cancelAnimationFrame(raf);
314-
ro.disconnect();
315-
if (loaded && loaded.parent) loaded.parent.remove(loaded);
316-
renderer.dispose();
317-
scene.traverse((obj: any) => {
318-
if (obj.geometry) obj.geometry.dispose?.();
319-
if (obj.material) {
320-
if (Array.isArray(obj.material)) obj.material.forEach((m: any) => m.dispose?.());
321-
else obj.material.dispose?.();
322-
}
323-
});
324-
};
325-
}, [title, modelPath]);
326-
32736
return (
32837
<div className="relative">
32938
<Lines />
33039
<Card className="w-full border-none shadow-none bg-card/60 backdrop-blur rounded-xl overflow-hidden h-full flex flex-col">
33140
<div className="p-4">
33241
<div
333-
ref={containerRef}
334-
className="w-full aspect-square rounded-lg border border-border/50 bg-muted/10 backdrop-blur supports-[backdrop-filter]:bg-muted/20 overflow-hidden"
335-
>
336-
<canvas ref={canvasRef} className="block w-full h-full" />
337-
</div>
42+
className={cn(
43+
"w-full h-2 rounded-md",
44+
colorClass ?? "bg-primary"
45+
)}
46+
/>
33847
</div>
33948
<CardHeader className="pt-0">
34049
<div className="flex items-center gap-3">

0 commit comments

Comments
 (0)