2 Commits

Author SHA1 Message Date
Suvodip
4b2656cea6 s11 2025-04-04 19:58:51 +05:30
Suvodip
96a027dafb s1 2025-04-04 19:21:24 +05:30
19 changed files with 1421 additions and 338 deletions

224
package-lock.json generated
View File

@@ -10,18 +10,18 @@
"dependencies": {
"@astrojs/react": "^4.2.1",
"@astrojs/tailwind": "^6.0.0",
"@babylonjs/core": "^8.0.2",
"@babylonjs/loaders": "^8.0.2",
"@google/model-viewer": "^4.0.0",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@shadcn/ui": "^0.0.4",
"@types/date-fns": "^2.5.3",
"@types/react": "^19.0.12",
"astro": "^5.5.2",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.484.0",
"pocketbase": "^0.25.2",
"postcss": "^8.5.3",
@@ -444,22 +444,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babylonjs/core": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-8.0.2.tgz",
"integrity": "sha512-Bq6Ue2mu6N351AhPqoPNT0jLMm3n9igAqLs9CLb4RiWNR+VBylIf+L+lw/HJ6L2iOOzT1YfY7jD+vg6mW5wY2A==",
"license": "Apache-2.0"
},
"node_modules/@babylonjs/loaders": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-8.0.2.tgz",
"integrity": "sha512-o9e5GUiysqpqsTfYf2EQhG4OZM+Tx4Cp1uy/6XKuk+xBqKjCp4cId0QDFkB3CISatB7BbC/TrSpgR5TuJ5/+Pw==",
"license": "Apache-2.0",
"peerDependencies": {
"@babylonjs/core": "^8.0.0",
"babylonjs-gltf2interface": "^8.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
@@ -908,22 +892,6 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@google/model-viewer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@google/model-viewer/-/model-viewer-4.0.0.tgz",
"integrity": "sha512-hf4mixP178gGtvWQq97a0uULr7k98BwJGRU3lvt12XnurOdLmZA/RPdW4tPsINNNQxZvmvQsjud4lZWre6WGNw==",
"license": "Apache-2.0",
"dependencies": {
"@monogrid/gainmap-js": "^3.0.1",
"lit": "^2.7.2"
},
"engines": {
"node": ">=6.0.0"
},
"peerDependencies": {
"three": "^0.169.0"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
@@ -1390,33 +1358,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==",
"license": "BSD-3-Clause"
},
"node_modules/@lit/reactive-element": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.0.0"
}
},
"node_modules/@monogrid/gainmap-js": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz",
"integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==",
"license": "MIT",
"dependencies": {
"promise-worker-transferable": "^1.0.4"
},
"peerDependencies": {
"three": ">= 0.159.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1798,6 +1739,37 @@
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collection": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
@@ -1859,6 +1831,36 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-roving-focus": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
@@ -2461,6 +2463,12 @@
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/date-fns": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz",
"integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==",
"license": "MIT"
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -2528,12 +2536,6 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2852,13 +2854,6 @@
"node": ">= 0.4"
}
},
"node_modules/babylonjs-gltf2interface": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.0.2.tgz",
"integrity": "sha512-H913H03OSNfRlq0HLwrUjaJMmqYdINZPsX7dfa0Wg4mXmUgEIWSPxPi1Bl4gVwVGemZ8/PsFYbeOtyofwZBFJA==",
"license": "Apache-2.0",
"peer": true
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -3354,6 +3349,16 @@
"node": ">= 12"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -4188,12 +4193,6 @@
],
"license": "BSD-3-Clause"
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@@ -4349,12 +4348,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"license": "MIT"
},
"node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
@@ -4487,15 +4480,6 @@
"node": ">=6"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -4514,37 +4498,6 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/lit": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^1.6.0",
"lit-element": "^3.3.0",
"lit-html": "^2.8.0"
}
},
"node_modules/lit-element": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.1.0",
"@lit/reactive-element": "^1.3.0",
"lit-html": "^2.8.0"
}
},
"node_modules/lit-html": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/log-symbols": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
@@ -6034,16 +5987,6 @@
"node": ">=6"
}
},
"node_modules/promise-worker-transferable": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
"license": "Apache-2.0",
"dependencies": {
"is-promise": "^2.1.0",
"lie": "^3.0.2"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -7157,13 +7100,6 @@
"node": ">=0.8"
}
},
"node_modules/three": {
"version": "0.169.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz",
"integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==",
"license": "MIT",
"peer": true
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",

View File

@@ -12,18 +12,18 @@
"dependencies": {
"@astrojs/react": "^4.2.1",
"@astrojs/tailwind": "^6.0.0",
"@babylonjs/core": "^8.0.2",
"@babylonjs/loaders": "^8.0.2",
"@google/model-viewer": "^4.0.0",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@shadcn/ui": "^0.0.4",
"@types/date-fns": "^2.5.3",
"@types/react": "^19.0.12",
"astro": "^5.5.2",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.484.0",
"pocketbase": "^0.25.2",
"postcss": "^8.5.3",

1
public/assets/clock.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="15px" height="15px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M12 8V12L15 15" stroke="#6d9e37" stroke-width="2" stroke-linecap="round"></path> <circle cx="12" cy="12" r="9" stroke="#6d9e37" stroke-width="2"></circle> </g></svg>

After

Width:  |  Height:  |  Size: 430 B

1
public/assets/send.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="30px" height="30px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M20 4L3 9.31372L10.5 13.5M20 4L14.5 21L10.5 13.5M20 4L10.5 13.5" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -279,42 +279,42 @@ export default function HireAIAgent() {
<div className="mt-16">
<h2 className="text-2xl font-bold mb-8 text-center">AI Agents vs. Human Developers</h2>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-neutral-800 text-white">
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Feature</th>
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">AI Agents</th>
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Human Developers</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Availability</td>
<td className="p-4 border-[1px] border-[#6d9e37]">24/7, instant access</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Limited by working hours and availability</td>
</tr>
<tr className="bg-neutral-800 text-white">
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Cost</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Low monthly subscription</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Higher hourly or project-based rates</td>
</tr>
<tr>
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Task Complexity</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Best for repetitive, well-defined tasks</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Better for complex, novel problems</td>
</tr>
<tr className="bg-neutral-800 text-white">
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Creativity</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Limited to patterns in training data</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Higher level of creativity and innovation</td>
</tr>
<tr>
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Scalability</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Highly scalable at no extra cost</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Requires additional hiring and onboarding</td>
</tr>
</tbody>
</table>
<table className="w-full border-collapse">
<thead>
<tr className="bg-neutral-800 text-white">
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Feature</th>
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">AI Agents</th>
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Human Developers</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Availability</td>
<td className="p-4 border-[1px] border-[#6d9e37]">24/7, instant access</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Limited by working hours and availability</td>
</tr>
<tr className="bg-neutral-800 text-white">
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Cost</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Low monthly subscription</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Higher hourly or project-based rates</td>
</tr>
<tr>
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Task Complexity</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Best for repetitive, well-defined tasks</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Better for complex, novel problems</td>
</tr>
<tr className="bg-neutral-800 text-white">
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Creativity</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Limited to patterns in training data</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Higher level of creativity and innovation</td>
</tr>
<tr>
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Scalability</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Highly scalable at no extra cost</td>
<td className="p-4 border-[1px] border-[#6d9e37]">Requires additional hiring and onboarding</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -1,31 +0,0 @@
import React from 'react';
import '@google/model-viewer';
const ModelViewer = () => {
return (
<div className="w-full h-[500px]">
<model-viewer
src="https://bscdn.sgp1.digitaloceanspaces.com/tb-revised-books/glb/alligator.glb"
ar
ar-modes="webxr scene-viewer quick-look"
camera-controls
tone-mapping="neutral"
poster="poster.webp"
shadow-intensity="1"
style={{ width: '100%', height: '100%' }}
>
<div className="progress-bar hide" slot="progress-bar">
<div className="update-bar"></div>
</div>
<button slot="ar-button" id="ar-button">
View in your space
</button>
<div id="ar-prompt">
<img src="https://modelviewer.dev/shared-assets/icons/hand.png" />
</div>
</model-viewer>
</div>
);
};
export default ModelViewer;

365
src/components/Ticket.tsx Normal file
View File

@@ -0,0 +1,365 @@
import React, { useState, useEffect } from "react";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
import Table from "./ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
import { Input } from "./ui/input";
import { Textarea } from "./ui/textarea";
import { Label } from "./ui/label";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
import { CustomTabs } from "./ui/CustomTabs";
import {localizeTime} from "../lib/localizeTime";
interface Ticket {
id: number;
title: string;
description: string;
status: 'open' | 'in-progress' | 'resolved' | 'closed'; // Add 'closed' here
created_at: string;
updated_at: string;
created_by: number;
}
interface Message {
id: number;
ticket_id: number;
user_id: number;
message: string;
created_at: string;
user_type: string;
}
const API_URL = 'http://localhost:2058/host-api/app/v1/ticket/index.php';
function Ticketing() {
const [tickets, setTickets] = useState<Ticket[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [selectedTicket, setSelectedTicket] = useState<(Ticket & { status: 'open' | 'in-progress' | 'resolved' | 'closed' }) | null>(null);
const [activeTab, setActiveTab] = useState<'open' | 'in-progress' | 'resolved' | 'closed'>('open');
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isMessageDialogOpen, setIsMessageDialogOpen] = useState(false);
const [newMessage, setNewMessage] = useState('');
const [formData, setFormData] = useState({title: '', description: '', status: 'open' as const, });
// Fetch tickets
useEffect(() => {fetchTickets(); }, [activeTab]);
// Fetch messages when ticket is selected
useEffect(() => {if (selectedTicket) {fetchMessages(selectedTicket.id);}}, [selectedTicket]);
const fetchTickets = async () => {
try {
const response = await fetch(`${API_URL}?action=get_tickets&status=${activeTab}`);
const data = await response.json();
if (data.success) {
setTickets(data.tickets);
}
} catch (error) {
console.error('Error fetching tickets:', error);
}
};
const fetchMessages = async (ticketId: number) => {
try {
const response = await fetch(`${API_URL}?action=get_messages&ticket_id=${ticketId}`);
const data = await response.json();
console.log('Messages response:', data); // Debug log
if (data.success) {
setMessages(data.messages || []);
}
} catch (error) {
console.error('Error fetching messages:', error);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create_ticket',
...formData,
}),
});
const data = await response.json();
if (data.success) {
fetchTickets(); // Refresh the list
setIsDialogOpen(false);
setFormData({ title: '', description: '', status: 'open' });
}
} catch (error) {
console.error('Error creating ticket:', error);
}
};
const handleStatusChange = async (ticketId: number, newStatus: 'open' | 'in-progress' | 'resolved' | 'closed') => {
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'update_ticket_status',
ticket_id: ticketId,
status: newStatus,
}),
});
const data = await response.json();
if (data.success) {
fetchTickets(); // Refresh the list
if (selectedTicket) {
setSelectedTicket({ ...selectedTicket, status: newStatus });
}
}
} catch (error) {
console.error('Error updating ticket status:', error);
}
};
const handleSendMessage = async () => {
if (!selectedTicket || !newMessage.trim()) return;
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'add_message',
ticket_id: selectedTicket.id,
message: newMessage,
user_type: 'user'
}),
});
const data = await response.json();
if (data.success) {
fetchMessages(selectedTicket.id); // Refresh messages
setNewMessage('');
}
} catch (error) {
console.error('Error sending message:', error);
}
};
const handleDeleteTicket = async (ticketId: number) => {
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'delete_ticket',
ticket_id: ticketId,
}),
});
const data = await response.json();
if (data.success) {
fetchTickets(); // Refresh the list
if (selectedTicket?.id === ticketId) {
setIsMessageDialogOpen(false);
}
}
} catch (error) {
console.error('Error deleting ticket:', error);
}
};
// Table configuration
const tableHeaders = ['ID', 'Title', 'Description', 'Status', 'Created At', 'Actions'];
const tableData = tickets.map(ticket => ({
id: ticket.id,
title: ticket.title,
description: ticket.description,
status: ticket.status,
created_at: localizeTime(ticket.created_at),
actions: (
<div className="flex space-x-2">
<Button size="sm" onClick={() => {setSelectedTicket(ticket); setIsMessageDialogOpen(true); }}>View/Reply</Button>
<Button size="sm" onClick={() => handleDeleteTicket(ticket.id)} className="bg-red-500 hover:bg-red-600">Delete</Button>
</div>
)
}));
const statusColors = {open: 'bg-green-500', 'in-progress': 'bg-yellow-500', resolved: 'bg-blue-500', closed: 'bg-red-500', };
return (
<div className="container mx-auto p-4">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Ticket / Report</h1>
<Button size="sm" onClick={() => setIsDialogOpen(true)}>Create Ticket</Button>
</div>
<CustomTabs
tabs={[
{ label: 'Open', value: 'open', content: null },
{ label: 'In Progress', value: 'in-progress', content: null },
{ label: 'Resolved', value: 'resolved', content: null },
{ label: 'Closed', value: 'closed', content: null },
]}
defaultValue={activeTab}
onValueChange={(value) => setActiveTab(value as any)}
/>
<table className="w-full border-collapse">
<thead>
<tr className="bg-neutral-800 text-white">
{
tableHeaders.map((thead, index) => (
<th key={index} className={`p-2 text-center border-[1px] border-[#6d9e37]`}>{thead}</th>
))
}
</tr>
</thead>
{
tableData.length > 0 ? (
<tbody className="[&>tr:nth-child(even)]:bg-[#262626]">
{
tableData.map((data, index) => (
<tr key={index}>
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.id}</td>
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.title}</td>
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.description}</td>
<td className="px-2 border-[1px] border-[#6d9e37] font-medium text-center">
<span className={`text-white px-2 py-1 rounded ${statusColors[data.status] || ''}`}>{data.status}</span>
</td>
<td className="px-2 border-[1px] border-[#6d9e37] font-medium text-center">{data.created_at}</td>
<td className="px-2 border-[1px] border-[#6d9e37] font-medium flex items-center justify-center p-1">{data.actions}</td>
</tr>
))
}
</tbody>
) : (
<tbody>
<tr>
<th colSpan={6} className="p-4 border-[1px] border-[#6d9e37] font-medium">No {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Ticket Found</th>
</tr>
</tbody>
)
}
</table>
{/* <Table headers={tableHeaders} data={tableData} striped hover /> */}
{/* Create Ticket Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Ticket</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="title">Title</Label>
<Input id="title" name="title" value={formData.title} onChange={handleInputChange} required className="w-full bg-white rounded-md outline-none border border-[#6d9e37] p-2" placeholder="Write ticket title" />
</div>
<div>
<Label htmlFor="description">Description</Label>
<textarea id="description" name="description" value={formData.description} onChange={handleInputChange} rows={4} className="w-full rounded-md outline-none border border-[#6d9e37] p-2" required placeholder="Write description here..."></textarea>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
<Button type="submit">Create</Button>
</div>
</form>
</DialogContent>
</Dialog>
{/* Ticket Messages Dialog */}
<Dialog open={isMessageDialogOpen} onOpenChange={setIsMessageDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Ticket #{selectedTicket?.id}: {selectedTicket?.title}</DialogTitle>
</DialogHeader>
{selectedTicket && (
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded">
<p className="font-semibold text-gray-500">Ticket Details</p>
<p className="text-gray-500">{selectedTicket.description}</p>
<p className="text-sm text-gray-500 mt-2 ">
<strong>Status:</strong> <span className={`font-bold px-1 rounded-md text-white ${statusColors[selectedTicket.status] || ''}`}>{selectedTicket.status}</span>&nbsp;|&nbsp;
<strong>Created:</strong> {new Date(selectedTicket.created_at).toLocaleString()}
</p>
</div>
<div className="border-t pt-4">
<p className="font-semibold mb-2 text-[#6d9e37]">Conversation</p>
<div className="space-y-4 max-h-64 overflow-y-auto">
{
messages.length > 0 ? (
messages.map(message => (
<div key={message.id} className={`px-3 py-1 border-b rounded flex flex-col text-gray-500 ${message.user_type === 'user' ? 'justify-end items-end border-[#6d9e37]' : 'justify-start items-start border-gray-500'} `}>
<p>{message.message}</p>
<p className="text-xs text-gray-500 mt-1 inline-flex items-center gap-1">
<img src="/assets/clock.svg" alt="" />
{localizeTime(message.created_at)}</p>
</div>
))
) : (
<p className="text-gray-500">No messages yet</p>
)
}
</div>
</div>
<div className="border-t pt-4">
<Label htmlFor="newMessage" className="text-[#6d9e37]">Add Reply</Label>
<div className="flex flex-row gap-x-2">
<textarea placeholder="Write your message..." id="newMessage" rows={1} value={newMessage} onChange={(e) => setNewMessage(e.target.value)} className="w-full p-2 mb-2 border border-[#6d9e37] outline-none focus:outline-none rounded text-gray-500 resize-none"></textarea>
<Button onClick={handleSendMessage} disabled={!newMessage.trim()} className="px-6">
<img src="/assets/send.svg" alt="" />
</Button>
</div>
<div className="flex justify-between">
<div className="space-x-2">
{selectedTicket.status === 'closed' ? (
<Button
size="sm"
variant="default"
onClick={() => handleStatusChange(selectedTicket.id, 'open')}
className="bg-green-600 hover:bg-green-700"
>
Reopen
</Button>
) : (
<>
{selectedTicket.status !== 'in-progress' && (
<Button
size="sm"
variant="outline"
onClick={() => handleStatusChange(selectedTicket.id, 'in-progress')}
>
Mark as In Progress
</Button>
)}
{selectedTicket.status !== 'resolved' && (
<Button
size="sm"
variant="outline"
onClick={() => handleStatusChange(selectedTicket.id, 'resolved')}
>
Mark as Resolved
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleStatusChange(selectedTicket.id, 'closed')}
>
Close
</Button>
</>
)}
</div>
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}
export default Ticketing;

View File

@@ -0,0 +1,590 @@
import React, { useState, useEffect } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
import { CustomTabs } from "./ui/CustomTabs";
import Table from "./ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { Card, CardHeader, CardContent, CardFooter } from "./ui/card";
import { Badge } from "./ui/badge";
import { formatDistanceToNow } from "./ui/date-format";
interface Message {
id?: number;
content: string;
sender: 'user' | 'admin';
createdAt: string;
}
interface Ticket {
id: number;
title: string;
description: string;
messages: Message[];
status: 'open' | 'in-progress' | 'resolved' | 'closed';
priority: 'low' | 'medium' | 'high';
category?: string;
assignedTo?: string;
createdAt: string;
updatedAt: string;
}
export default function Ticketing() {
const [tickets, setTickets] = useState<Ticket[]>([]);
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isDetailOpen, setIsDetailOpen] = useState(false);
const [currentTicket, setCurrentTicket] = useState<Partial<Ticket>>({
title: '',
description: '',
status: 'open',
priority: 'medium',
category: 'general'
});
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const [newMessage, setNewMessage] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [activeTab, setActiveTab] = useState('open');
const [searchTerm, setSearchTerm] = useState('');
const API_URL = 'http://localhost:2058/host-api/v1/ticketing/index.php';
const fetchTickets = async () => {
try {
setLoading(true);
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error('Failed to fetch tickets');
}
const data = await response.json();
// Convert admin_message to messages array for backward compatibility
const processedData = data.map((ticket: any) => ({
...ticket,
messages: [
{
content: ticket.description,
sender: 'user',
createdAt: ticket.createdAt
},
...(ticket.admin_message ? [{
content: ticket.admin_message,
sender: 'admin',
createdAt: ticket.updatedAt
}] : [])
]
}));
setTickets(processedData);
filterTickets(processedData, activeTab, searchTerm);
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
} finally {
setLoading(false);
}
};
const filterTickets = (ticketsToFilter: Ticket[], status: string, search: string) => {
let filtered = ticketsToFilter;
if (status !== 'all') {
filtered = filtered.filter(ticket => ticket.status === status);
}
if (search) {
const searchLower = search.toLowerCase();
filtered = filtered.filter(ticket =>
ticket.title.toLowerCase().includes(searchLower) ||
ticket.description.toLowerCase().includes(searchLower) ||
ticket.messages.some(msg => msg.content.toLowerCase().includes(searchLower))
);
}
setFilteredTickets(filtered);
};
const saveTicket = async () => {
try {
const method = isEditing ? 'PUT' : 'POST';
const url = isEditing ? `${API_URL}?id=${currentTicket.id}` : API_URL;
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: currentTicket.title,
description: currentTicket.description,
status: currentTicket.status,
priority: currentTicket.priority,
category: currentTicket.category
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Failed to ${isEditing ? 'update' : 'create'} ticket`);
}
await fetchTickets();
setIsDialogOpen(false);
resetForm();
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
}
};
const addMessage = async (ticketId: number) => {
try {
if (!newMessage.trim()) return;
const response = await fetch(`${API_URL}?id=${ticketId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: newMessage
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to add message');
}
setNewMessage('');
await fetchTickets();
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
}
};
const reopenTicket = async (ticketId: number) => {
try {
const response = await fetch(`${API_URL}?id=${ticketId}&action=reopen`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to reopen ticket');
}
await fetchTickets();
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
}
};
const deleteTicket = async (id: number) => {
try {
const response = await fetch(`${API_URL}?id=${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete ticket');
}
await fetchTickets();
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
}
};
const viewTicketDetails = (ticket: Ticket) => {
setSelectedTicket(ticket);
setIsDetailOpen(true);
};
const initNewTicket = () => {
setCurrentTicket({
title: '',
description: '',
status: 'open',
priority: 'medium',
category: 'general'
});
setIsEditing(false);
setIsDialogOpen(true);
};
const initEditTicket = (ticket: Ticket) => {
setCurrentTicket({ ...ticket });
setIsEditing(true);
setIsDialogOpen(true);
};
const resetForm = () => {
setCurrentTicket({
title: '',
description: '',
status: 'open',
priority: 'medium',
category: 'general'
});
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setCurrentTicket(prev => ({ ...prev, [name]: value }));
};
const handleSelectChange = (name: string, value: string) => {
setCurrentTicket(prev => ({ ...prev, [name]: value }));
};
useEffect(() => {
fetchTickets();
}, []);
useEffect(() => {
filterTickets(tickets, activeTab, searchTerm);
}, [activeTab, searchTerm, tickets]);
const renderTicketTable = () => {
if (loading) {
return <div className="text-center py-8">Loading tickets...</div>;
}
if (filteredTickets.length === 0) {
return <div className="text-center py-8">No tickets found</div>;
}
const tableHeaders = ['ID', 'Title', 'Category', 'Status', 'Priority', 'Created', 'Actions'];
const tableData = filteredTickets.map(ticket => ({
'ID': ticket.id,
'Title': (
<button
onClick={() => viewTicketDetails(ticket)}
className="text-left hover:underline"
>
{ticket.title}
</button>
),
'Category': ticket.category || 'General',
'Status': (
<Badge
variant={
ticket.status === 'open' ? 'default' :
ticket.status === 'in-progress' ? 'secondary' :
ticket.status === 'resolved' ? 'success' : 'outline'
}
>
{ticket.status}
</Badge>
),
'Priority': (
<Badge
variant={
ticket.priority === 'low' ? 'success' :
ticket.priority === 'medium' ? 'warning' : 'destructive'
}
>
{ticket.priority}
</Badge>
),
'Created': formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true }),
'Actions': (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => viewTicketDetails(ticket)}
className="h-8"
>
View
</Button>
<Button
variant="outline"
size="sm"
onClick={() => initEditTicket(ticket)}
className="h-8"
>
Edit
</Button>
{ticket.status === 'closed' ? (
<Button
size="sm"
onClick={() => reopenTicket(ticket.id)}
className="h-8"
>
Reopen
</Button>
) : (
<Button
size="sm"
onClick={() => deleteTicket(ticket.id)}
className="h-8"
>
Delete
</Button>
)}
</div>
)
}));
return (
<Table
headers={tableHeaders}
data={tableData}
className="border rounded-lg shadow-sm"
striped
hover
/>
);
};
const renderMessages = () => {
if (!selectedTicket) return null;
return (
<div className="space-y-4">
{selectedTicket.messages.map((message, index) => (
<div key={index} className={`flex ${message.sender === 'admin' ? 'justify-start' : 'justify-end'}`}>
<div className={`max-w-[80%] rounded-lg p-4 ${message.sender === 'admin' ? 'bg-gray-100' : 'bg-blue-100'}`}>
<div className="flex items-center gap-2 mb-1">
<Avatar className="h-6 w-6">
<AvatarFallback>{message.sender === 'admin' ? 'A' : 'U'}</AvatarFallback>
</Avatar>
<span className="font-medium">{message.sender === 'admin' ? 'Admin' : 'You'}</span>
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(message.createdAt), { addSuffix: true })}
</span>
</div>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
</div>
))}
</div>
);
};
const tabs = [
{
label: "Open Tickets",
value: "open",
content: renderTicketTable()
},
{
label: "In Progress",
value: "in-progress",
content: renderTicketTable()
},
{
label: "Resolved",
value: "resolved",
content: renderTicketTable()
},
{
label: "Closed Tickets",
value: "closed",
content: renderTicketTable()
},
{
label: "All Tickets",
value: "all",
content: renderTicketTable()
},
];
return (
<div className="container mx-auto p-4">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Ticketing System</h1>
<Button onClick={initNewTicket}>
Create New Ticket
</Button>
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<div className="mb-4 flex justify-between items-center">
<div className="w-1/3">
<Input
placeholder="Search tickets..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<CustomTabs
tabs={tabs}
defaultValue="open"
onValueChange={(value) => setActiveTab(value)}
/>
{/* Ticket Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{isEditing ? 'Edit Ticket' : 'Create New Ticket'}
</DialogTitle>
<DialogDescription>
{isEditing ? 'Update the ticket details' : 'Fill in the details for the new ticket'}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
Title
</Label>
<Input
id="title"
name="title"
value={currentTicket.title}
onChange={handleInputChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea
id="description"
name="description"
value={currentTicket.description}
onChange={handleInputChange}
className="col-span-3"
rows={5}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="category" className="text-right">
Category
</Label>
<Select
value={currentTicket.category}
onValueChange={(value) => handleSelectChange('category', value)}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">General</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="technical">Technical</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="status" className="text-right">
Status
</Label>
<Select
value={currentTicket.status}
onValueChange={(value) => handleSelectChange('status', value)}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="in-progress">In Progress</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="priority" className="text-right">
Priority
</Label>
<Select
value={currentTicket.priority}
onValueChange={(value) => handleSelectChange('priority', value)}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button>
<Button onClick={saveTicket}>
{isEditing ? 'Update Ticket' : 'Create Ticket'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Ticket Detail Dialog */}
<Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{selectedTicket?.title}</DialogTitle>
<div className="flex gap-4 items-center">
<Badge variant={
selectedTicket?.status === 'open' ? 'default' :
selectedTicket?.status === 'in-progress' ? 'secondary' :
selectedTicket?.status === 'resolved' ? 'success' : 'outline'
}>
{selectedTicket?.status}
</Badge>
<Badge variant={
selectedTicket?.priority === 'low' ? 'success' :
selectedTicket?.priority === 'medium' ? 'warning' : 'destructive'
}>
{selectedTicket?.priority}
</Badge>
<span className="text-sm text-gray-500">
Created {formatDistanceToNow(new Date(selectedTicket?.createdAt || ''), { addSuffix: true })}
</span>
</div>
</DialogHeader>
<div className="py-4 max-h-[60vh] overflow-y-auto">
{renderMessages()}
</div>
{selectedTicket?.status !== 'closed' && (
<div className="flex gap-2">
<Input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type your message..."
className="flex-1"
/>
<Button onClick={() => selectedTicket && addMessage(selectedTicket.id)}>
Send
</Button>
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import React from "react";
interface Tab {
label: string;
value: string;
content: React.ReactNode;
}
interface CustomTabsProps {
tabs: Tab[];
defaultValue?: string;
onValueChange?: (value: string) => void;
}
export function CustomTabs({ tabs, defaultValue, onValueChange }: CustomTabsProps) {
const [activeTab, setActiveTab] = React.useState(defaultValue || tabs[0]?.value || '');
const handleTabChange = (value: string) => {
setActiveTab(value);
if (onValueChange) {
onValueChange(value);
}
};
return (
<div className="w-full">
<div className="flex border-b border-[#6d9e37]">
{tabs.map((tab) => (
<button
key={tab.value}
className={`px-4 py-2 font-medium text-sm focus:outline-none ${activeTab === tab.value ? 'border-x border-t border-[#6d9e37] text-[#6d9e37] rounded-t-md' : 'text-[#c7cccb]'}`}
onClick={() => handleTabChange(tab.value)}
>
{tab.label}
</button>
))}
</div>
<div className="py-4">
{tabs.find((tab) => tab.value === activeTab)?.content}
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
success:
"border-transparent bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
warning:
"border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
info: "border-transparent bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,104 @@
import * as React from "react";
// Utility function with proper error handling
export function customFormatDistanceToNow(
date: Date | string | number,
options: { addSuffix?: boolean } = { addSuffix: true }
): string {
try {
const dateObj = typeof date === "string" || typeof date === "number"
? new Date(date)
: date;
// Check for invalid date
if (isNaN(dateObj.getTime())) {
return "Invalid date";
}
const now = new Date();
const diffInSeconds = (now.getTime() - dateObj.getTime()) / 1000;
// Check for infinite or NaN values
if (!isFinite(diffInSeconds)) {
return "Invalid date range";
}
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const absoluteSeconds = Math.abs(diffInSeconds);
let value: number;
let unit: Intl.RelativeTimeFormatUnit;
if (absoluteSeconds < 60) {
value = Math.round(diffInSeconds);
unit = 'second';
} else if (absoluteSeconds < 3600) {
value = Math.round(diffInSeconds / 60);
unit = 'minute';
} else if (absoluteSeconds < 86400) {
value = Math.round(diffInSeconds / 3600);
unit = 'hour';
} else if (absoluteSeconds < 2592000) {
value = Math.round(diffInSeconds / 86400);
unit = 'day';
} else if (absoluteSeconds < 31536000) {
value = Math.round(diffInSeconds / 2592000);
unit = 'month';
} else {
value = Math.round(diffInSeconds / 31536000);
unit = 'year';
}
// Ensure value is finite before formatting
if (!isFinite(value)) {
return "Invalid date range";
}
let result = rtf.format(value, unit);
if (!options.addSuffix) {
result = result.replace(/^in /, '').replace(/ ago$/, '');
}
return result;
} catch (error) {
console.error("Error formatting date:", error);
return "Invalid date";
}
}
// React component interface
interface DateFormatProps {
date: Date | string | number;
className?: string;
addSuffix?: boolean;
fallback?: string;
}
// React component implementation
const DateFormat: React.FC<DateFormatProps> = ({
date,
className = "",
addSuffix = true,
fallback = "Invalid date",
}) => {
let formattedDate: string;
try {
formattedDate = customFormatDistanceToNow(date, { addSuffix });
} catch (error) {
formattedDate = fallback;
}
return (
<time
dateTime={new Date(date).toISOString()}
className={className}
title={new Date(date).toLocaleString()}
>
{formattedDate}
</time>
);
};
// Export both with distinct names
export { DateFormat, customFormatDistanceToNow as formatDistanceToNow };

View File

@@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
@@ -22,3 +22,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
Input.displayName = "Input";
export { Input };

View File

@@ -2,37 +2,61 @@ interface TableProps {
headers: string[];
data: any[];
className?: string;
children?: React.ReactNode; // Add children here
caption?: string;
striped?: boolean;
hover?: boolean;
}
const Table: React.FC<TableProps> = ({ headers, data, className = '', children }) => {
const Table: React.FC<TableProps> = ({
headers,
data,
className = '',
caption,
striped = false,
hover = false
}) => {
return (
<div className={`overflow-x-auto ${className}`}>
<table className="min-w-full bg-white border border-gray-300">
<thead>
<table className="min-w-full bg-white border border-gray-200 rounded-lg">
{caption && (
<caption className="text-sm text-gray-500 p-2 text-left">
{caption}
</caption>
)}
<thead className="bg-gray-50">
<tr>
{headers.map((header, index) => (
<th key={index} className="px-6 py-3 text-left text-sm font-medium text-gray-600">
<th
key={index}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
<tbody className="divide-y divide-gray-200">
{data.map((row, index) => (
<tr key={index} className="border-t">
<tr
key={index}
className={`${striped && index % 2 === 0 ? 'bg-gray-50' : ''} ${
hover ? 'hover:bg-gray-100' : ''
}`}
>
{Object.values(row).map((value, idx) => (
<td key={idx} className="px-6 py-4 text-sm text-gray-800">
<td
key={idx}
className="px-6 py-4 text-sm text-gray-800"
>
{value as React.ReactNode}
</td>
))}
</tr>
))}
{children} {/* This will allow you to pass additional content */}
</tbody>
</table>
</div>
);
};
export default Table;
export default Table;

View File

@@ -0,0 +1,33 @@
import * as React from "react";
import * as Tabs from "@radix-ui/react-tabs";
import { cn } from "../../lib/utils";
export interface CustomTabsProps {
tabs: { label: string; value: string; content: React.ReactNode }[];
className?: string;
}
const CustomTabs: React.FC<CustomTabsProps> = ({ tabs, className }) => {
return (
<Tabs.Root defaultValue={tabs[0]?.value} className={cn("w-full", className)}>
<Tabs.List className="flex border-b border-neutral-600 bg-neutral-800">
{tabs.map((tab) => (
<Tabs.Trigger
key={tab.value}
value={tab.value}
className="px-4 py-2 text-sm text-neutral-400 hover:text-white focus-visible:ring-2 focus-visible:ring-[#6d9e37]"
>
{tab.label}
</Tabs.Trigger>
))}
</Tabs.List>
{tabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4 text-neutral-300">
{tab.content}
</Tabs.Content>
))}
</Tabs.Root>
);
};
export { CustomTabs };

37
src/lib/localizeTime.tsx Normal file
View File

@@ -0,0 +1,37 @@
export function localizeTime(timeValue: string, targetTimeZone?: string): string {
// 1. Auto-detect user's timezone if none provided
if (!targetTimeZone) {
targetTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
}
// 2. Format the date in the target timezone
const date = new Date(timeValue.replace(' ', 'T') + 'Z'); // Ensure UTC
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: targetTimeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
const [
{ value: month },,
{ value: day },,
{ value: year },,
{ value: hour },,
{ value: minute },,
{ value: second }
] = formatter.formatToParts(date);
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
// Usage:
// const localTime = localizeTime(ticket.created_at); // Auto-detects timezone
// console.log(localTime); // "2025-04-03 20:14:50" (in user's local time)
// const timeInTokyo = localizeTime(ticket.created_at, "Asia/Tokyo");
// console.log(timeInTokyo); // "2025-04-04 05:14:50" (Tokyo time)

View File

@@ -4,3 +4,9 @@ import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Type helpers
export type MergeElementProps<
T extends React.ElementType,
P extends object = {}
> = Omit<React.ComponentPropsWithRef<T>, keyof P> & P;

View File

@@ -1,11 +0,0 @@
---
import Layout from "../layouts/Layout.astro";
import ModelViewer from "../components/ModelViewer"; // Ensure the name matches
---
<Layout title="3D Model Viewer">
<div class="flex flex-col items-center justify-center h-screen">
<h1 class="text-4xl font-bold mb-4">3D Model Viewer</h1>
<ModelViewer client:only="react" />
</div>
</Layout>

7
src/pages/ticket.astro Normal file
View File

@@ -0,0 +1,7 @@
---
import Layout from "../layouts/Layout.astro";
import TickitingSystem from "../components/Ticket";
---
<Layout title="">
<TickitingSystem client:load />
</Layout>

141
yarn.lock
View File

@@ -243,16 +243,6 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babylonjs/core@^8.0.0", "@babylonjs/core@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@babylonjs/core/-/core-8.0.2.tgz"
integrity sha512-Bq6Ue2mu6N351AhPqoPNT0jLMm3n9igAqLs9CLb4RiWNR+VBylIf+L+lw/HJ6L2iOOzT1YfY7jD+vg6mW5wY2A==
"@babylonjs/loaders@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-8.0.2.tgz"
integrity sha512-o9e5GUiysqpqsTfYf2EQhG4OZM+Tx4Cp1uy/6XKuk+xBqKjCp4cId0QDFkB3CISatB7BbC/TrSpgR5TuJ5/+Pw==
"@esbuild/linux-x64@0.25.1":
version "0.25.1"
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz"
@@ -285,14 +275,6 @@
resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz"
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
"@google/model-viewer@^4.0.0":
version "4.0.0"
resolved "https://registry.npmjs.org/@google/model-viewer/-/model-viewer-4.0.0.tgz"
integrity sha512-hf4mixP178gGtvWQq97a0uULr7k98BwJGRU3lvt12XnurOdLmZA/RPdW4tPsINNNQxZvmvQsjud4lZWre6WGNw==
dependencies:
"@monogrid/gainmap-js" "^3.0.1"
lit "^2.7.2"
"@img/sharp-libvips-linux-x64@1.0.4":
version "1.0.4"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz"
@@ -361,25 +343,6 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
version "1.3.0"
resolved "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz"
integrity sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==
"@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0":
version "1.6.3"
resolved "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz"
integrity sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==
dependencies:
"@lit-labs/ssr-dom-shim" "^1.0.0"
"@monogrid/gainmap-js@^3.0.1":
version "3.1.0"
resolved "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz"
integrity sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==
dependencies:
promise-worker-transferable "^1.0.4"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@@ -544,6 +507,21 @@
dependencies:
"@radix-ui/react-slot" "1.1.2"
"@radix-ui/react-roving-focus@1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz"
integrity sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-collection" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-select@^2.1.6":
version "2.1.6"
resolved "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz"
@@ -578,6 +556,20 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-tabs@^1.1.3":
version "1.1.3"
resolved "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz"
integrity sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-roving-focus" "1.1.2"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-toast@^1.2.6":
version "1.2.6"
resolved "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz"
@@ -778,6 +770,11 @@
resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz"
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
"@types/date-fns@^2.5.3":
version "2.5.3"
resolved "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz"
integrity sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==
"@types/debug@^4.0.0":
version "4.1.12"
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
@@ -828,11 +825,6 @@
dependencies:
csstype "^3.0.2"
"@types/trusted-types@^2.0.2":
version "2.0.7"
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.3"
resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz"
@@ -1011,11 +1003,6 @@ axobject-query@^4.1.0:
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
babylonjs-gltf2interface@^8.0.0:
version "8.0.2"
resolved "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.0.2.tgz"
integrity sha512-H913H03OSNfRlq0HLwrUjaJMmqYdINZPsX7dfa0Wg4mXmUgEIWSPxPi1Bl4gVwVGemZ8/PsFYbeOtyofwZBFJA==
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz"
@@ -1301,6 +1288,11 @@ data-uri-to-buffer@^4.0.0:
resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz"
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
date-fns@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz"
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0:
version "4.4.0"
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
@@ -1813,11 +1805,6 @@ ieee754@^1.2.1:
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
import-meta-resolve@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz"
@@ -1896,11 +1883,6 @@ is-plain-obj@^4.0.0:
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz"
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
is-promise@^2.1.0:
version "2.2.2"
resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
is-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz"
@@ -1978,13 +1960,6 @@ kleur@^4.1.5:
resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
lie@^3.0.2:
version "3.3.0"
resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
lilconfig@^3.0.0, lilconfig@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz"
@@ -1995,31 +1970,6 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
lit-element@^3.3.0:
version "3.3.3"
resolved "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz"
integrity sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==
dependencies:
"@lit-labs/ssr-dom-shim" "^1.1.0"
"@lit/reactive-element" "^1.3.0"
lit-html "^2.8.0"
lit-html@^2.8.0:
version "2.8.0"
resolved "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz"
integrity sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==
dependencies:
"@types/trusted-types" "^2.0.2"
lit@^2.7.2:
version "2.8.0"
resolved "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz"
integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==
dependencies:
"@lit/reactive-element" "^1.6.0"
lit-element "^3.3.0"
lit-html "^2.8.0"
log-symbols@^5.1.0:
version "5.1.0"
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz"
@@ -2840,14 +2790,6 @@ prismjs@^1.29.0:
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz"
integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==
promise-worker-transferable@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz"
integrity sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==
dependencies:
is-promise "^2.1.0"
lie "^3.0.2"
prompts@^2.4.2:
version "2.4.2"
resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz"
@@ -3444,11 +3386,6 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
three@^0.169.0, "three@>= 0.159.0":
version "0.169.0"
resolved "https://registry.npmjs.org/three/-/three-0.169.0.tgz"
integrity sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==
tinyexec@^0.3.2:
version "0.3.2"
resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz"