mirror of
https://github.com/ClipFusion-org/clipfusion.git
synced 2025-08-03 16:55:08 +00:00
ui: implemented project creation functionality and persistent project storage
This commit is contained in:
parent
dd3ec99d3c
commit
2e4fe8be22
97
package-lock.json
generated
97
package-lock.json
generated
@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "clipfusion-community",
|
||||
"name": "clipfusion",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "clipfusion-editor",
|
||||
"name": "clipfusion",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
@ -17,6 +19,7 @@
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@react-hook/hotkey": "^3.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dexie": "^4.0.11",
|
||||
@ -27,6 +30,7 @@
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.0.5"
|
||||
},
|
||||
@ -1041,6 +1045,34 @@
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz",
|
||||
"integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"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-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
@ -1087,6 +1119,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
|
||||
"integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"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-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@ -1658,6 +1720,27 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-hook/event": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/event/-/event-1.2.6.tgz",
|
||||
"integrity": "sha512-JUL5IluaOdn5w5Afpe/puPa1rj8X6udMlQ9dt4hvMuKmTrBS1Ya6sb4sVgvfe2eU4yDuOfAhik8xhbcCekbg9Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-hook/hotkey": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/hotkey/-/hotkey-3.1.0.tgz",
|
||||
"integrity": "sha512-ekIIW8S12P/fP9krrsOWZCIqzQPzjz2WVkD3v1epP6SAy/XxDIWgJrItd2ySh0RKLkYcfW8z+eii3W/h1TKjOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-hook/event": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@ -6356,6 +6439,16 @@
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
|
||||
"integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "clipfusion-community",
|
||||
"name": "clipfusion",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -10,7 +10,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
@ -18,6 +20,7 @@
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@react-hook/hotkey": "^3.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dexie": "^4.0.11",
|
||||
@ -28,6 +31,7 @@
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.0.5"
|
||||
},
|
||||
|
@ -1,7 +1,3 @@
|
||||
ARG ENABLE_ANALYTICS=false
|
||||
ARG ANALYTICS_SCRIPT
|
||||
ARG ANALYTICS_WEBSITE_ID
|
||||
|
||||
FROM node:lts-alpine3.22 AS base
|
||||
|
||||
FROM base AS deps
|
||||
|
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
@ -1,106 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
</defs>
|
||||
<rect x="0.0" y="0.0" width="51.2" height="51.2" fill="#ff0040" />
|
||||
<rect x="0.0" y="51.2" width="51.2" height="51.2" fill="#ff1c38" />
|
||||
<rect x="0.0" y="102.4" width="51.2" height="51.2" fill="#ff3831" />
|
||||
<rect x="0.0" y="153.60000000000002" width="51.2" height="51.2" fill="#ff552a" />
|
||||
<rect x="0.0" y="204.8" width="51.2" height="51.2" fill="#ff7123" />
|
||||
<rect x="0.0" y="256.0" width="51.2" height="51.2" fill="#ff8d1c" />
|
||||
<rect x="0.0" y="307.20000000000005" width="51.2" height="51.2" fill="#ffaa15" />
|
||||
<rect x="0.0" y="358.40000000000003" width="51.2" height="51.2" fill="#ffc60e" />
|
||||
<rect x="0.0" y="409.6" width="51.2" height="51.2" fill="#ffe207" />
|
||||
<rect x="0.0" y="460.8" width="51.2" height="51.2" fill="#ffff00" />
|
||||
<rect x="51.2" y="0.0" width="51.2" height="51.2" fill="#ff1c38" />
|
||||
<rect x="51.2" y="51.2" width="51.2" height="51.2" fill="#ff3233" />
|
||||
<rect x="51.2" y="102.4" width="51.2" height="51.2" fill="#ff482d" />
|
||||
<rect x="51.2" y="153.60000000000002" width="51.2" height="51.2" fill="#ff5e28" />
|
||||
<rect x="51.2" y="204.8" width="51.2" height="51.2" fill="#ff7422" />
|
||||
<rect x="51.2" y="256.0" width="51.2" height="51.2" fill="#ff8a1d" />
|
||||
<rect x="51.2" y="307.20000000000005" width="51.2" height="51.2" fill="#ffa017" />
|
||||
<rect x="51.2" y="358.40000000000003" width="51.2" height="51.2" fill="#ffb612" />
|
||||
<rect x="51.2" y="409.6" width="51.2" height="51.2" fill="#ffcc0c" />
|
||||
<rect x="51.2" y="460.8" width="51.2" height="51.2" fill="#ffe207" />
|
||||
<rect x="102.4" y="0.0" width="51.2" height="51.2" fill="#ff3831" />
|
||||
<rect x="102.4" y="51.2" width="51.2" height="51.2" fill="#ff482d" />
|
||||
<rect x="102.4" y="102.4" width="51.2" height="51.2" fill="#ff5829" />
|
||||
<rect x="102.4" y="153.60000000000002" width="51.2" height="51.2" fill="#ff6725" />
|
||||
<rect x="102.4" y="204.8" width="51.2" height="51.2" fill="#ff7721" />
|
||||
<rect x="102.4" y="256.0" width="51.2" height="51.2" fill="#ff871e" />
|
||||
<rect x="102.4" y="307.20000000000005" width="51.2" height="51.2" fill="#ff971a" />
|
||||
<rect x="102.4" y="358.40000000000003" width="51.2" height="51.2" fill="#ffa616" />
|
||||
<rect x="102.4" y="409.6" width="51.2" height="51.2" fill="#ffb612" />
|
||||
<rect x="102.4" y="460.8" width="51.2" height="51.2" fill="#ffc60e" />
|
||||
<rect x="153.60000000000002" y="0.0" width="51.2" height="51.2" fill="#ff552a" />
|
||||
<rect x="153.60000000000002" y="51.2" width="51.2" height="51.2" fill="#ff5e28" />
|
||||
<rect x="153.60000000000002" y="102.4" width="51.2" height="51.2" fill="#ff6725" />
|
||||
<rect x="153.60000000000002" y="153.60000000000002" width="51.2" height="51.2" fill="#ff7123" />
|
||||
<rect x="153.60000000000002" y="204.8" width="51.2" height="51.2" fill="#ff7a21" />
|
||||
<rect x="153.60000000000002" y="256.0" width="51.2" height="51.2" fill="#ff841e" />
|
||||
<rect x="153.60000000000002" y="307.20000000000005" width="51.2" height="51.2" fill="#ff8d1c" />
|
||||
<rect x="153.60000000000002" y="358.40000000000003" width="51.2" height="51.2" fill="#ff971a" />
|
||||
<rect x="153.60000000000002" y="409.6" width="51.2" height="51.2" fill="#ffa017" />
|
||||
<rect x="153.60000000000002" y="460.8" width="51.2" height="51.2" fill="#ffaa15" />
|
||||
<rect x="204.8" y="0.0" width="51.2" height="51.2" fill="#ff7123" />
|
||||
<rect x="204.8" y="51.2" width="51.2" height="51.2" fill="#ff7422" />
|
||||
<rect x="204.8" y="102.4" width="51.2" height="51.2" fill="#ff7721" />
|
||||
<rect x="204.8" y="153.60000000000002" width="51.2" height="51.2" fill="#ff7a21" />
|
||||
<rect x="204.8" y="204.8" width="51.2" height="51.2" fill="#ff7d20" />
|
||||
<rect x="204.8" y="256.0" width="51.2" height="51.2" fill="#ff811f" />
|
||||
<rect x="204.8" y="307.20000000000005" width="51.2" height="51.2" fill="#ff841e" />
|
||||
<rect x="204.8" y="358.40000000000003" width="51.2" height="51.2" fill="#ff871e" />
|
||||
<rect x="204.8" y="409.6" width="51.2" height="51.2" fill="#ff8a1d" />
|
||||
<rect x="204.8" y="460.8" width="51.2" height="51.2" fill="#ff8d1c" />
|
||||
<rect x="256.0" y="0.0" width="51.2" height="51.2" fill="#ff8d1c" />
|
||||
<rect x="256.0" y="51.2" width="51.2" height="51.2" fill="#ff8a1d" />
|
||||
<rect x="256.0" y="102.4" width="51.2" height="51.2" fill="#ff871e" />
|
||||
<rect x="256.0" y="153.60000000000002" width="51.2" height="51.2" fill="#ff841e" />
|
||||
<rect x="256.0" y="204.8" width="51.2" height="51.2" fill="#ff811f" />
|
||||
<rect x="256.0" y="256.0" width="51.2" height="51.2" fill="#ff7d20" />
|
||||
<rect x="256.0" y="307.20000000000005" width="51.2" height="51.2" fill="#ff7a21" />
|
||||
<rect x="256.0" y="358.40000000000003" width="51.2" height="51.2" fill="#ff7721" />
|
||||
<rect x="256.0" y="409.6" width="51.2" height="51.2" fill="#ff7422" />
|
||||
<rect x="256.0" y="460.8" width="51.2" height="51.2" fill="#ff7123" />
|
||||
<rect x="307.20000000000005" y="0.0" width="51.2" height="51.2" fill="#ffaa15" />
|
||||
<rect x="307.20000000000005" y="51.2" width="51.2" height="51.2" fill="#ffa017" />
|
||||
<rect x="307.20000000000005" y="102.4" width="51.2" height="51.2" fill="#ff971a" />
|
||||
<rect x="307.20000000000005" y="153.60000000000002" width="51.2" height="51.2" fill="#ff8d1c" />
|
||||
<rect x="307.20000000000005" y="204.8" width="51.2" height="51.2" fill="#ff841e" />
|
||||
<rect x="307.20000000000005" y="256.0" width="51.2" height="51.2" fill="#ff7a21" />
|
||||
<rect x="307.20000000000005" y="307.20000000000005" width="51.2" height="51.2" fill="#ff7123" />
|
||||
<rect x="307.20000000000005" y="358.40000000000003" width="51.2" height="51.2" fill="#ff6725" />
|
||||
<rect x="307.20000000000005" y="409.6" width="51.2" height="51.2" fill="#ff5e28" />
|
||||
<rect x="307.20000000000005" y="460.8" width="51.2" height="51.2" fill="#ff552a" />
|
||||
<rect x="358.40000000000003" y="0.0" width="51.2" height="51.2" fill="#ffc60e" />
|
||||
<rect x="358.40000000000003" y="51.2" width="51.2" height="51.2" fill="#ffb612" />
|
||||
<rect x="358.40000000000003" y="102.4" width="51.2" height="51.2" fill="#ffa616" />
|
||||
<rect x="358.40000000000003" y="153.60000000000002" width="51.2" height="51.2" fill="#ff971a" />
|
||||
<rect x="358.40000000000003" y="204.8" width="51.2" height="51.2" fill="#ff871e" />
|
||||
<rect x="358.40000000000003" y="256.0" width="51.2" height="51.2" fill="#ff7721" />
|
||||
<rect x="358.40000000000003" y="307.20000000000005" width="51.2" height="51.2" fill="#ff6725" />
|
||||
<rect x="358.40000000000003" y="358.40000000000003" width="51.2" height="51.2" fill="#ff5829" />
|
||||
<rect x="358.40000000000003" y="409.6" width="51.2" height="51.2" fill="#ff482d" />
|
||||
<rect x="358.40000000000003" y="460.8" width="51.2" height="51.2" fill="#ff3831" />
|
||||
<rect x="409.6" y="0.0" width="51.2" height="51.2" fill="#ffe207" />
|
||||
<rect x="409.6" y="51.2" width="51.2" height="51.2" fill="#ffcc0c" />
|
||||
<rect x="409.6" y="102.4" width="51.2" height="51.2" fill="#ffb612" />
|
||||
<rect x="409.6" y="153.60000000000002" width="51.2" height="51.2" fill="#ffa017" />
|
||||
<rect x="409.6" y="204.8" width="51.2" height="51.2" fill="#ff8a1d" />
|
||||
<rect x="409.6" y="256.0" width="51.2" height="51.2" fill="#ff7422" />
|
||||
<rect x="409.6" y="307.20000000000005" width="51.2" height="51.2" fill="#ff5e28" />
|
||||
<rect x="409.6" y="358.40000000000003" width="51.2" height="51.2" fill="#ff482d" />
|
||||
<rect x="409.6" y="409.6" width="51.2" height="51.2" fill="#ff3233" />
|
||||
<rect x="409.6" y="460.8" width="51.2" height="51.2" fill="#ff1c38" />
|
||||
<rect x="460.8" y="0.0" width="51.2" height="51.2" fill="#ffff00" />
|
||||
<rect x="460.8" y="51.2" width="51.2" height="51.2" fill="#ffe207" />
|
||||
<rect x="460.8" y="102.4" width="51.2" height="51.2" fill="#ffc60e" />
|
||||
<rect x="460.8" y="153.60000000000002" width="51.2" height="51.2" fill="#ffaa15" />
|
||||
<rect x="460.8" y="204.8" width="51.2" height="51.2" fill="#ff8d1c" />
|
||||
<rect x="460.8" y="256.0" width="51.2" height="51.2" fill="#ff7123" />
|
||||
<rect x="460.8" y="307.20000000000005" width="51.2" height="51.2" fill="#ff552a" />
|
||||
<rect x="460.8" y="358.40000000000003" width="51.2" height="51.2" fill="#ff3831" />
|
||||
<rect x="460.8" y="409.6" width="51.2" height="51.2" fill="#ff1c38" />
|
||||
<rect x="460.8" y="460.8" width="51.2" height="51.2" fill="#ff0040" />
|
||||
</svg>
|
Before Width: | Height: | Size: 7.8 KiB |
@ -6,6 +6,8 @@ import Dashboard from "@/components/dashboard";
|
||||
import "./globals.css";
|
||||
import ThemeProvider from "./theme-provider";
|
||||
import Analytics from "./analytics";
|
||||
import PersistenceProvider from "./persistence-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
const geist = Geist({
|
||||
variable: "--font-geist",
|
||||
@ -23,7 +25,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: ReactNode;
|
||||
@ -37,10 +39,13 @@ export default function RootLayout({
|
||||
<ThemeProvider>
|
||||
<SidebarProvider>
|
||||
<Dashboard/>
|
||||
<main className="w-full">
|
||||
{children}
|
||||
<main className="w-full h-full">
|
||||
<PersistenceProvider>
|
||||
{children}
|
||||
</PersistenceProvider>
|
||||
</main>
|
||||
</SidebarProvider>
|
||||
<Toaster/>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
125
src/app/page.tsx
125
src/app/page.tsx
@ -1,23 +1,41 @@
|
||||
"use client";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { ReactNode } from "react";
|
||||
import { FormEvent, ReactNode, useState } from "react";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { db } from "@/lib/db";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ListIcon, PlusIcon } from "lucide-react";
|
||||
import { ListCheckIcon, PlusIcon } from "lucide-react";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import Search from "@/components/search";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import Project from "@/types/Project";
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { generateUUID } from "@/lib/uuid";
|
||||
|
||||
const Project = (): ReactNode => {
|
||||
const NewProjectFormSchema = z.object({
|
||||
title: z.string().nonempty("Title cannot be empty"),
|
||||
description: z.string().or(z.literal(""))
|
||||
});
|
||||
|
||||
const ProjectContainer = ({
|
||||
project
|
||||
}: {
|
||||
project: Project
|
||||
}): ReactNode => {
|
||||
return (
|
||||
<AspectRatio ratio={16 / 9}>
|
||||
<AspectRatio ratio={16 / 9} key={project.uuid}>
|
||||
<Card className=" rounded-lg shadow-md p-4 w-full h-full overflow-hidden">
|
||||
<h3 className="text-lg font-semibold">Project Title</h3>
|
||||
<p className="text-sm text-gray-600">Project description goes here.</p>
|
||||
<h3 className="text-lg font-semibold">{project.title}</h3>
|
||||
{project.description && <p className="text-sm text-gray-600">{project.description}</p>}
|
||||
</Card>
|
||||
</AspectRatio>
|
||||
)
|
||||
@ -25,39 +43,104 @@ const Project = (): ReactNode => {
|
||||
|
||||
export default function Home(): ReactNode {
|
||||
const isMobile = useIsMobile();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const projectsCount = useLiveQuery(() => {
|
||||
return db.projects.count();
|
||||
const projects = useLiveQuery(() => {
|
||||
return db.projects.filter((project) => project.title.includes(search)).toArray();
|
||||
});
|
||||
|
||||
const newProjectForm = useForm<z.infer<typeof NewProjectFormSchema>>({
|
||||
resolver: zodResolver(NewProjectFormSchema),
|
||||
defaultValues: {
|
||||
title: "New ClipFusion Project",
|
||||
description: ""
|
||||
}
|
||||
});
|
||||
|
||||
const newProjectSubmit = async (data: z.infer<typeof NewProjectFormSchema>) => {
|
||||
const date = Date.now();
|
||||
await db.projects.add({
|
||||
uuid: generateUUID(),
|
||||
creationDate: date,
|
||||
editDate: date,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
origin: ""
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5 w-full">
|
||||
<div className="p-5 w-full h-full">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<SidebarTrigger size="lg"/>
|
||||
<h2 className="font-bold break-keep text-xl sm:text-2xl md:text-3xl lg:text-4xl leading-none">Project Library</h2>
|
||||
<Label className="text-muted-foreground text-sm">(Found {projectsCount != undefined ? projectsCount : '-'} projects)</Label>
|
||||
{projects && <Label className="text-muted-foreground text-sm">(Found {projects.length} projects)</Label>}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between sticky top-0 bg-background gap-2 mt-3 pb-2 pt-2 w-full overscroll-none z-50">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button>
|
||||
<PlusIcon/> {!isMobile && "New Project"}
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon/> {!isMobile && "New Project"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Create New Project
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Fill in the information about your project. You can change it at any time later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...newProjectForm}>
|
||||
<form onSubmit={newProjectForm.handleSubmit(newProjectSubmit)} className="grid gap-3">
|
||||
<FormField control={newProjectForm.control} name="title" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}/>
|
||||
<FormField control={newProjectForm.control} name="description" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Tell something about your project" className="resize-y" {...field}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}/>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button type="submit">Create</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Toggle variant="outline">
|
||||
<ListIcon/> {!isMobile && "Select Projects"}
|
||||
<ListCheckIcon/> {!isMobile && "Select Projects"}
|
||||
</Toggle>
|
||||
<Search placeholder="Search Projects"/>
|
||||
<Search placeholder="Search Projects" value={search} onChange={(e) => setSearch(e.target.value)}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-5">
|
||||
<Project/>
|
||||
<Project/>
|
||||
<Project/>
|
||||
<Project/>
|
||||
<Project/>
|
||||
<Project/>
|
||||
{projects && projects.map((project) => <ProjectContainer project={project}/>)}
|
||||
</div>
|
||||
{(projects != undefined && projects.length == 0) && (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Label className="text-muted-foreground">Nothing to Show</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
106
src/app/persistence-provider.tsx
Normal file
106
src/app/persistence-provider.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||
import { isStoragePersisted, persist, tryPersistWithoutPromtingUser } from "@/lib/db";
|
||||
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface PersistenceContextData {
|
||||
persist: () => void;
|
||||
}
|
||||
|
||||
const PersistenceContext = createContext<PersistenceContextData | null>(null);
|
||||
|
||||
export const usePersistenceContext = (): PersistenceContextData => {
|
||||
const context = useContext(PersistenceContext);
|
||||
if (context == null) throw new Error("PersistenceContext is not provided!");
|
||||
return context;
|
||||
}
|
||||
|
||||
const PersistenceProvider = ({
|
||||
children
|
||||
}: {
|
||||
children: ReactNode
|
||||
}): ReactNode => {
|
||||
const [persistenceAlertOpen, setPersistenceAlertOpen] = useState(false);
|
||||
const [requestPersistenceAlertOpen, setRequestPersistenceOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const tryToPersist = async () => {
|
||||
const isPersistent = await isStoragePersisted();
|
||||
console.log(isPersistent);
|
||||
if (!isPersistent) {
|
||||
if (localStorage.getItem('persistence-status') != "persisted" && localStorage.getItem('persistence-status') == undefined) {
|
||||
const persistenceStatus = await tryPersistWithoutPromtingUser();
|
||||
localStorage.setItem("persistence-status", persistenceStatus);
|
||||
if (persistenceStatus == "never") {
|
||||
setPersistenceAlertOpen(true);
|
||||
}
|
||||
if (persistenceStatus == "prompt") {
|
||||
setRequestPersistenceOpen(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tryToPersist();
|
||||
}, []);
|
||||
|
||||
const allowPersistenceStorage = async () => {
|
||||
const persistResult = await persist();
|
||||
if (!persistResult) {
|
||||
console.log("Something went really wrong while enabling persistent storage");
|
||||
toast("Failed to Enable Persistent Storage", {
|
||||
description: persistResult == undefined ? "Persistent storage is not supported in your browser" : "Try again later"
|
||||
})
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('persistence-status', "persisted");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersistenceContext.Provider value={{persist: () => setRequestPersistenceOpen(true)}}>
|
||||
{children}
|
||||
</PersistenceContext.Provider>
|
||||
<AlertDialog open={persistenceAlertOpen} onOpenChange={setPersistenceAlertOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Persistent Storage is Unavailable
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
The browser may delete your local projects without notifying you in case it needs to free up space for other website's data that was used more recently than ClipFusion.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>
|
||||
OK
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<AlertDialog open={requestPersistenceAlertOpen} onOpenChange={setRequestPersistenceOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Enable Persistent Storage
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Persistent storage prevents browser from deleting your local data to free up space for other websites. You can enable persistent storage later in settings
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={allowPersistenceStorage}>
|
||||
Enable
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PersistenceProvider;
|
@ -1,13 +1,58 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { ReactNode } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { CheckedState } from "@radix-ui/react-checkbox";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { usePersistenceContext } from "../persistence-provider";
|
||||
|
||||
function PersistentStorageControl({
|
||||
status
|
||||
}: {
|
||||
status: string | null
|
||||
}): ReactNode {
|
||||
const { persist } = usePersistenceContext();
|
||||
if (status == null || status == '') return <Label className="text-muted-foreground">No information</Label>;
|
||||
if (status == "never") return <Label className="text-red-500">Unavailable</Label>;
|
||||
if (status == "prompt") return <Button onClick={persist}>Enable</Button>;
|
||||
return <Label className="text-green-400">Enabled</Label>;
|
||||
};
|
||||
|
||||
export default function Settings(): ReactNode {
|
||||
const [status, setStatus] = useState<string | null>('');
|
||||
|
||||
useEffect(() => {
|
||||
setStatus(localStorage.getItem("persistence-status"));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<div className="p-5 w-full">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<SidebarTrigger/>
|
||||
<h2 className="font-bold break-keep text-xl sm:text-2xl md:text-3xl lg:text-4xl leading-none">Settings</h2>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 md:lg:gap-2 mt-2 md:mt-4 lg:mt-5">
|
||||
<h3 className="font-semibold break-keep text-lg sm:text-xl md:text-2xl lg:text-3xl leading-none">Storage</h3>
|
||||
<div className="">
|
||||
<div className="flex flex-row justify-between items-center w-full max-w-96">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Label>Persistent Storage</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon size="15" className="opacity-60 hover:opacity-80"/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Persistent storage prevents browser from deleting your local data to free up space for other websites.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<PersistentStorageControl status={status}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -34,9 +34,11 @@ export const Dashboard = (): ReactNode => {
|
||||
return (
|
||||
<Sidebar>
|
||||
<SidebarHeader className="flex justify-center items-center mt-2">
|
||||
<ClipFusionLogo width="30" height="30">
|
||||
<p className="font-bold text-xl select-none">ClipFusion</p>
|
||||
</ClipFusionLogo>
|
||||
<Link href="/">
|
||||
<ClipFusionLogo width="30" height="30">
|
||||
<p className="font-bold text-xl select-none">ClipFusion</p>
|
||||
</ClipFusionLogo>
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
@ -78,7 +80,7 @@ export const Dashboard = (): ReactNode => {
|
||||
<PlusIcon/> <span className="sr-only">Add Folder</span>
|
||||
</SidebarGroupAction>
|
||||
<SidebarGroupContent>
|
||||
<Label className="flex justify-center text-sm text-muted-foreground">Nothing to show</Label>
|
||||
<Label className="flex justify-center text-sm text-muted-foreground">Nothing to Show</Label>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
@ -92,7 +94,7 @@ export const Dashboard = (): ReactNode => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<a href="https://github.com/ClipFusion-org/clipfusion" target="_blank">
|
||||
<Image src="/github-mark.svg" width="25" height="25" alt="ClipFusion GitHub Repository" className="dark:invert hover:opacity-95 active:scale-95"/>
|
||||
<Image src="/github-mark.svg" width="25" height="25" alt="ClipFusion GitHub Repository" className="duration-100 dark:invert hover:opacity-95 active:scale-95"/>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@ -102,7 +104,7 @@ export const Dashboard = (): ReactNode => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<a href="https://git.clipfusion.org/ClipFusion-org/clipfusion" target="_blank">
|
||||
<Image src="/clipfusion-git-logo.png" width="25" height="25" alt="ClipFusion Git Mirror" className="grayscale hover:opacity-95 active:scale-95"/>
|
||||
<Image src="/clipfusion-git-logo.png" width="25" height="25" alt="ClipFusion Git Mirror" className="duration-100 hover:opacity-95 active:scale-95"/>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
|
157
src/components/ui/alert-dialog.tsx
Normal file
157
src/components/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
106
src/favicon.svg
106
src/favicon.svg
@ -1,106 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
</defs>
|
||||
<rect x="0.0" y="0.0" width="51.2" height="51.2" fill="#e77728" />
|
||||
<rect x="0.0" y="51.2" width="51.2" height="51.2" fill="#e78523" />
|
||||
<rect x="0.0" y="102.4" width="51.2" height="51.2" fill="#e9931f" />
|
||||
<rect x="0.0" y="153.60000000000002" width="51.2" height="51.2" fill="#eaa11a" />
|
||||
<rect x="0.0" y="204.8" width="51.2" height="51.2" fill="#ebaf16" />
|
||||
<rect x="0.0" y="256.0" width="51.2" height="51.2" fill="#ecbd11" />
|
||||
<rect x="0.0" y="307.20000000000005" width="51.2" height="51.2" fill="#edcb0d" />
|
||||
<rect x="0.0" y="358.40000000000003" width="51.2" height="51.2" fill="#eed908" />
|
||||
<rect x="0.0" y="409.6" width="51.2" height="51.2" fill="#efe704" />
|
||||
<rect x="0.0" y="460.8" width="51.2" height="51.2" fill="#f0f600" />
|
||||
<rect x="51.2" y="0.0" width="51.2" height="51.2" fill="#cd7c36" />
|
||||
<rect x="51.2" y="51.2" width="51.2" height="51.2" fill="#d18732" />
|
||||
<rect x="51.2" y="102.4" width="51.2" height="51.2" fill="#d5922f" />
|
||||
<rect x="51.2" y="153.60000000000002" width="51.2" height="51.2" fill="#d99e2b" />
|
||||
<rect x="51.2" y="204.8" width="51.2" height="51.2" fill="#dda928" />
|
||||
<rect x="51.2" y="256.0" width="51.2" height="51.2" fill="#e1b424" />
|
||||
<rect x="51.2" y="307.20000000000005" width="51.2" height="51.2" fill="#e5bf20" />
|
||||
<rect x="51.2" y="358.40000000000003" width="51.2" height="51.2" fill="#e9ca1d" />
|
||||
<rect x="51.2" y="409.6" width="51.2" height="51.2" fill="#edd619" />
|
||||
<rect x="51.2" y="460.8" width="51.2" height="51.2" fill="#f1e116" />
|
||||
<rect x="102.4" y="0.0" width="51.2" height="51.2" fill="#b48144" />
|
||||
<rect x="102.4" y="51.2" width="51.2" height="51.2" fill="#bb8a42" />
|
||||
<rect x="102.4" y="102.4" width="51.2" height="51.2" fill="#c2923f" />
|
||||
<rect x="102.4" y="153.60000000000002" width="51.2" height="51.2" fill="#c99a3c" />
|
||||
<rect x="102.4" y="204.8" width="51.2" height="51.2" fill="#d0a339" />
|
||||
<rect x="102.4" y="256.0" width="51.2" height="51.2" fill="#d7ab37" />
|
||||
<rect x="102.4" y="307.20000000000005" width="51.2" height="51.2" fill="#deb334" />
|
||||
<rect x="102.4" y="358.40000000000003" width="51.2" height="51.2" fill="#e5bc31" />
|
||||
<rect x="102.4" y="409.6" width="51.2" height="51.2" fill="#ecc42e" />
|
||||
<rect x="102.4" y="460.8" width="51.2" height="51.2" fill="#f3cc2c" />
|
||||
<rect x="153.60000000000002" y="0.0" width="51.2" height="51.2" fill="#9b8753" />
|
||||
<rect x="153.60000000000002" y="51.2" width="51.2" height="51.2" fill="#a58c51" />
|
||||
<rect x="153.60000000000002" y="102.4" width="51.2" height="51.2" fill="#af924f" />
|
||||
<rect x="153.60000000000002" y="153.60000000000002" width="51.2" height="51.2" fill="#b9974d" />
|
||||
<rect x="153.60000000000002" y="204.8" width="51.2" height="51.2" fill="#c39c4b" />
|
||||
<rect x="153.60000000000002" y="256.0" width="51.2" height="51.2" fill="#cda249" />
|
||||
<rect x="153.60000000000002" y="307.20000000000005" width="51.2" height="51.2" fill="#d7a748" />
|
||||
<rect x="153.60000000000002" y="358.40000000000003" width="51.2" height="51.2" fill="#e1ad46" />
|
||||
<rect x="153.60000000000002" y="409.6" width="51.2" height="51.2" fill="#ebb244" />
|
||||
<rect x="153.60000000000002" y="460.8" width="51.2" height="51.2" fill="#f5b842" />
|
||||
<rect x="204.8" y="0.0" width="51.2" height="51.2" fill="#828c61" />
|
||||
<rect x="204.8" y="51.2" width="51.2" height="51.2" fill="#8f8f60" />
|
||||
<rect x="204.8" y="102.4" width="51.2" height="51.2" fill="#9c915f" />
|
||||
<rect x="204.8" y="153.60000000000002" width="51.2" height="51.2" fill="#a9945e" />
|
||||
<rect x="204.8" y="204.8" width="51.2" height="51.2" fill="#b6965d" />
|
||||
<rect x="204.8" y="256.0" width="51.2" height="51.2" fill="#c3995c" />
|
||||
<rect x="204.8" y="307.20000000000005" width="51.2" height="51.2" fill="#cf9b5b" />
|
||||
<rect x="204.8" y="358.40000000000003" width="51.2" height="51.2" fill="#dc9e5a" />
|
||||
<rect x="204.8" y="409.6" width="51.2" height="51.2" fill="#e9a059" />
|
||||
<rect x="204.8" y="460.8" width="51.2" height="51.2" fill="#f6a358" />
|
||||
<rect x="256.0" y="0.0" width="51.2" height="51.2" fill="#699270" />
|
||||
<rect x="256.0" y="51.2" width="51.2" height="51.2" fill="#799170" />
|
||||
<rect x="256.0" y="102.4" width="51.2" height="51.2" fill="#89916f" />
|
||||
<rect x="256.0" y="153.60000000000002" width="51.2" height="51.2" fill="#99916f" />
|
||||
<rect x="256.0" y="204.8" width="51.2" height="51.2" fill="#a8906f" />
|
||||
<rect x="256.0" y="256.0" width="51.2" height="51.2" fill="#b8906f" />
|
||||
<rect x="256.0" y="307.20000000000005" width="51.2" height="51.2" fill="#c88f6f" />
|
||||
<rect x="256.0" y="358.40000000000003" width="51.2" height="51.2" fill="#d88f6e" />
|
||||
<rect x="256.0" y="409.6" width="51.2" height="51.2" fill="#e88f6e" />
|
||||
<rect x="256.0" y="460.8" width="51.2" height="51.2" fill="#f88e6e" />
|
||||
<rect x="307.20000000000005" y="0.0" width="51.2" height="51.2" fill="#50977e" />
|
||||
<rect x="307.20000000000005" y="51.2" width="51.2" height="51.2" fill="#63947f" />
|
||||
<rect x="307.20000000000005" y="102.4" width="51.2" height="51.2" fill="#769180" />
|
||||
<rect x="307.20000000000005" y="153.60000000000002" width="51.2" height="51.2" fill="#888d80" />
|
||||
<rect x="307.20000000000005" y="204.8" width="51.2" height="51.2" fill="#9b8a81" />
|
||||
<rect x="307.20000000000005" y="256.0" width="51.2" height="51.2" fill="#ae8782" />
|
||||
<rect x="307.20000000000005" y="307.20000000000005" width="51.2" height="51.2" fill="#c18382" />
|
||||
<rect x="307.20000000000005" y="358.40000000000003" width="51.2" height="51.2" fill="#d48083" />
|
||||
<rect x="307.20000000000005" y="409.6" width="51.2" height="51.2" fill="#e77d84" />
|
||||
<rect x="307.20000000000005" y="460.8" width="51.2" height="51.2" fill="#fa7a84" />
|
||||
<rect x="358.40000000000003" y="0.0" width="51.2" height="51.2" fill="#379d8d" />
|
||||
<rect x="358.40000000000003" y="51.2" width="51.2" height="51.2" fill="#4d968e" />
|
||||
<rect x="358.40000000000003" y="102.4" width="51.2" height="51.2" fill="#629090" />
|
||||
<rect x="358.40000000000003" y="153.60000000000002" width="51.2" height="51.2" fill="#788a91" />
|
||||
<rect x="358.40000000000003" y="204.8" width="51.2" height="51.2" fill="#8e8493" />
|
||||
<rect x="358.40000000000003" y="256.0" width="51.2" height="51.2" fill="#a47e94" />
|
||||
<rect x="358.40000000000003" y="307.20000000000005" width="51.2" height="51.2" fill="#ba7796" />
|
||||
<rect x="358.40000000000003" y="358.40000000000003" width="51.2" height="51.2" fill="#d07197" />
|
||||
<rect x="358.40000000000003" y="409.6" width="51.2" height="51.2" fill="#e56b99" />
|
||||
<rect x="358.40000000000003" y="460.8" width="51.2" height="51.2" fill="#fb659a" />
|
||||
<rect x="409.6" y="0.0" width="51.2" height="51.2" fill="#1ea29b" />
|
||||
<rect x="409.6" y="51.2" width="51.2" height="51.2" fill="#36999d" />
|
||||
<rect x="409.6" y="102.4" width="51.2" height="51.2" fill="#4f90a0" />
|
||||
<rect x="409.6" y="153.60000000000002" width="51.2" height="51.2" fill="#6887a2" />
|
||||
<rect x="409.6" y="204.8" width="51.2" height="51.2" fill="#817ea5" />
|
||||
<rect x="409.6" y="256.0" width="51.2" height="51.2" fill="#9a75a7" />
|
||||
<rect x="409.6" y="307.20000000000005" width="51.2" height="51.2" fill="#b26ba9" />
|
||||
<rect x="409.6" y="358.40000000000003" width="51.2" height="51.2" fill="#cb62ac" />
|
||||
<rect x="409.6" y="409.6" width="51.2" height="51.2" fill="#e459ae" />
|
||||
<rect x="409.6" y="460.8" width="51.2" height="51.2" fill="#fd50b0" />
|
||||
<rect x="460.8" y="0.0" width="51.2" height="51.2" fill="#05a8aa" />
|
||||
<rect x="460.8" y="51.2" width="51.2" height="51.2" fill="#209bad" />
|
||||
<rect x="460.8" y="102.4" width="51.2" height="51.2" fill="#3c90b0" />
|
||||
<rect x="460.8" y="153.60000000000002" width="51.2" height="51.2" fill="#5884b3" />
|
||||
<rect x="460.8" y="204.8" width="51.2" height="51.2" fill="#7478b6" />
|
||||
<rect x="460.8" y="256.0" width="51.2" height="51.2" fill="#8f6cba" />
|
||||
<rect x="460.8" y="307.20000000000005" width="51.2" height="51.2" fill="#ab60bd" />
|
||||
<rect x="460.8" y="358.40000000000003" width="51.2" height="51.2" fill="#c754c0" />
|
||||
<rect x="460.8" y="409.6" width="51.2" height="51.2" fill="#e348c3" />
|
||||
<rect x="460.8" y="460.8" width="51.2" height="51.2" fill="#ff3cc7" />
|
||||
</svg>
|
Before Width: | Height: | Size: 7.8 KiB |
@ -1,3 +1,73 @@
|
||||
import EditorDB from "@/types/EditorDB";
|
||||
|
||||
export const db = new EditorDB();
|
||||
export const db = new EditorDB();
|
||||
|
||||
// StorageManager code from https://dexie.org/docs/StorageManager
|
||||
|
||||
/** Check if storage is persisted already.
|
||||
@returns {Promise<boolean>} Promise resolved with true if current origin is
|
||||
using persistent storage, false if not, and undefined if the API is not
|
||||
present.
|
||||
*/
|
||||
export async function isStoragePersisted() : Promise<boolean | undefined> {
|
||||
return await navigator.storage && navigator.storage.persisted ?
|
||||
navigator.storage.persisted() :
|
||||
undefined;
|
||||
}
|
||||
|
||||
/** Tries to convert to persisted storage.
|
||||
@returns {Promise<boolean>} Promise resolved with true if successfully
|
||||
persisted the storage, false if not, and undefined if the API is not present.
|
||||
*/
|
||||
export async function persist(): Promise<boolean | undefined> {
|
||||
return await navigator.storage && navigator.storage.persist ?
|
||||
navigator.storage.persist() :
|
||||
undefined;
|
||||
}
|
||||
|
||||
/** Queries available disk quota.
|
||||
@see https://developer.mozilla.org/en-US/docs/Web/API/StorageEstimate
|
||||
@returns {Promise<{quota: number, usage: number}>} Promise resolved with
|
||||
{quota: number, usage: number} or undefined.
|
||||
*/
|
||||
export async function showEstimatedQuota(): Promise<StorageEstimate | undefined> {
|
||||
return await navigator.storage && navigator.storage.estimate ?
|
||||
navigator.storage.estimate() :
|
||||
undefined;
|
||||
}
|
||||
|
||||
/** Tries to persist storage without ever prompting user.
|
||||
@returns {Promise<string>}
|
||||
"never" In case persisting is not ever possible. Caller don't bother
|
||||
asking user for permission.
|
||||
"prompt" In case persisting would be possible if prompting user first.
|
||||
"persisted" In case this call successfully silently persisted the storage,
|
||||
or if it was already persisted.
|
||||
*/
|
||||
export async function tryPersistWithoutPromtingUser() {
|
||||
if (!navigator.storage || !navigator.storage.persisted) {
|
||||
return "never";
|
||||
}
|
||||
let persisted = await navigator.storage.persisted();
|
||||
if (persisted) {
|
||||
return "persisted";
|
||||
}
|
||||
if (!navigator.permissions || !navigator.permissions.query) {
|
||||
return "prompt"; // It MAY be successful to prompt. Don't know.
|
||||
}
|
||||
const permission = await navigator.permissions.query({
|
||||
name: "persistent-storage"
|
||||
});
|
||||
if (permission.state === "granted") {
|
||||
persisted = await navigator.storage.persist();
|
||||
if (persisted) {
|
||||
return "persisted";
|
||||
} else {
|
||||
throw new Error("Failed to persist");
|
||||
}
|
||||
}
|
||||
if (permission.state === "prompt") {
|
||||
return "prompt";
|
||||
}
|
||||
return "never";
|
||||
}
|
7
src/lib/uuid.ts
Normal file
7
src/lib/uuid.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const generateUUID = (): string => (
|
||||
"randomUUID" in crypto
|
||||
? crypto.randomUUID()
|
||||
: "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
||||
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
|
||||
)
|
||||
);
|
@ -1,11 +1,11 @@
|
||||
import { Entity } from "dexie";
|
||||
import type EditorDB from "./EditorDB";
|
||||
|
||||
class Project extends Entity<EditorDB> {
|
||||
export default class Project extends Entity<EditorDB> {
|
||||
uuid!: string;
|
||||
name!: string;
|
||||
origin!: string; // If the project was duplicated, origin will be equal to the UUID of original project
|
||||
title!: string;
|
||||
description!: string;
|
||||
creationDate!: number;
|
||||
editDate!: number;
|
||||
}
|
||||
|
||||
export default Project;
|
||||
}
|
Loading…
Reference in New Issue
Block a user