Merge branch 'main' into feat-block-console-until-verify

This commit is contained in:
Harsh Mahajan
2025-09-22 17:49:48 +05:30
committed by GitHub
93 changed files with 2404 additions and 344 deletions
+3 -1
View File
@@ -32,7 +32,9 @@ export default ts.config(
'svelte/no-at-html-tags': 'off',
'svelte/no-useless-mustaches': 'off',
'svelte/no-reactive-reassign': 'off',
'svelte/no-reactive-literals': 'off'
'svelte/no-reactive-literals': 'off',
// TODO: @itznotabug, this requires a big refactor!
'svelte/no-navigation-without-resolve': 'off'
}
},
{
+4 -4
View File
@@ -22,11 +22,11 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f4da718",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f4da718",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1",
"@faker-js/faker": "^9.9.0",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
@@ -54,7 +54,7 @@
"@melt-ui/svelte": "^0.86.5",
"@playwright/test": "^1.51.1",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.36.2",
"@sveltejs/kit": "^2.42.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
+47 -40
View File
@@ -12,20 +12,20 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
'@appwrite.io/pink-icons-svelte':
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f4da718
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f4da718(svelte@5.25.3)
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1(svelte@5.25.3)
'@appwrite.io/pink-legacy':
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f4da718
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f4da718(svelte@5.25.3)
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1(svelte@5.25.3)
'@faker-js/faker':
specifier: ^9.9.0
version: 9.9.0
@@ -34,7 +34,7 @@ importers:
version: 2.11.8
'@sentry/sveltekit':
specifier: ^8.38.0
version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
'@stripe/stripe-js':
specifier: ^3.5.0
version: 3.5.0
@@ -101,10 +101,10 @@ importers:
version: 1.51.1
'@sveltejs/adapter-static':
specifier: ^3.0.8
version: 3.0.8(@sveltejs/kit@2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))
version: 3.0.8(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))
'@sveltejs/kit':
specifier: ^2.36.2
version: 2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
specifier: ^2.42.1
version: 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/vite-plugin-svelte':
specifier: ^5.0.3
version: 5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
@@ -260,8 +260,8 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562}
version: 1.10.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
@@ -269,8 +269,8 @@ packages:
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f4da718':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f4da718}
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1}
version: 2.0.0-RC.1
peerDependencies:
svelte: ^4.0.0
@@ -284,8 +284,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f4da718':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f4da718}
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
@@ -352,8 +352,8 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.28.3':
resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==}
'@babel/parser@7.28.4':
resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -373,8 +373,8 @@ packages:
resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.2':
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
'@babel/types@7.28.4':
resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
engines: {node: '>=6.9.0'}
'@csstools/color-helpers@5.0.2':
@@ -1264,8 +1264,8 @@ packages:
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/kit@2.36.3':
resolution: {integrity: sha512-MVzwZz1GFznEQbL3f0i2v9AIc3lZH01izQj6XfIrthmZAwOzyXJCgXbLRss8vk//HfYsE4w6Tz+ukbf3rScPNQ==}
'@sveltejs/kit@2.42.1':
resolution: {integrity: sha512-SoWdb/OxEKHMlXZ78eNZa4pR1YdOGnUfcFj5NylJv+ZaTrnHK5xapw+7RJP2MiS1D4T+sEH25/tmMtmEl8p6VA==}
engines: {node: '>=18.13'}
hasBin: true
peerDependencies:
@@ -2607,6 +2607,9 @@ packages:
magic-string@0.30.18:
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
magic-string@0.30.7:
resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==}
engines: {node: '>=12'}
@@ -3050,8 +3053,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
seroval-plugins@1.3.2:
resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==}
seroval-plugins@1.3.3:
resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==}
engines: {node: '>=10'}
peerDependencies:
seroval: ^1.0
@@ -3700,13 +3703,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@7747562': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f4da718(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@f2198f1(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3719,7 +3722,7 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f4da718(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@f2198f1(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
@@ -3822,9 +3825,9 @@ snapshots:
dependencies:
'@babel/types': 7.27.0
'@babel/parser@7.28.3':
'@babel/parser@7.28.4':
dependencies:
'@babel/types': 7.28.2
'@babel/types': 7.28.4
'@babel/runtime@7.27.0':
dependencies:
@@ -3853,7 +3856,7 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/types@7.28.2':
'@babel/types@7.28.4':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
@@ -4671,14 +4674,14 @@ snapshots:
magic-string: 0.30.7
svelte: 5.25.3
'@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))':
'@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))':
dependencies:
'@sentry/core': 8.55.0
'@sentry/node': 8.55.0
'@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)
'@sentry/svelte': 8.55.0(svelte@5.25.3)
'@sentry/vite-plugin': 2.22.6
'@sveltejs/kit': 2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/kit': 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
magic-string: 0.30.7
magicast: 0.2.8
sorcery: 1.0.0
@@ -4750,11 +4753,11 @@ snapshots:
dependencies:
acorn: 8.15.0
'@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))':
'@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))':
dependencies:
'@sveltejs/kit': 2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/kit': 2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/kit@2.36.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))':
'@sveltejs/kit@2.42.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@7.0.6(@types/node@22.13.14)(sass@1.86.0))':
dependencies:
'@standard-schema/spec': 1.0.0
'@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0)
@@ -5121,7 +5124,7 @@ snapshots:
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.28.3
'@babel/parser': 7.28.4
'@vue/shared': 3.5.13
entities: 4.5.0
estree-walker: 2.0.2
@@ -5134,13 +5137,13 @@ snapshots:
'@vue/compiler-sfc@3.5.13':
dependencies:
'@babel/parser': 7.28.3
'@babel/parser': 7.28.4
'@vue/compiler-core': 3.5.13
'@vue/compiler-dom': 3.5.13
'@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13
estree-walker: 2.0.2
magic-string: 0.30.18
magic-string: 0.30.19
postcss: 8.5.6
source-map-js: 1.2.1
@@ -6282,6 +6285,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magic-string@0.30.19:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magic-string@0.30.7:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -6703,7 +6710,7 @@ snapshots:
semver@7.7.1: {}
seroval-plugins@1.3.2(seroval@1.3.2):
seroval-plugins@1.3.3(seroval@1.3.2):
dependencies:
seroval: 1.3.2
@@ -6786,7 +6793,7 @@ snapshots:
dependencies:
csstype: 3.1.3
seroval: 1.3.2
seroval-plugins: 1.3.2(seroval@1.3.2)
seroval-plugins: 1.3.3(seroval@1.3.2)
solid-swr-store@0.10.7(solid-js@1.9.5)(swr-store@0.10.6):
dependencies:
+4 -3
View File
@@ -6,7 +6,7 @@ import { user } from '$lib/stores/user';
import { ENV, MODE, VARS, isCloud } from '$lib/system';
import { AppwriteException } from '@appwrite.io/console';
import { browser } from '$app/environment';
import { getReferrerAndUtmSource } from '$lib/helpers/utm';
import { getReferrerAndUtmSource, getTrackedQueryParams } from '$lib/helpers/utm';
function plausible(domain: string): AnalyticsPlugin {
if (!browser) return { name: 'analytics-plugin-plausible' };
@@ -65,7 +65,7 @@ export function trackEvent(name: string, data: object = null): void {
};
}
data = { ...data, ...getReferrerAndUtmSource() };
data = { ...data, ...getReferrerAndUtmSource(), ...getTrackedQueryParams() };
if (ENV.DEV || ENV.PREVIEW) {
console.debug(`[Analytics] Event ${name} ${path}`, data);
@@ -429,5 +429,6 @@ export enum Submit {
RecordUpdate = 'submit_dns_record_update',
RecordDelete = 'submit_dns_record_delete',
SearchClear = 'submit_clear_search',
FrameworkDetect = 'submit_framework_detect'
FrameworkDetect = 'submit_framework_detect',
TestimonialView = 'testimonial_view'
}
+7 -4
View File
@@ -21,13 +21,16 @@ export const timer: Action<HTMLElement, Props> = (node, props) => {
}
function step() {
const diffInSeconds = Math.floor((new Date().getTime() - startDate.getTime()) / 1000);
const minutes = Math.floor(diffInSeconds / 60);
const elapsedSeconds = Math.max(
0,
Math.floor((new Date().getTime() - startDate.getTime()) / 1000)
);
const minutes = Math.floor(elapsedSeconds / 60);
if (minutes > 0) {
const seconds = diffInSeconds % 60;
const seconds = elapsedSeconds % 60;
node.textContent = `${minutes}m ${seconds}s`;
} else {
node.textContent = calculateTime(diffInSeconds);
node.textContent = calculateTime(elapsedSeconds);
}
frame = window.requestAnimationFrame(step);
}
@@ -23,7 +23,7 @@
}
</script>
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes('/console/account')}
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes(base + '/account')}
<GradientBanner on:close={handleClose}>
<Layout.Stack
gap="m"
@@ -14,10 +14,13 @@
$: plans = Object.values(page.data.plans.plans) as Plan[];
$: currentPlanInList = plans.some((plan) => plan.$id === $currentPlan?.$id);
// experiment to remove scale plan temporarily
$: plansWithoutScale = plans.filter((plan) => plan.$id != BillingPlan.SCALE);
</script>
<Layout.Stack>
{#each plans as plan}
{#each plansWithoutScale as plan}
<LabelCard
name="plan"
bind:group={billingPlan}
+33 -10
View File
@@ -18,14 +18,15 @@
import { BottomSheet } from '$lib/components';
import { isSmallViewport } from '$lib/stores/viewport';
import { isCloud } from '$lib/system';
import { goto } from '$app/navigation';
import { goto, beforeNavigate } from '$app/navigation';
import { base } from '$app/paths';
import { currentPlan, newOrgModal, organization } from '$lib/stores/organization';
import { Click, trackEvent } from '$lib/actions/analytics';
import { type Models, Query } from '@appwrite.io/console';
import { ID, type Models, Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import { BillingPlan } from '$lib/constants';
import { onDestroy } from 'svelte';
type Organization = {
name: string;
@@ -34,11 +35,13 @@
isSelected: boolean;
};
const menubarBuilder = createMenubar();
const {
elements: { menubar },
builders: { createMenu }
} = createMenubar();
} = menubarBuilder;
const orgMenuBuilder = createMenu();
const {
elements: {
trigger: triggerOrganizations,
@@ -46,8 +49,9 @@
item: itemOrganizations,
separator: separatorOrganizations
},
builders: { createSubmenu: createSubmenuOrganizations, createMenuRadioGroup }
} = createMenu();
builders: { createSubmenu: createSubmenuOrganizations, createMenuRadioGroup },
states: orgMenuStates
} = orgMenuBuilder;
const {
elements: { radioGroup: radioGroupOrganizations }
@@ -57,14 +61,16 @@
elements: { subMenu: subMenuOrganizations, subTrigger: subTriggerOrganizations }
} = createSubmenuOrganizations();
const projectsMenuBuilder = createMenu();
const {
elements: {
trigger: triggerProjects,
menu: menuProjects,
item: itemProjects,
separator: separatorProjects
}
} = createMenu();
},
states: projectsMenuStates
} = projectsMenuBuilder;
let isLoadingProjects = false;
let loadedProjects: Models.ProjectList = { total: 0, projects: [] };
@@ -76,6 +82,23 @@
let organisationBottomSheetOpen = false;
let projectsBottomSheet: Promise<SheetMenu | null> = null;
function closeAllMenus() {
if (orgMenuStates?.open) {
orgMenuStates.open.set(false);
}
if (projectsMenuStates?.open) {
projectsMenuStates.open.set(false);
}
projectsBottomSheetOpen = false;
organisationBottomSheetOpen = false;
}
beforeNavigate(closeAllMenus);
onDestroy(closeAllMenus);
function createOrg() {
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
if (isCloud) {
@@ -105,6 +128,8 @@
async function createProjectsBottomSheet(
organization: Organization
): Promise<SheetMenu | null> {
const isOnProjects = page.route.id.includes('project-[region]-[project]');
if (!isOnProjects) return null;
isLoadingProjects = true;
@@ -212,12 +237,10 @@
? $currentPlan.name
: selectedOrg?.tierName; // fallback
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`;
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}-${ID.unique()}`;
$: organizationId = currentProject?.teamId;
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
$: shouldReloadProjects = isLoadingProjects
? false
: currentProject && loadedProjects.projects.length
+60 -3
View File
@@ -6,10 +6,11 @@
InputText,
InputTags,
InputSelectCheckbox,
InputDateTime
InputDateTime,
InputPoint
} from '$lib/elements/forms';
import { onMount, createEventDispatcher } from 'svelte';
import { operators, addFilter, queries, tags } from './store';
import { operators, addFilter, queries, tags, ValidOperators } from './store';
import type { Column } from '$lib/helpers/types';
import type { Writable } from 'svelte/store';
import { TagList } from '.';
@@ -18,6 +19,7 @@
let {
value = $bindable(null),
distanceValue = $bindable(null),
columns,
columnId = $bindable(null),
arrayValues = $bindable([]),
@@ -27,6 +29,7 @@
// We cast to any to not cause type errors in the input components
/* eslint @typescript-eslint/no-explicit-any: 'off' */
value?: any;
distanceValue?: number | null;
columns: Writable<Column[]>;
columnId?: string | null;
arrayValues?: string[];
@@ -61,21 +64,57 @@
}));
});
// Check if the current operator is a distance-based operator
let isDistanceOperator = $derived(
operatorKey &&
[
ValidOperators.DistanceEqual,
ValidOperators.DistanceNotEqual,
ValidOperators.DistanceGreaterThan,
ValidOperators.DistanceLessThan
].includes(operatorKey as ValidOperators)
);
onMount(() => {
value = column?.array ? [] : null;
if (column?.type === 'datetime') {
const now = new Date();
value = now.toISOString().slice(0, 16);
}
// Initialize spatial data with default values
if (column?.type === 'point') {
value = [0, 0];
} else if (column?.type === 'linestring') {
value = [
[0, 0],
[1, 1]
];
} else if (column?.type === 'polygon') {
value = [
[
[0, 0],
[1, 1],
[2, 2],
[0, 0]
]
];
}
});
const dispatch = createEventDispatcher<{ clear: void; apply: { applied: number } }>();
function addFilterAndReset() {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues);
// For distance operators, pass the distance as a separate parameter
if (isDistanceOperator && distanceValue !== null && value !== null) {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues, distanceValue);
} else {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues);
}
columnId = null;
operatorKey = null;
value = null;
distanceValue = null;
arrayValues = [];
dispatch('apply', { applied: appliedTags.length });
if (singleCondition) {
@@ -143,11 +182,29 @@
{#key value}
<InputDateTime id="value" bind:value step={60} type="datetime-local" />
{/key}
{:else if column.type === 'point' || column.type === 'linestring' || column.type === 'polygon'}
<InputPoint
values={value || [0, 0]}
onChangePoint={(index, newValue) => {
if (!value) value = [0, 0];
value[index] = newValue;
}} />
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
{/if}
</ul>
{/if}
{#if isDistanceOperator}
<div class="u-margin-block-start-8">
<InputNumber
id="distance"
bind:value={distanceValue}
placeholder="Enter distance"
step={0.001}
required />
</div>
{/if}
{/if}
{#if !singleCondition}
<Button text disabled={isDisabled} class="u-margin-block-start-4" submit>
+23 -1
View File
@@ -36,6 +36,7 @@
/* eslint @typescript-eslint/no-explicit-any: 'off' */
let value: any = null;
let distanceValue: number | null = null;
let selectedColumn: string | null = null;
let operatorKey: string | null = null;
let arrayValues: string[] = [];
@@ -46,6 +47,16 @@
$: filtersAppliedCount = $tags.length;
// Check if the current operator is a distance-based operator
$: isDistanceOperator =
operatorKey &&
[
ValidOperators.DistanceEqual,
ValidOperators.DistanceNotEqual,
ValidOperators.DistanceGreaterThan,
ValidOperators.DistanceLessThan
].includes(operatorKey as ValidOperators);
beforeNavigate(() => {
showFiltersDesktop = false;
showFiltersMobile = false;
@@ -54,6 +65,8 @@
function clearAll() {
selectedColumn = null;
value = null;
distanceValue = null;
queries.clearAll();
if (clearOnClick) {
trackEvent(Submit.FilterClear, { source: analyticsSource });
@@ -73,9 +86,15 @@
value ||
arrayValues.length)
) {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
// For distance operators, pass the distance as a separate parameter
if (isDistanceOperator && distanceValue !== null && value !== null) {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues, distanceValue);
} else {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
}
selectedColumn = null;
value = null;
distanceValue = null;
operatorKey = null;
arrayValues = [];
}
@@ -97,6 +116,7 @@
$: if (!showFiltersDesktop && !showFiltersMobile) {
selectedColumn = null;
value = null;
distanceValue = null;
operatorKey = null;
arrayValues = [];
}
@@ -163,6 +183,7 @@
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:distanceValue
bind:arrayValues
{columns}
{singleCondition}
@@ -225,6 +246,7 @@
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:distanceValue
bind:arrayValues
{singleCondition}
on:apply={afterApply}
+155 -14
View File
@@ -5,6 +5,7 @@ import { Query } from '@appwrite.io/console';
import { toLocaleDateTime } from '$lib/helpers/date';
import { derived, get, writable } from 'svelte/store';
import type { Column, ColumnType } from '$lib/helpers/types';
import { isSpatialType } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store';
export type TagValue = {
tag: string;
@@ -13,7 +14,7 @@ export type TagValue = {
export type Operator = {
toTag: (column: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (column: string, input?: string | number | string[]) => string;
toQuery: (column: string, input?: string | number | string[], distance?: number) => string;
types: ColumnType[];
hideInput?: boolean;
};
@@ -35,13 +36,14 @@ function initQueries(initialValue = new Map<TagValue, string>()) {
operator: Operator;
column: Column;
value: string | number | string[];
distance?: number;
};
function addFilter({ column, operator, value }: AddFilterArgs) {
function addFilter({ column, operator, value, distance }: AddFilterArgs) {
queries.update((map) => {
map.set(
operator.toTag(column.title, value, column?.type),
operator.toQuery(column.id, value)
operator.toQuery(column.id, value, distance)
);
return map;
});
@@ -101,15 +103,27 @@ export function addFilter(
columnId: string,
operatorKey: string,
value: any, // We cast to any to not cause type errors in the input components
arrayValues: string[] = []
arrayValues: string[] = [],
distance?: number
) {
const operator = operatorKey ? operators[operatorKey] : null;
const column = columns.find((c) => c.id === columnId) as Column;
if (!column || !operator) return;
if (column.array) {
queries.addFilter({ column, operator, value: arrayValues });
queries.addFilter({ column, operator, value: arrayValues, distance });
} else {
queries.addFilter({ column, operator, value: value ?? '' });
const isSpatialArrayWrappingRequired =
isSpatialType(column) &&
[
ValidOperators.Contains,
ValidOperators.Equal,
ValidOperators.NotEqual,
ValidOperators.NotContains
].includes(operatorKey as ValidOperators);
const preparedValue =
isSpatialArrayWrappingRequired && value != null ? [value] : (value ?? '');
queries.addFilter({ column, operator, value: preparedValue, distance });
}
}
@@ -124,7 +138,20 @@ export enum ValidOperators {
NotEqual = 'not equal',
IsNotNull = 'is not null',
IsNull = 'is null',
Contains = 'contains'
Contains = 'contains',
NotContains = 'not contains',
Crosses = 'crosses',
NotCrosses = 'not crosses',
DistanceEqual = 'distance equal',
DistanceNotEqual = 'distance not equal',
DistanceGreaterThan = 'distance greater than',
DistanceLessThan = 'distance less than',
Intersects = 'intersects',
NotIntersects = 'not intersects',
Overlaps = 'overlaps',
NotOverlaps = 'not overlaps',
Touches = 'touches',
NotTouches = 'not touches'
}
export enum ValidTypes {
@@ -134,13 +161,16 @@ export enum ValidTypes {
Boolean = 'boolean',
Datetime = 'datetime',
Relationship = 'relationship',
Enum = 'enum'
Enum = 'enum',
Point = 'point',
Line = 'linestring',
Polygon = 'polygon'
}
const operatorsDefault = new Map<
ValidOperators,
{
query: (col: string, input: string | number | string[]) => string;
query: (col: string, input: string | number | string[], distance?: number) => string;
types: ColumnType[];
hideInput?: boolean;
}
@@ -184,7 +214,10 @@ const operatorsDefault = new Map<
ValidTypes.Integer,
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Enum
ValidTypes.Enum,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
]
}
],
@@ -192,7 +225,15 @@ const operatorsDefault = new Map<
ValidOperators.NotEqual,
{
query: Query.notEqual,
types: [ValidTypes.String, ValidTypes.Integer, ValidTypes.Double, ValidTypes.Boolean]
types: [
ValidTypes.String,
ValidTypes.Integer,
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
]
}
],
[
@@ -205,7 +246,10 @@ const operatorsDefault = new Map<
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Datetime,
ValidTypes.Relationship
ValidTypes.Relationship,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
],
hideInput: true
}
@@ -220,7 +264,10 @@ const operatorsDefault = new Map<
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Datetime,
ValidTypes.Relationship
ValidTypes.Relationship,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
],
hideInput: true
}
@@ -235,9 +282,103 @@ const operatorsDefault = new Map<
ValidTypes.Double,
ValidTypes.Boolean,
ValidTypes.Datetime,
ValidTypes.Enum
ValidTypes.Enum,
ValidTypes.Point,
ValidTypes.Line,
ValidTypes.Polygon
]
}
],
[
ValidOperators.NotContains,
{
query: Query.contains,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.Crosses,
{
query: Query.crosses,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotCrosses,
{
query: Query.notCrosses,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.DistanceEqual,
{
query: Query.distanceEqual,
types: [ValidTypes.Point]
}
],
[
ValidOperators.DistanceNotEqual,
{
query: Query.distanceNotEqual,
types: [ValidTypes.Point]
}
],
[
ValidOperators.DistanceGreaterThan,
{
query: Query.distanceGreaterThan,
types: [ValidTypes.Point]
}
],
[
ValidOperators.DistanceLessThan,
{
query: Query.distanceLessThan,
types: [ValidTypes.Point]
}
],
[
ValidOperators.Intersects,
{
query: Query.intersects,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotIntersects,
{
query: Query.notIntersects,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.Overlaps,
{
query: Query.overlaps,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotOverlaps,
{
query: Query.notOverlaps,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.Touches,
{
query: Query.touches,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
],
[
ValidOperators.NotTouches,
{
query: Query.notTouches,
types: [ValidTypes.Point, ValidTypes.Line, ValidTypes.Polygon]
}
]
]);
+7 -6
View File
@@ -42,6 +42,7 @@
import type { HTMLAttributes } from 'svelte/elements';
import type { Models } from '@appwrite.io/console';
import { noWidthTransition } from '$lib/stores/sidebar';
import { base } from '$app/paths';
type $$Props = HTMLAttributes<HTMLElement> & {
state?: 'closed' | 'open' | 'icons';
@@ -138,7 +139,7 @@
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
class="progress-card"
href={`/console/project-${project.region}-${project.$id}/get-started`}
href={`${base}/project-${project?.region}-${project?.$id}/get-started`}
on:click={() => {
trackEvent('click_menu_get_started');
sideBarIsOpen = false;
@@ -162,7 +163,7 @@
<Layout.Stack direction="column" gap="s">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/overview/platforms`}
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}
class="link"
class:active={isSelected('overview')}
on:click={() => {
@@ -192,7 +193,7 @@
{#each buildProjectOptions as projectOption}
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/${projectOption.slug}`}
href={`${base}/project-${project.region}-${project.$id}/${projectOption.slug}`}
class="link"
class:active={isSelected(projectOption.slug)}
on:click={() => {
@@ -223,7 +224,7 @@
{#each deployProjectOptions as projectOption}
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/${projectOption.slug}`}
href={`${base}/project-${project.region}-${project.$id}/${projectOption.slug}`}
class="link"
class:active={isSelected(projectOption.slug)}
on:click={() => {
@@ -253,7 +254,7 @@
<div class="mobile-tablet-settings">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/settings`}
href={`${base}/project-${project.region}-${project.$id}/settings`}
on:click={() => {
trackEvent('click_menu_settings');
sideBarIsOpen = false;
@@ -313,7 +314,7 @@
<div class="only-desktop">
<Tooltip placement="right" disabled={state !== 'icons'}>
<a
href={`/console/project-${project.region}-${project.$id}/settings`}
href={`${base}/project-${project.region}-${project.$id}/settings`}
class="link"
on:click={() => {
trackEvent('click_menu_settings');
+58
View File
@@ -0,0 +1,58 @@
import DevKind from '$lib/images/testimonials/hassan.png';
import LangX from '$lib/images/testimonials/xue.webp';
import KCollect from '$lib/images/testimonials/ryan-oconner-testimonial.png';
import Phil from '$lib/images/testimonials/phil.jpg';
export interface Testimonial {
id: string;
logo: string;
headline: string;
blurb: string;
name: string;
title: string;
avatar: string;
}
export const testimonials: Testimonial[] = [
{
id: 'hassan-devkind',
logo: DevKind,
headline: 'DevKind reduced development time by 60% and lowered server costs by 40%',
blurb: 'A special thanks to Appwrite for providing robust features and seamless functionality.',
name: 'Hassan Ahmed',
title: 'Engineer at DevKind',
avatar: DevKind
},
{
id: 'xue-langx',
logo: LangX,
headline: 'LangX handled millions of requests using Appwrite',
blurb: 'With its comprehensive suite of services, Appwrite emerged as an ideal choice for my needs.',
name: 'Xue',
title: 'Founder at LangX',
avatar: LangX
},
{
id: 'ryan-kcollect',
logo: KCollect,
headline: 'K-Collect reduced infrastructure costs by 700%',
blurb: 'A major impact that Appwrite made was the amount of time and stress saved.',
name: "Ryan O'Connor",
title: 'Founder at K-Collect',
avatar: KCollect
},
{
id: 'phil-majikkids',
logo: Phil,
headline: 'Just like a Swiss Army Knife - choose and use the tools you need',
blurb: 'Just like a Swiss Army Knife you can choose and use the tools that you need with Appwrite.',
name: 'Phil McCluskey',
title: 'App Manager, Majik Kids',
avatar: Phil
}
];
export function getRandomTestimonial(): Testimonial {
const randomIndex = Math.floor(Math.random() * testimonials.length);
return testimonials[randomIndex];
}
+3
View File
@@ -31,3 +31,6 @@ export { default as InputDigits } from './inputDigits.svelte';
export { default as InputSelectCheckbox } from './inputSelectCheckbox.svelte';
export { default as InputFilePicker } from './inputFilePicker.svelte';
export { default as Seekbar } from './Seekbar.svelte';
export { default as InputPoint } from './inputPoint.svelte';
export { default as InputLine } from './inputLine.svelte';
export { default as InputPolygon } from './inputPolygon.svelte';
+61
View File
@@ -0,0 +1,61 @@
<script lang="ts">
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import InputPoint from './inputPoint.svelte';
import Button from './button.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Snippet } from 'svelte';
type Props = {
values: number[][];
nullable?: boolean;
minDeletableIndex?: number;
allowLineDelete?: boolean;
onAddPoint: (index: number) => void;
onDeletePoint: (index: number) => void;
onChangePoint: (pointIndex: number, coordIndex: number, newValue: number) => void;
addLineButton?: Snippet;
};
let {
values,
nullable = false,
minDeletableIndex = 2,
allowLineDelete,
onAddPoint,
onDeletePoint,
onChangePoint,
addLineButton
}: Props = $props();
function isDeleteDisabled(index: number) {
let disable = index < minDeletableIndex;
if (allowLineDelete !== undefined) {
disable = disable && allowLineDelete;
}
return disable;
}
</script>
<Layout.Stack alignItems="flex-start" gap="xs">
<Layout.Stack>
{#each values as value, index}
<InputPoint
{nullable}
values={value}
deletePoints
disableDelete={isDeleteDisabled(index)}
onDeletePoint={() => onDeletePoint(index)}
onChangePoint={(coordIndex, newValue) =>
onChangePoint?.(index, coordIndex, newValue)} />
{/each}
</Layout.Stack>
{#if values}
<Layout.Stack direction="row" gap="s" alignItems="center">
<Button size="xs" compact on:click={() => onAddPoint(-1)} disabled={nullable}>
<Icon icon={IconPlus} size="s" /> Add coordinate
</Button>
{@render addLineButton?.()}
</Layout.Stack>
{/if}
</Layout.Stack>
+54
View File
@@ -0,0 +1,54 @@
<script lang="ts">
import { Input } from '@appwrite.io/pink-svelte';
import { Layout, Icon } from '@appwrite.io/pink-svelte';
import Button from './button.svelte';
import { IconX } from '@appwrite.io/pink-icons-svelte';
interface Props {
values: number[];
nullable?: boolean;
deletePoints?: boolean;
onDeletePoint?: () => void;
disableDelete?: boolean;
onChangePoint: (index: number, newValue: number) => void;
}
const nullableSkeletonShape: number[] = [0, 0];
let {
values,
nullable = false,
deletePoints = false,
disableDelete = false,
onDeletePoint,
onChangePoint
}: Props = $props();
</script>
<Layout.Stack>
<Layout.Stack direction="row" gap="m">
{#if nullable}
{#each nullableSkeletonShape as _, index}
<Input.Number id={`default-${index}`} placeholder="0" disabled />
{/each}
{:else}
{#each values as _, index}
<Input.Number
id={`point-${index}`}
placeholder="Enter value"
step={0.0001}
value={values[index]}
on:change={(e) => onChangePoint(index, Number.parseFloat(`${e.detail}`))} />
{/each}
{/if}
{#if deletePoints}
<Button
size="s"
secondary
disabled={nullable || disableDelete}
on:click={() => onDeletePoint?.()}>
<Icon icon={IconX} size="s" />
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
@@ -0,0 +1,59 @@
<script lang="ts">
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import Button from './button.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import InputLine from './inputLine.svelte';
type Props = {
values: number[][][];
nullable?: boolean;
minDeletableIndex?: number;
onAddPoint: (index: number) => void;
onAddLine?: (index: number) => void;
onDeletePoint: (index: number) => void;
onChangePoint: (
lineIndex: number,
pointIndex: number,
coordIndex: number,
newValue: number
) => void;
};
let {
values,
nullable = false,
minDeletableIndex = 4,
onAddPoint,
onAddLine,
onDeletePoint,
onChangePoint
}: Props = $props();
</script>
<Layout.Stack gap="s">
{#each values as value, index}
<Layout.Stack gap="xs">
<InputLine
values={value}
onAddPoint={() => onAddPoint(index)}
{nullable}
onDeletePoint={() => onDeletePoint(index)}
onChangePoint={(pointIndex, coordIndex, newValue) =>
onChangePoint(index, pointIndex, coordIndex, newValue)}
allowLineDelete={index < 2}
{minDeletableIndex}>
{#snippet addLineButton()}
{#if index === values.length - 1}
<Button
disabled={nullable}
size="xs"
compact
on:click={() => onAddLine?.(-1)}>
<Icon icon={IconPlus} size="s" /> Add line
</Button>
{/if}
{/snippet}
</InputLine>
</Layout.Stack>
{/each}
</Layout.Stack>
+53 -6
View File
@@ -3,6 +3,7 @@ import type { Columns } from '$routes/(console)/project-[region]-[project]/datab
import { ID, type Models } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { isWithinSafeRange } from '$lib/helpers/numbers';
import type { NestedNumberArray } from './types';
export async function generateColumns(
project: Models.Project,
@@ -42,7 +43,24 @@ export async function generateColumns(
size: 1000,
required: false
}),
client.tablesDB.createBooleanColumn({ databaseId, tableId, key: 'active', required: false })
client.tablesDB.createBooleanColumn({
databaseId,
tableId,
key: 'active',
required: false
}),
client.tablesDB.createPointColumn({
databaseId,
tableId,
key: 'location',
required: false
}),
client.tablesDB.createLineColumn({
databaseId,
tableId,
key: 'route',
required: false
})
]);
}
@@ -66,7 +84,10 @@ export function generateFakeRecords(
const id = ID.unique();
ids.push(id);
let row: Record<string, string | number | boolean | Array<string | number | boolean>> = {
let row: Record<
string,
string | number | boolean | Array<string | number | boolean | NestedNumberArray>
> = {
$id: id
};
@@ -78,7 +99,12 @@ export function generateFakeRecords(
age: faker.number.int({ min: 18, max: 80 }),
city: faker.location.city(),
description: faker.lorem.sentence(),
active: faker.datatype.boolean()
active: faker.datatype.boolean(),
location: [faker.location.longitude(), faker.location.latitude()],
route: Array.from({ length: 5 }, () => [
faker.location.longitude(),
faker.location.latitude()
])
};
} else {
for (const column of filteredColumns) {
@@ -116,10 +142,10 @@ function generateStringValue(key: string, maxLength: number): string {
function generateValueForColumn(
column: Columns
): string | number | boolean | null | Array<string | number | boolean> {
): string | number | boolean | null | Array<string | number | boolean | NestedNumberArray> {
if (column.array) {
const arraySize = faker.number.int({ min: 1, max: 5 });
const items: Array<string | number | boolean> = [];
const items: Array<string | number | NestedNumberArray | boolean> = [];
for (let i = 0; i < arraySize; i++) {
const itemAttribute = { ...column, array: false };
@@ -135,7 +161,9 @@ function generateValueForColumn(
return generateSingleValue(column);
}
function generateSingleValue(column: Columns): string | number | boolean | null {
function generateSingleValue(
column: Columns
): string | number | boolean | NestedNumberArray | null {
switch (column.type) {
case 'string': {
if ('format' in column && column.format) {
@@ -192,6 +220,25 @@ function generateSingleValue(column: Columns): string | number | boolean | null
return faker.date.recent({ days: 365 }).toISOString();
}
case 'point': {
return [faker.location.longitude(), faker.location.latitude()];
}
case 'linestring': {
return Array.from({ length: 5 }, () => [
faker.location.longitude(),
faker.location.latitude()
]);
}
case 'polygon': {
const points = Array.from({ length: 5 }, () => [
faker.location.longitude(),
faker.location.latitude()
]);
points.push(points[0]); // close polygon
return [points];
}
default: {
return faker.lorem.word();
}
+6 -1
View File
@@ -35,7 +35,10 @@ const columnTypes = [
'boolean',
'datetime',
'relationship',
'enum'
'enum',
'point',
'linestring',
'polygon'
] as const;
export type ColumnType = (typeof columnTypes)[number];
export type Column = PinkColumn & {
@@ -77,3 +80,5 @@ export type TableRootProp = {
addAvailableId: (id: string) => void;
removeAvailableId: (id: string) => void;
};
export type NestedNumberArray = Array<number | NestedNumberArray>;
+37
View File
@@ -1,3 +1,17 @@
const LINKEDIN_TRACKING_ID = 'li_fat_id';
const GOOGLE_TRACKING_ID = 'gclid';
const TWITTER_TRACKING_ID = 'twclid';
const REDDIT_TRACKING_ID = 'rdt_cid';
const TRACKED_QUERY_PARAMS = [
LINKEDIN_TRACKING_ID,
GOOGLE_TRACKING_ID,
TWITTER_TRACKING_ID,
REDDIT_TRACKING_ID
] as const;
type TrackedParams = Record<(typeof TRACKED_QUERY_PARAMS)[number], string>;
export function getReferrerAndUtmSource() {
if (sessionStorage) {
let values = {};
@@ -15,3 +29,26 @@ export function getReferrerAndUtmSource() {
}
return {};
}
export function getTrackedQueryParams(): Partial<TrackedParams> {
if (typeof window === 'undefined' || !window.location) {
return {};
}
try {
const urlParams = new URLSearchParams(window.location.search);
const params: Partial<TrackedParams> = {};
for (const paramName of TRACKED_QUERY_PARAMS) {
const value = urlParams.get(paramName);
if (value) {
params[paramName] = value;
}
}
return params;
} catch (error) {
console.warn('Failed to parse URL search parameters:', error);
return {};
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+1 -1
View File
@@ -205,7 +205,7 @@
<Navbar {...navbarProps} bind:sideBarIsOpen={$isSidebarOpen} bind:showAccountMenu />
{/if}
{#if !$isNewWizardStatusOpen}
{#if !$isNewWizardStatusOpen && isProjectPage}
<Sidebar
project={selectedProject}
progressCard={getProgressCard()}
+1 -1
View File
@@ -109,7 +109,7 @@
<Layout.Stack gap="s" direction="row">
{#if currentReview?.image}
<Avatar
src={getCampaignImageUrl(currentReview?.image)}
src={currentReview.image}
alt={currentReview.name}
size="m" />
{:else}
+1 -1
View File
@@ -620,7 +620,7 @@ export const billingURL = derived(
($page) => `${base}/organization-${$page.data?.organization?.$id}/billing`
);
export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account'];
export const hideBillingHeaderRoutes = [base + '/create-organization', base + '/account'];
export function calculateExcess(addon: AggregationTeam, plan: Plan) {
return {
+9 -9
View File
@@ -83,7 +83,7 @@
keys: ['g', 'p'],
group: 'navigation',
disabled:
(page.url.pathname.includes('/console/organization-') &&
(page.url.pathname.includes(base + '/organization-') &&
!page.url.pathname.endsWith('/members') &&
!page.url.pathname.endsWith('/settings')) ||
!$canSeeProjects,
@@ -336,14 +336,14 @@
<Shell
showSideNavigation={page.url.pathname !== '/' &&
!page?.params.organization &&
!page.url.pathname.includes('/console/account') &&
!page.url.pathname.includes('/console/card') &&
!page.url.pathname.includes('/console/onboarding') &&
!page.url.pathname.includes('/console/verify-email')}
showHeader={!page.url.pathname.includes('/console/onboarding/create-project') &&
!page.url.pathname.includes('/console/verify-email')}
showFooter={!page.url.pathname.includes('/console/onboarding/create-project') &&
!page.url.pathname.includes('/console/verify-email')}
!page.url.pathname.includes(base + '/account') &&
!page.url.pathname.includes(base + '/card') &&
!page.url.pathname.includes(base + '/onboarding') &&
!page.url.pathname.includes(base + '/verify-email')}
showHeader={!page.url.pathname.includes(base + '/onboarding/create-project') &&
!page.url.pathname.includes(base + '/verify-email')}
showFooter={!page.url.pathname.includes(base + '/onboarding/create-project') &&
!page.url.pathname.includes(base + '/verify-email')}
selectedProject={page.data?.project}>
<!-- <Header slot="header" />-->
<slot />
@@ -174,7 +174,7 @@
'',
clientSecret,
paymentMethodId,
`/console/apply-credit?${params}`
`${base}/apply-credit?${params}`
);
org = await sdk.forConsole.billing.validateOrganization(org.teamId, collaborators);
}
+10 -11
View File
@@ -1,8 +1,8 @@
import { isCloud } from '$lib/system';
import { isSameDay } from '$lib/helpers/date';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
import TablesApiLight from '$lib/images/promos/tables-api-light.png';
import TablesApiDark from '$lib/images/promos/tables-api-dark.png';
import SpatialColumnsLight from '$lib/images/promos/spatial-columns-api-light.png';
import SpatialColumnsDark from '$lib/images/promos/spatial-columns-api-dark.png';
import InversionQueriesDark from '$lib/images/promos/inversion-queries-dark.png';
import InversionQueriesLight from '$lib/images/promos/inversion-queries-light.png';
import TimeHelperQueriesDark from '$lib/images/promos/time-helper-queries-dark.png';
@@ -11,21 +11,20 @@ import TimeHelperQueriesLight from '$lib/images/promos/time-helper-queries-light
const listOfPromotions: BottomModalAlertItem[] = [];
if (isCloud) {
const tablesApiPromo: BottomModalAlertItem = {
id: 'modal:tables_api_announcement',
const spatialColumnsPromo: BottomModalAlertItem = {
id: 'modal:spatial_columns_announcement',
src: {
dark: TablesApiDark,
light: TablesApiLight
dark: SpatialColumnsDark,
light: SpatialColumnsLight
},
title: 'Announcing the new TablesDB UI and more!',
message:
'This is Appwrite Databases most significant update so far, introducing new terminology, a new UI, and a supporting TablesDB API.',
title: 'Announcing API for spatial columns',
message: 'Store and query geo data directly in your database.',
plan: 'free',
importance: 8,
scope: 'project',
cta: {
text: 'Read announcement',
link: () => 'https://apwr.dev/E64A90f',
link: () => 'https://appwrite.io/blog/post/announcing-spatial-columns',
external: true,
hideOnClick: true
},
@@ -71,7 +70,7 @@ if (isCloud) {
},
show: true
};
listOfPromotions.push(inversionQueriesPromo, timeHelperQueriesPromo, tablesApiPromo);
listOfPromotions.push(spatialColumnsPromo, inversionQueriesPromo, timeHelperQueriesPromo);
}
export function addBottomModalAlerts() {
@@ -143,7 +143,7 @@
'',
clientSecret,
paymentMethodId,
`/console/create-organization?${params}`
`${base}/create-organization?${params}`
);
await validate(org.teamId, collaborators);
}
@@ -82,13 +82,13 @@
<Loading {startAnimation} />
{:else}
<img
src="/console/images/appwrite-logo-light.svg"
src="{base}/images/appwrite-logo-light.svg"
width="120"
height="22"
class="u-only-light"
alt="Appwrite Logo" />
<img
src="/console/images/appwrite-logo-dark.svg"
src="{base}/images/appwrite-logo-dark.svg"
width="120"
height="22"
class="u-only-dark"
@@ -4,11 +4,12 @@ import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
export const load: PageLoad = async ({ params, url, route, depends, parent }) => {
const { scopes } = await parent();
if (!scopes.includes('projects.read') && scopes.includes('billing.read')) {
return redirect(301, `/console/organization-${params.organization}/billing`);
return redirect(301, `${base}/organization-${params.organization}/billing`);
}
depends(Dependencies.ORGANIZATION);
@@ -5,12 +5,13 @@ import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { isCloud } from '$lib/system';
import { base } from '$app/paths';
export const load: PageLoad = async ({ parent, depends }) => {
const { organization, scopes, currentPlan, countryList, locale } = await parent();
if (!scopes.includes('billing.read')) {
return redirect(301, `/console/organization-${organization.$id}`);
return redirect(301, `${base}/organization-${organization.$id}`);
}
depends(Dependencies.PAYMENT_METHODS);
@@ -146,7 +146,7 @@
function getBillingData(currentPlan, currentAggregation, isSmallViewport) {
const projectsList = getProjectsList(currentAggregation);
const base = {
const basePlan = {
id: 'base-plan',
expandable: false,
cells: {
@@ -303,7 +303,7 @@
{
id: `usage-details`,
cells: {
item: `<a href="/console/project-${String(project.region || 'default')}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`,
item: `<a href="${base}/project-${String(project.region || 'default')}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`,
usage: '',
price: ''
}
@@ -311,7 +311,7 @@
]
}));
const noProjects = [];
return [base, ...addons, ...projects, ...noProjects];
return [basePlan, ...addons, ...projects, ...noProjects];
}
$: billingData = getBillingData(currentPlan, currentAggregation, $isSmallViewport);
@@ -277,7 +277,7 @@
'',
clientSecret,
paymentMethodId,
'/console/change-plan?' + params.toString()
base + '/change-plan?' + params.toString()
);
await validate(org.teamId, collaborators);
}
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
@@ -20,7 +21,7 @@
type: 'success',
message: 'Domain moved successfully'
});
await goto(`/console/organization-${selectedOrg}/domains/`);
await goto(`${base}/organization-${selectedOrg}/domains/`);
await invalidate(Dependencies.ORGANIZATION);
await invalidate(Dependencies.DOMAINS);
} catch (e) {
@@ -15,7 +15,7 @@
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import { isRelationship, isString } from '../rows/store';
import { isRelationship, isSpatialType, isString } from '../rows/store';
import FailedModal from '../failedModal.svelte';
import {
columns,
@@ -329,7 +329,7 @@
{#if _default === null}
<Badge variant="secondary" content="NULL" size="xs" />
{:else}
{_default}
{isSpatialType(column) ? JSON.stringify(_default) : _default}
{/if}
</Spreadsheet.Cell>
<Spreadsheet.Cell column="actions" {root} isEditable={false}>
@@ -0,0 +1,147 @@
<script module lang="ts">
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
export async function submitLine(
databaseId: string,
tableId: string,
key: string,
data: Partial<Models.ColumnLine>
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createLineColumn({
databaseId,
tableId,
key,
required: data.required,
xdefault: data?.default
});
}
export async function updateLine(
databaseId: string,
tableId: string,
data: Partial<Models.ColumnLine>,
originalKey?: string
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.updateLineColumn({
databaseId,
tableId,
key: originalKey,
required: data.required,
xdefault: data?.default,
newKey: data.key !== originalKey ? data.key : undefined
});
}
</script>
<script lang="ts">
import { createConservative } from '$lib/helpers/stores';
import { Selector, Layout, Typography } from '@appwrite.io/pink-svelte';
import { InputLine } from '$lib/elements/forms';
import { getDefaultSpatialData } from '../store';
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnLine>;
editing?: boolean;
}
let { data = { required: false, default: null }, editing = false }: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
data.default = null;
} else {
data.default = savedDefault ?? getDefaultSpatialData('linestring');
}
}
function pushCoordinate() {
if (data.default) {
data.default.push(getDefaultSpatialData('point') as number[]);
data.default = [...data.default];
}
}
function deleteCoordinate(_: number) {
if (data.default && data.default.length > 2) {
data.default.pop();
data.default = [...data.default];
}
}
const {
stores: { required },
listen
} = createConservative<Partial<Models.ColumnLine>>({
required: false,
...data
});
$effect(() => {
listen(data);
});
$effect(() => {
data.required = $required;
if ($required) {
handleDefaultState(true);
}
});
onMount(() => {
if (!editing) {
savedDefault = getDefaultSpatialData('linestring');
defaultChecked = false;
$required = false;
}
});
</script>
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
}}
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="default"
label="Default value"
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
$required = false;
handleDefaultState(false);
} else {
data.default = null;
}
}}
description="Enable to set a predefined value for this column" />
<Layout.Stack>
{#if defaultChecked}
<Layout.Stack direction="row" alignItems="center">
<Typography.Text variant="m-600">Default</Typography.Text>
<Typography.Caption variant="400">Optional</Typography.Caption>
</Layout.Stack>
{/if}
<InputLine
values={defaultChecked ? data.default : null}
onAddPoint={() => pushCoordinate()}
onDeletePoint={deleteCoordinate}
onChangePoint={(pointIndex: number, coordIndex: number, newValue: number) => {
if (data.default) {
data.default[pointIndex][coordIndex] = newValue;
data.default = [...data.default];
}
}} />
</Layout.Stack>
@@ -0,0 +1,136 @@
<script module lang="ts">
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
export async function submitPoint(
databaseId: string,
tableId: string,
key: string,
data: Partial<Models.ColumnPoint>
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createPointColumn({
databaseId,
tableId,
key,
required: data.required,
xdefault: data?.default
});
}
export async function updatePoint(
databaseId: string,
tableId: string,
data: Partial<Models.ColumnPoint>,
originalKey?: string
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.updatePointColumn({
databaseId,
tableId,
key: originalKey,
required: data.required,
xdefault: data?.default,
newKey: data.key !== originalKey ? data.key : undefined
});
}
</script>
<script lang="ts">
import { InputPoint } from '$lib/elements/forms';
import { createConservative } from '$lib/helpers/stores';
import { Selector, Typography, Layout } from '@appwrite.io/pink-svelte';
import { getDefaultSpatialData } from '../store';
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnPoint>;
editing?: boolean;
}
let { data = { required: false, default: null }, editing }: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
data.default = null;
} else {
data.default = savedDefault ?? getDefaultSpatialData('point');
}
}
const {
stores: { required },
listen
} = createConservative<Partial<Models.ColumnPoint>>({
required: false,
...data
});
$effect(() => {
listen(data);
});
$effect(() => {
data.required = $required;
if ($required) {
handleDefaultState(true);
}
});
$effect(() => {
defaultChecked = data.default !== null;
});
onMount(() => {
if (!editing) {
savedDefault = getDefaultSpatialData('point');
defaultChecked = false;
$required = false;
}
});
</script>
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
}}
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="default"
label="Default value"
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
$required = false;
handleDefaultState(false);
} else {
data.default = null;
}
}}
description="Enable to set a predefined value for this column" />
<Layout.Stack gap="xl">
{#if defaultChecked}
<Layout.Stack direction="row" alignItems="center">
<Typography.Text variant="m-600">Default</Typography.Text>
<Typography.Caption variant="400">Optional</Typography.Caption>
</Layout.Stack>
{/if}
<InputPoint
values={defaultChecked ? data.default : null}
onChangePoint={(index, newValue) => {
if (data.default) {
data.default[index] = newValue;
data.default = [...data.default];
}
}} />
</Layout.Stack>
@@ -0,0 +1,161 @@
<script module lang="ts">
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
export async function submitPolygon(
databaseId: string,
tableId: string,
key: string,
data: Partial<Models.ColumnPolygon>
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createPolygonColumn({
databaseId,
tableId,
key,
required: data.required,
xdefault: data?.default
});
}
export async function updatePolygon(
databaseId: string,
tableId: string,
data: Partial<Models.ColumnPolygon>,
originalKey?: string
) {
await sdk.forProject(page.params.region, page.params.project).tablesDB.updatePolygonColumn({
databaseId,
tableId,
key: originalKey,
required: data.required,
xdefault: data?.default,
newKey: data.key !== originalKey ? data.key : undefined
});
}
</script>
<script lang="ts">
import { createConservative } from '$lib/helpers/stores';
import { Selector, Typography, Layout } from '@appwrite.io/pink-svelte';
import { InputPolygon } from '$lib/elements/forms';
import { getDefaultSpatialData, getSingleRingPolygon } from '../store';
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnPolygon>;
editing?: boolean;
}
let { data = { required: false, default: null }, editing = false }: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
data.default = null;
} else {
data.default = savedDefault ?? getDefaultSpatialData('polygon');
}
}
const {
stores: { required },
listen
} = createConservative<Partial<Models.ColumnPolygon>>({
required: false,
...data
});
function pushCoordinate(ringIndex: number) {
const ring = data.default?.at(ringIndex);
if (!ring) return;
const newPoint = getDefaultSpatialData('point') as number[];
ring.splice(ring.length - 1, 0, newPoint);
ring[ring.length - 1] = [...ring[0]];
data.default = [...(data.default || [])];
}
function pushLine() {
data.default = [...(data.default || []), getSingleRingPolygon()];
}
function deleteCoordinate(ringIndex: number) {
const ring = data.default?.at(ringIndex);
ring.splice(ring.length - 1, 1);
if (ring.length === 0) {
data.default.splice(ringIndex, 1);
}
data.default = [...(data.default || [])];
}
$effect(() => {
listen(data);
});
$effect(() => {
data.required = $required;
if ($required) {
handleDefaultState(true);
}
});
onMount(() => {
if (!editing) {
savedDefault = getDefaultSpatialData('polygon');
defaultChecked = false;
$required = false;
}
});
</script>
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
else data.default = null;
}}
description="Indicate whether this column is required" />
<Selector.Checkbox
size="s"
id="default"
label="Default value"
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
$required = false;
handleDefaultState(false);
} else {
data.default = null;
}
}}
description="Enable to set a predefined value for this column" />
<Layout.Stack gap="xl">
{#if defaultChecked}
<Layout.Stack direction="row" alignItems="center">
<Typography.Text variant="m-600">Default</Typography.Text>
<Typography.Caption variant="400">Optional</Typography.Caption>
</Layout.Stack>
{/if}
<InputPolygon
values={defaultChecked ? data.default : null}
onAddLine={pushLine}
onAddPoint={pushCoordinate}
onDeletePoint={deleteCoordinate}
onChangePoint={(lineIndex, pointIndex, coordIndex, newValue) => {
if (data.default && data.default[lineIndex] && data.default[lineIndex][pointIndex]) {
data.default[lineIndex][pointIndex][coordIndex] = newValue;
data.default = [...data.default];
}
}} />
</Layout.Stack>
@@ -138,7 +138,7 @@
$: search = data.relatedTable || undefined;
$: if (search) {
$: if (search && !editing) {
const exists = tableList?.tables?.some((c) =>
c.$id.toLocaleLowerCase().includes(search.toLocaleLowerCase())
);
@@ -191,6 +191,7 @@
bind:value={data.key}
helper="Allowed characters: a-z, A-Z, 0-9, -, ."
required />
{#if way === 'two'}
<InputText
id="keyRelated"
@@ -9,6 +9,9 @@ import Ip, { submitIp, updateIp } from './ip.svelte';
import String, { submitString, updateString } from './string.svelte';
import Url, { submitUrl, updateUrl } from './url.svelte';
import Datetime, { submitDatetime, updateDatetime } from './datetime.svelte';
import Point, { submitPoint, updatePoint } from './point.svelte';
import Line, { submitLine, updateLine } from './line.svelte';
import Polygon, { submitPolygon, updatePolygon } from './polygon.svelte';
import type { Columns } from '../store';
import Relationship, { submitRelationship, updateRelationship } from './relationship.svelte';
import {
@@ -20,7 +23,10 @@ import {
IconMail,
IconRelationship,
IconText,
IconToggle
IconToggle,
IconPoint,
IconLine,
IconPolygon
} from '@appwrite.io/pink-icons-svelte';
import type { ComponentType } from 'svelte';
@@ -35,10 +41,22 @@ export type Option = {
| 'IP'
| 'URL'
| 'Enum'
| 'Relationship';
| 'Relationship'
| 'Point'
| 'Line'
| 'Polygon';
sentenceName: string;
component: Component;
type: 'string' | 'integer' | 'double' | 'boolean' | 'datetime' | 'relationship';
type:
| 'string'
| 'integer'
| 'double'
| 'boolean'
| 'datetime'
| 'relationship'
| 'point'
| 'linestring'
| 'polygon';
create: (
databaseId: string,
tableId: string,
@@ -101,6 +119,33 @@ export const columnOptions: Option[] = [
update: updateDatetime,
icon: IconCalendar
},
{
name: 'Point',
sentenceName: 'point',
component: Point,
type: 'point',
create: submitPoint,
update: updatePoint,
icon: IconPoint
},
{
name: 'Line',
sentenceName: 'linestring',
component: Line,
type: 'linestring',
create: submitLine,
update: updateLine,
icon: IconLine
},
{
name: 'Polygon',
sentenceName: 'polygon',
component: Polygon,
type: 'polygon',
create: submitPolygon,
update: updatePolygon,
icon: IconPolygon
},
{
name: 'Email',
sentenceName: 'email',
@@ -9,7 +9,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { IndexType } from '@appwrite.io/console';
import { isRelationship } from '../rows/store';
import { isRelationship, isSpatialType } from '../rows/store';
import { table, indexes } from '../store';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
@@ -26,23 +26,51 @@
const databaseId = page.params.database;
let key = $state('');
let types = [
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
{ value: IndexType.Fulltext, label: 'Fulltext' }
];
let selectedType = $state(IndexType.Key);
let selectedType = $state<IndexType>(IndexType.Key);
let columnOptions = $table.columns
.filter((column) => !isRelationship(column))
.map((column) => ({
value: column.key,
label: column.key
}));
let columnOptions = $derived(
$table.columns
.filter((column) => {
if (selectedType === IndexType.Spatial) {
return isSpatialType(column); // keep only spatial
}
return !isRelationship(column) && !isSpatialType(column); // keep non-relationship and non-spatial
})
.map((column) => ({ value: column.key, label: column.key }))
);
let columnList = $state([{ value: '', order: '', length: null }]);
const types = [
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
{ value: IndexType.Fulltext, label: 'Fulltext' },
{ value: IndexType.Spatial, label: 'Spatial' }
];
// order options derived from selected type
let orderOptions = $derived.by(() =>
selectedType === IndexType.Spatial
? [
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' },
{ value: null, label: 'NONE' }
]
: [
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' }
]
);
// spatial type selected -> reset column list to single empty column
// and the column already is not spatial type
$effect(() => {
if (selectedType === IndexType.Spatial && !columnList.at(0).value) {
columnList = [{ value: '', order: null, length: null }];
}
});
function generateIndexKey() {
let indexKeys = $indexes.map((index) => index.key);
@@ -55,29 +83,41 @@
}
function initialize() {
const column = $table.columns.filter((column) => externalColumnKey === column.key);
const isSpatial = column.length && isSpatialType(column[0]);
const order = isSpatial ? null : 'ASC';
selectedType = isSpatial ? IndexType.Spatial : IndexType.Key;
columnList = externalColumnKey
? [{ value: externalColumnKey, order: 'ASC', length: null }]
: [{ value: '', order: 'ASC', length: null }];
selectedType = IndexType.Key;
? [{ value: externalColumnKey, order, length: null }]
: [{ value: '', order, length: null }];
key = `index_${$indexes.length + 1}`;
}
const addColumnDisabled = $derived(!columnList.at(-1)?.value || !columnList.at(-1)?.order);
const addColumnDisabled = $derived(
selectedType === IndexType.Spatial ||
!columnList.at(-1)?.value ||
(!columnList.at(-1)?.order && columnList.at(-1)?.order !== null)
);
const isOnIndexesPage = $derived(page.route.id?.endsWith('/indexes'));
const navigatorPathToIndexes = $derived(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${$table?.$id}/indexes`
);
let initializedForOpen = $state(false);
$effect(() => {
if (showCreateIndex) {
if (showCreateIndex && !initializedForOpen) {
initialize();
key = generateIndexKey();
initializedForOpen = true;
}
if (!showCreateIndex && initializedForOpen) {
initializedForOpen = false;
}
});
export async function create() {
if (!(key && selectedType && !addColumnDisabled)) {
if (!key || !selectedType || (selectedType !== IndexType.Spatial && addColumnDisabled)) {
addNotification({
type: 'error',
message: 'Selected column key or type invalid'
@@ -86,14 +126,15 @@
}
try {
const orders = columnList.map((a) => a.order).filter((order) => order !== null);
await sdk.forProject(page.params.region, page.params.project).tablesDB.createIndex({
databaseId,
tableId: $table.$id,
key,
type: selectedType,
columns: columnList.map((a) => a.value),
orders: columnList.map((a) => a.order),
lengths: columnList.map((a) => (a.length ? Number(a.length) : null))
lengths: columnList.map((a) => (a.length ? Number(a.length) : null)),
...(orders.length ? { orders } : {})
});
await Promise.allSettled([
@@ -151,9 +192,14 @@
<InputSelect
required
options={[
{ value: '$id', label: '$id' },
{ value: '$createdAt', label: '$createdAt' },
{ value: '$updatedAt', label: '$updatedAt' },
// allow system fields only for non-spatial index types
...(selectedType === IndexType.Spatial
? []
: [
{ value: '$id', label: '$id' },
{ value: '$createdAt', label: '$createdAt' },
{ value: '$updatedAt', label: '$updatedAt' }
]),
...columnOptions
]}
id={`column-${index}`}
@@ -162,10 +208,7 @@
bind:value={column.value} />
<InputSelect
options={[
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' }
]}
options={orderOptions}
required
id={`order-${index}`}
label={index === 0 ? 'Order' : undefined}
@@ -8,7 +8,7 @@
Typography
} from '@appwrite.io/pink-svelte';
import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
import { SortButton } from '$lib/components';
import type { Column } from '$lib/helpers/types';
import {
@@ -156,7 +156,9 @@
const spreadsheetColumns = $derived(mode === 'rows' ? getRowColumns() : getIndexesColumns());
const emptyCells = $derived(($isSmallViewport ? 14 : 17) + (!$expandTabs ? 2 : 0));
const emptyCells = $derived(
($isSmallViewport ? 14 : $isTabletViewport ? 17 : 24) + (!$expandTabs ? 2 : 0)
);
</script>
<div
@@ -7,6 +7,10 @@
import Relationship from './types/relationship.svelte';
import String from './types/string.svelte';
import Url from './types/url.svelte';
import Point from './types/point.svelte';
import Line from './types/line.svelte';
import Polygon from './types/polygon.svelte';
import { isSpatialType } from '../store';
export let id: string;
export let label: string;
@@ -24,7 +28,10 @@
| Models.ColumnIp
| Models.ColumnString
| Models.ColumnDatetime
| Models.ColumnUrl;
| Models.ColumnUrl
| Models.ColumnPoint
| Models.ColumnLine
| Models.ColumnPolygon;
const columnsTypeMap = {
string: String,
@@ -32,7 +39,10 @@
double: Integer,
boolean: Boolean,
datetime: Datetime,
relationship: Relationship
relationship: Relationship,
point: Point,
linestring: Line,
polygon: Polygon
};
const columnsFormatMap = {
@@ -59,7 +69,9 @@
{:else}
<!-- the `on:click` is from string > array mode for advanced edit button -->
<svelte:component
this={column.array ? columnsTypeMap['string'] : columnsTypeMap[column.type]}
this={column.array || (isSpatialType(column) && limited)
? columnsTypeMap['string']
: columnsTypeMap[column.type]}
{id}
{editing}
{limited}
@@ -5,11 +5,17 @@
import ColumnItem from './columnItem.svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
export let columns: Columns[] = [];
export let formValues: object = {};
export let customId: string | null | undefined = undefined;
let {
columns = [],
formValues = $bindable({}),
customId = $bindable(undefined)
}: {
columns: Columns[];
formValues: object;
customId: string | null | undefined;
} = $props();
let showCustomId = false;
let showCustomId = $state(false);
</script>
{#if columns.length}
@@ -28,22 +28,18 @@
function removeArrayItem(key: string, index: number) {
const currentArray = Array.isArray($formStore[key]) ? $formStore[key] : [];
const filteredArray = currentArray.filter((_: object, i: number) => i !== index);
const next = {
...formValues,
[key]: filteredArray.length === 0 ? [] : filteredArray
};
formStore.set(next);
formStore.update((values) => {
values[key] = filteredArray.length === 0 ? [] : filteredArray;
return values;
});
}
function addArrayItem(key: string) {
const currentArray = Array.isArray($formStore[key]) ? $formStore[key] : null;
const next = {
...formValues,
[key]: currentArray ? [...currentArray, null] : [null]
};
formStore.set(next);
formStore.update((values) => {
values[key] = currentArray ? [...currentArray, null] : [null];
return values;
});
}
function getColumnType(column: Columns) {
@@ -58,8 +54,7 @@
case 'enum':
return 'Enum';
default:
'String';
break;
return 'String';
}
}
return `${capitalize(column.type)}${column.array ? '[]' : ''}`;
@@ -0,0 +1,67 @@
<script lang="ts">
import { InputLine } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { getDefaultSpatialData } from '../../../store';
interface Props {
label: string;
value: number[][];
limited: boolean;
column: Models.ColumnLine;
}
let { label, value = $bindable(), limited = false, column }: Props = $props();
const defaultData = getDefaultSpatialData('linestring') as number[][];
function onAddPoint() {
value = [...(value || defaultData), getDefaultSpatialData('point') as number[]];
}
function onDeletePoint() {
if (value && value?.length > 2) value = value.slice(0, value.length - 1);
}
function handlePointChange(pointIndex: number, coordIndex: number, newValue: number) {
if (value && value[pointIndex]) {
value[pointIndex][coordIndex] = newValue;
value = [...value];
}
}
function handleAddDefault() {
value = getDefaultSpatialData('linestring') as number[][];
}
const nullable = $derived(!limited ? !column.required : false);
</script>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{#if nullable}
optional
{:else}
Line
{/if}
</Typography.Text>
</Layout.Stack>
{#if !value}
<Button secondary on:click={handleAddDefault}>
<Icon icon={IconPlus} slot="start" size="s" />
Add Line
</Button>
{/if}
</Layout.Stack>
<InputLine
values={value}
nullable={false}
{onAddPoint}
{onDeletePoint}
onChangePoint={handlePointChange}
minDeletableIndex={2} />
</Layout.Stack>
@@ -0,0 +1,53 @@
<script lang="ts">
import { InputPoint } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { getDefaultSpatialData } from '../../../store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
interface Props {
label: string;
value: number[];
limited: boolean;
column: Models.ColumnPoint;
}
let { label, value = $bindable(), limited = false, column }: Props = $props();
function handlePointChange(index: number, newValue: number) {
if (value) {
value[index] = newValue;
value = [...value];
}
}
function handleAddDefault() {
value = getDefaultSpatialData('point') as number[];
}
const nullable = $derived(!limited ? !column.required : false);
</script>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{#if nullable}
optional
{:else}
Point
{/if}
</Typography.Text>
</Layout.Stack>
{#if !value}
<Button secondary on:click={handleAddDefault}>
<Icon icon={IconPlus} slot="start" size="s" />
Add Point
</Button>
{/if}
</Layout.Stack>
<InputPoint values={value} nullable={false} onChangePoint={handlePointChange} />
</Layout.Stack>
@@ -0,0 +1,89 @@
<script lang="ts">
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte';
import { getDefaultSpatialData, getSingleRingPolygon } from '../../../store';
import InputPolygon from '$lib/elements/forms/inputPolygon.svelte';
interface Props {
label: string;
value: number[][][];
limited: boolean;
column: Models.ColumnPolygon;
}
let { label, value = $bindable(), limited = false, column }: Props = $props();
function pushCoordinate(ringIndex: number) {
const ring = value[ringIndex];
if (!ring) return;
const newPoint = getDefaultSpatialData('point') as number[];
const newRing = [...ring];
newRing.splice(newRing.length - 1, 0, newPoint);
newRing[newRing.length - 1] = [...newRing[0]];
value = value.map((r, i) => (i === ringIndex ? newRing : r));
}
function pushLine() {
if (!value) return;
value = [...value, getSingleRingPolygon()];
}
function deleteCoordinate(ringIndex: number) {
if (!value) return;
value = value
.map((ring, i) => (i === ringIndex ? ring.slice(0, -1) : ring))
.filter((ring) => ring.length > 0);
}
function handlePointChange(
lineIndex: number,
pointIndex: number,
coordIndex: number,
newValue: number
) {
if (value && value[lineIndex] && value[lineIndex][pointIndex]) {
value[lineIndex][pointIndex][coordIndex] = newValue;
value = [...value];
}
}
function handleAddDefault() {
value = getDefaultSpatialData('polygon') as number[][][];
}
const nullable = $derived(!limited ? !column.required : false);
</script>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{#if nullable}
optional
{:else}
Polygon
{/if}
</Typography.Text>
</Layout.Stack>
{#if !value}
<Button secondary on:click={handleAddDefault}>
<Icon icon={IconPlus} slot="start" size="s" />
Add Polygon
</Button>
{/if}
</Layout.Stack>
<InputPolygon
values={value}
nullable={false}
onAddLine={pushLine}
onAddPoint={pushCoordinate}
onDeletePoint={deleteCoordinate}
onChangePoint={handlePointChange} />
</Layout.Stack>
@@ -69,7 +69,8 @@
} else {
if (value && typeof value === 'object') {
row = value as Models.Row;
singleRel = row?.$id;
// set for combobox and select.
newItemValue = singleRel = row?.$id;
}
}
});
@@ -86,7 +87,7 @@
databaseId,
tableId: column.relatedTable,
// limit `5` as `25` would look too much on sheet!
queries: [Query.select(displayNames), Query.limit(2)]
queries: [Query.select(displayNames), Query.limit(5)]
});
cachedRowsCopyList = rows;
@@ -111,7 +112,8 @@
}
function getAvailableOptions(excludeIndex?: number): SelectOption[] {
return options?.filter((option) => {
const source = options ?? [];
return source?.filter((option) => {
const otherItems =
excludeIndex !== undefined
? relatedList.filter((_, idx) => idx !== excludeIndex)
@@ -368,23 +370,30 @@
<!-- Input for adding new items -->
{#if showInput}
<Layout.Stack direction="row">
<Input.ComboBox
{id}
required
placeholder={`Select ${column.key}`}
bind:value={newItemValue}
options={getAvailableOptions()}
on:change={addNewItem}
noResultsOption={searchNoResultsOption}
leadingIcon={!limited ? IconRelationship : undefined} />
{@const availableOptions = getAvailableOptions()}
{@const noAvailableOptions = availableOptions.length <= 0}
{#key availableOptions}
<Layout.Stack direction="row">
<Input.ComboBox
{id}
required
on:change={addNewItem}
bind:value={newItemValue}
options={availableOptions}
disabled={noAvailableOptions}
placeholder={noAvailableOptions
? 'No related items available'
: `Select ${column.key}`}
noResultsOption={searchNoResultsOption}
leadingIcon={!limited ? IconRelationship : undefined} />
<div style:padding-block-start="0.5rem">
<Button icon extraCompact on:click={cancelAddItem}>
<Icon icon={IconX} size="s" />
</Button>
</div>
</Layout.Stack>
<div style:padding-block-start="0.5rem">
<Button icon extraCompact on:click={cancelAddItem}>
<Icon icon={IconX} size="s" />
</Button>
</div>
</Layout.Stack>
{/key}
{/if}
</Layout.Stack>
@@ -400,44 +409,77 @@
</Layout.Stack>
{:else}
<Layout.Stack direction="row" alignItems="center" gap="s">
<Input.ComboBox
{id}
{options}
autofocus={limited}
bind:value={newItemValue}
required={column.required}
label={limited ? undefined : label}
placeholder={`Select ${column.key}`}
noResultsOption={searchNoResultsOption}
on:change={() => {
if (newItemValue === null) {
value = null;
singleRel = null;
} else {
const selectedRow = rowList.rows.find((row) => row.$id === newItemValue);
{#key options}
{#if limited}
<!-- for unlink badge -->
<Input.Select
{id}
{options}
autofocus={limited}
bind:value={newItemValue}
required={column.required}
label={limited ? undefined : label}
placeholder={`Select ${column.key}`}
noResultsOption={searchNoResultsOption}
on:change={() => {
if (newItemValue === null) {
value = null;
singleRel = null;
} else {
const selectedRow = rowList.rows.find(
(row) => row.$id === newItemValue
);
if (selectedRow) {
value = selectedRow;
singleRel = newItemValue;
}
}
if (selectedRow) {
value = selectedRow;
singleRel = newItemValue;
}
}
}}
leadingIcon={!limited ? IconRelationship : undefined} />
{:else}
{@const noOptions = options.length <= 0}
<Input.ComboBox
{id}
{options}
autofocus={limited}
bind:value={newItemValue}
required={column.required}
label={limited ? undefined : label}
disabled={noOptions}
placeholder={noOptions ? 'No related items available' : `Select ${column.key}`}
noResultsOption={searchNoResultsOption}
on:change={() => {
if (newItemValue === null) {
value = null;
singleRel = null;
} else {
const selectedRow = rowList.rows.find(
(row) => row.$id === newItemValue
);
newItemValue = null;
}}
leadingIcon={!limited ? IconRelationship : undefined} />
if (selectedRow) {
value = selectedRow;
singleRel = newItemValue;
}
}
}}
leadingIcon={!limited ? IconRelationship : undefined} />
{/if}
{#if !limited && singleRel}
<div style:padding-block-start="2.25rem">
<Button
icon
extraCompact
on:click={() => {
value = null;
singleRel = null;
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
{#if !limited && singleRel}
<div style:padding-block-start="2.25rem">
<Button
icon
extraCompact
on:click={() => {
value = null;
newItemValue = singleRel = null;
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
{/key}
</Layout.Stack>
{/if}
@@ -3,6 +3,7 @@
import { Layout, Link } from '@appwrite.io/pink-svelte';
import { IconText } from '@appwrite.io/pink-icons-svelte';
import { InputText, InputTextarea } from '$lib/elements/forms';
import { isSpatialType } from '../../store';
let {
id,
@@ -21,7 +22,10 @@
| Models.ColumnString
| Models.ColumnInteger
| Models.ColumnFloat
| Models.ColumnBoolean;
| Models.ColumnBoolean
| Models.ColumnPoint
| Models.ColumnLine
| Models.ColumnPolygon;
} = $props();
const autofocus = $derived(limited);
@@ -40,7 +44,6 @@
function parseValue(str: string | null): number | boolean | string | null {
const trimmed = str?.trim() ?? null;
if (!trimmed) return null;
switch (column.type) {
@@ -60,7 +63,10 @@
if (lower === 'false' || lower === '0') return false;
return null;
}
case 'point':
case 'linestring':
case 'polygon':
return trimmed;
case 'string':
default:
return trimmed;
@@ -68,7 +74,9 @@
}
$effect(() => {
if (array && Array.isArray(value)) {
if (isSpatialType(column) && Array.isArray(value)) {
stringValue = JSON.stringify(value);
} else if (array && Array.isArray(value)) {
stringValue = value.map(String).join(', ');
} else if (value !== null && value !== undefined) {
stringValue = String(value);
@@ -83,13 +91,14 @@
.split(',')
.map((item) => parseValue(item))
.filter((item) => item !== null);
if (JSON.stringify(newArray) !== JSON.stringify(value)) {
value = newArray as string[] | number[] | boolean[];
}
} else {
const parsedValue = parseValue(stringValue);
if (parsedValue !== value) {
const parsedValue = isSpatialType(column)
? JSON.parse(stringValue)
: parseValue(stringValue);
if (JSON.stringify(parsedValue) !== JSON.stringify(value)) {
value = parsedValue;
}
}
@@ -124,7 +133,7 @@
};
</script>
{#if columnSize >= 50 || array}
{#if columnSize >= 50 || array || isSpatialType(column)}
<InputTextarea
{id}
{label}
@@ -136,7 +145,7 @@
placeholder={getPlaceholder()}
leadingIcon={!limited ? IconText : undefined}>
<Layout.Stack direction="column" alignItems="flex-start" slot="end">
{#if array}
{#if array || isSpatialType(column)}
<Link.Button on:click size="s" variant="quiet">Advanced edit</Link.Button>
{/if}
</Layout.Stack>
@@ -7,7 +7,7 @@
import { writable } from 'svelte/store';
import ColumnForm from './columns/columnForm.svelte';
import { Permissions } from '$lib/components/permissions';
import { type Columns } from '../store';
import { type Columns, spreadsheetRenderKey } from '../store';
import { ID, type Models } from '@appwrite.io/console';
import { Alert, Layout, Typography, Selector } from '@appwrite.io/pink-svelte';
import SideSheet from '../layout/sidesheet.svelte';
@@ -15,6 +15,7 @@
import { Dependencies } from '$lib/constants';
import { tick } from 'svelte';
import { isRelationship, isRelationshipToMany } from './store';
import { hash } from '$lib/helpers/string';
type CreateRow = {
id?: string;
@@ -93,13 +94,15 @@
$createRow.row = prepareRowPayload($createRow);
try {
await sdk.forProject(page.params.region, page.params.project).tablesDB.createRow({
databaseId: page.params.database,
tableId: page.params.table,
rowId: $createRow.id ?? ID.unique(),
data: $createRow.row,
permissions: $createRow.permissions
});
const row = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.createRow({
databaseId: page.params.database,
tableId: page.params.table,
rowId: $createRow.id ?? ID.unique(),
data: $createRow.row,
permissions: $createRow.permissions
});
addNotification({
message: 'Row has been created',
@@ -109,7 +112,11 @@
trackEvent(Submit.RowCreate, {
customId: !!$createRow.id
});
await invalidate(Dependencies.ROW);
await invalidate(Dependencies.ROWS);
// re-render spreadsheet on addition!
spreadsheetRenderKey.set(hash(row.$id));
if (createMore) {
resetCreateRow();
@@ -1,4 +1,5 @@
import { page } from '$app/state';
import type { Column } from '$lib/helpers/types';
import type { Columns } from '../store';
import { type Models, Query } from '@appwrite.io/console';
@@ -26,6 +27,16 @@ export function isString(column: Columns): column is Models.ColumnString {
return column?.type === 'string';
}
export function isSpatialType(
column: Columns | Column
): column is Models.ColumnPoint | Models.ColumnLine | Models.ColumnPolygon {
if (!column) return false;
const spatialTypes = ['point', 'linestring', 'polygon'];
return spatialTypes.includes(column.type.toLowerCase());
}
/**
* Returns select queries for all main and related fields in a table.
*/
@@ -4,7 +4,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Confirm, Id, SortButton } from '$lib/components';
import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants';
import { Button as ConsoleButton, InputChoice, InputSelect } from '$lib/elements/forms';
import { Button as ConsoleButton, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
@@ -16,6 +16,7 @@
buildWildcardColumnsQuery,
isRelationship,
isRelationshipToMany,
isSpatialType,
isString
} from './rows/store';
import {
@@ -66,10 +67,13 @@
IconDotsHorizontal,
IconFingerPrint,
IconHashtag,
IconLine,
IconLink,
IconLocationMarker,
IconMail,
IconPoint,
IconPlus,
IconPolygon,
IconRelationship,
IconSwitchHorizontal,
IconText,
@@ -78,7 +82,7 @@
} from '@appwrite.io/pink-icons-svelte';
import type { HeaderCellAction, RowCellAction } from './sheetOptions.svelte';
import SheetOptions from './sheetOptions.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
import SpreadsheetContainer from './layout/spreadsheet.svelte';
import EditRowCell from './rows/cell/edit.svelte';
import { copy } from '$lib/helpers/copy';
@@ -107,7 +111,13 @@
const organizationId = data.organization.$id ?? data.project.teamId;
const minimumWidth = 168;
const emptyCellsLimit = $isSmallViewport ? 12 : 18;
const emptyCellsLimit = $spreadsheetLoading
? 30
: $isSmallViewport
? 12
: $isTabletViewport
? 18
: 24;
let selectedRows = [];
let spreadsheetContainer: SpreadsheetContainer;
@@ -117,7 +127,6 @@
let showDelete = false;
let showColumnDelete = false;
let deleteConfirmationChecked = false;
let selectedRowForDelete: Models.Row['$id'] | null = null;
onMount(async () => {
@@ -305,6 +314,12 @@
return IconViewList;
case 'relationship':
return IconRelationship;
case 'point':
return IconPoint;
case 'linestring':
return IconLine;
case 'polygon':
return IconPolygon;
}
}
@@ -909,6 +924,10 @@
variant="secondary"
size="s" />
{/if}
{:else if isSpatialType(rowColumn) && row[columnId] !== null}
<Typography.Text truncate>
{JSON.stringify(row[columnId])}
</Typography.Text>
{:else}
{@const value = row[columnId]}
{@const formatted = formatColumn(row[columnId])}
@@ -1073,8 +1092,12 @@
</SpreadsheetContainer>
<Confirm
confirmDeletion
bind:open={showDelete}
onSubmit={handleDelete}
confirmDeletionLabel={relatedColumns?.length
? `Delete ${selectedRowForDelete !== null ? 'row' : 'rows'} from ${$table.name}`
: undefined}
title={selectedRows.length === 1 ? 'Delete Row' : 'Delete Rows'}>
{@const isSingle = selectedRowForDelete !== null}
@@ -1125,17 +1148,6 @@
<Layout.Stack direction="column" gap="m">
<Alert.Inline>To change the selection edit the relationship settings.</Alert.Inline>
<ul>
<InputChoice
id="delete"
label="Delete"
showLabel={false}
bind:value={deleteConfirmationChecked}>
Delete {isSingle ? 'row' : 'rows'} from
<span data-private>{$table.name}</span>
</InputChoice>
</ul>
</Layout.Stack>
{:else}
<p class="u-bold">This action is irreversible.</p>
@@ -1,5 +1,5 @@
import { page } from '$app/stores';
import type { Column } from '$lib/helpers/types';
import type { Column, ColumnType } from '$lib/helpers/types';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
import type { SortDirection } from '$lib/components';
@@ -15,6 +15,9 @@ export type Columns =
| Models.ColumnIp
| Models.ColumnString
| Models.ColumnUrl
| Models.ColumnPoint
| Models.ColumnLine
| Models.ColumnPolygon
| (Models.ColumnRelationship & { default?: never });
type Table = Omit<Models.Table, 'columns'> & {
@@ -188,3 +191,32 @@ export const PROHIBITED_ROW_KEYS = [
];
export const sheetHeightStore = writable('74.5vh');
export const getDefaultSpatialData = (
type: Extract<ColumnType, 'point' | 'linestring' | 'polygon'>
) => {
if (type === 'point') return [0, 0];
else if (type === 'linestring')
return [
[0, 0],
[0, 0]
];
else if (type === 'polygon')
return [
[
[0, 0],
[0, 0],
[0, 0],
[0, 0]
]
];
};
export const getSingleRingPolygon = () => {
return [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
];
};
@@ -78,7 +78,7 @@
{/snippet}
<Card padding="m" radius="m" {variant}>
<Layout.Stack gap="xxl">
<Layout.Stack gap="l">
<Layout.GridFraction start={4} end={6}>
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center" gap="s" wrap="wrap">
@@ -21,6 +21,7 @@
import { Copy } from '$lib/components';
import { logStatusConverter } from './store';
import { LogsRequest, LogsResponse } from '$lib/components/logs';
import { timer } from '$lib/actions/timer';
export let selectedLogId: string;
export let logs: Models.Execution[];
@@ -139,7 +140,11 @@
Duration
</Typography.Text>
<Typography.Text variant="m-400">
{calculateTime(selectedLog.duration)}
{#if ['processing', 'waiting'].includes(selectedLog.status)}
<span use:timer={{ start: selectedLog.$createdAt }}></span>
{:else}
{calculateTime(selectedLog.duration)}
{/if}
</Typography.Text>
</Layout.Stack>
@@ -25,6 +25,7 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { timer } from '$lib/actions/timer';
export let columns: Column[];
export let executions: Models.ExecutionList;
@@ -121,7 +122,11 @@
</span>
</Tooltip>
{:else if column.id === 'duration'}
{calculateTime(log.duration)}
{#if ['processing', 'waiting'].includes(log.status)}
<span use:timer={{ start: log.$createdAt }}></span>
{:else}
{calculateTime(log.duration)}
{/if}
{/if}
</Table.Cell>
{/each}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 171 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 171 KiB

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.62112 12.97L8.10468 10.9839L8.16326 10.8114L8.10468 10.7155H7.93493L7.35274 10.679L5.3623 10.6244L3.63606 10.5515L1.96362 10.4604L1.54282 10.3693L1.14832 9.84087L1.18896 9.57727L1.54282 9.33554L2.04969 9.38048L3.16983 9.45823L4.85065 9.57606L6.07001 9.64894L7.87635 9.83965H8.16326L8.2039 9.72183L8.10588 9.64894L8.02937 9.57606L6.28998 8.37832L4.40713 7.11257L3.42088 6.38372L2.88771 6.01444L2.61873 5.66824L2.50277 4.91267L2.98693 4.3709L3.63726 4.41585L3.80343 4.46079L4.46212 4.97584L5.86918 6.08247L7.70659 7.45755L7.97557 7.68471L8.08316 7.60696L8.09631 7.5523L7.97557 7.34701L6.97617 5.51154L5.90982 3.64449L5.43523 2.8707L5.3097 2.40667C5.26547 2.21595 5.23319 2.05561 5.23319 1.86004L5.7843 1.09961L6.08914 1L6.82435 1.09961L7.13397 1.37293L7.59063 2.43461L8.33062 4.10609L9.47826 6.37886L9.81419 7.05304L9.9935 7.67742L10.0605 7.86813H10.1764V7.75881L10.2709 6.47847L10.4454 4.9066L10.6151 2.88406L10.6737 2.31435L10.9511 1.63166L11.5022 1.26238L11.9325 1.47132L12.2864 1.98637L12.2374 2.31921L12.027 3.70887L11.6145 5.88568L11.3456 7.34337H11.5022L11.6815 7.16116L12.4071 6.18208L13.6265 4.63328L14.1645 4.01863L14.7921 3.33959L15.1949 3.01647H15.9564L16.5171 3.86314L16.2661 4.73775L15.4818 5.74841L14.8315 6.6048L13.8991 7.88028L13.3169 8.90066L13.3707 8.98205L13.5093 8.96869L15.6157 8.51316L16.7538 8.30422L18.1119 8.06735L18.7263 8.35889L18.7933 8.65528L18.5518 9.26144L17.0993 9.62586L15.3958 9.97206L12.859 10.5819L12.8279 10.6049L12.8638 10.6499L14.0067 10.7592L14.4956 10.7859H15.6923L17.9206 10.9548L18.5028 11.3459L18.8518 11.8245L18.7933 12.189L17.8967 12.653L16.6869 12.3615L13.8632 11.6788L12.8949 11.4334H12.761V11.5148L13.5679 12.3165L15.0467 13.6734L16.8985 15.4226L16.9929 15.855L16.755 16.1964L16.504 16.1599L14.8769 14.916L14.2493 14.3561L12.8279 13.1401H12.7335V13.2676L13.061 13.7548L14.7909 16.3968L14.8805 17.207L14.755 17.4706L14.3067 17.6298L13.8142 17.5387L12.8016 16.0943L11.7568 14.4678L10.914 13.0101L10.8112 13.0696L10.3139 18.5129L10.0808 18.7911L9.54282 19L9.09452 18.6538L8.85663 18.0938L9.09452 16.9872L9.38143 15.5429L9.61454 14.3949L9.82495 12.9688L9.95047 12.4951L9.9421 12.4635L9.83929 12.4769L8.78131 13.9528L7.17222 16.1624L5.89906 17.5472L5.59422 17.6699L5.06583 17.3917L5.11484 16.8949L5.41012 16.4527L7.17222 14.1751L8.23499 12.7635L8.92118 11.9484L8.9164 11.8306H8.87575L4.19554 14.9185L3.3623 15.0278L3.00367 14.6865L3.0479 14.1265L3.21765 13.9443L4.62471 12.9603L4.61992 12.9652L4.62112 12.97Z" fill="#2D2D31"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,37 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_8409_469" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="10" width="16" height="9">
<path d="M9.93746 18.3333L17.177 14.1667L9.93746 10L2.69788 14.1667L9.93746 18.3333Z" fill="url(#paint0_linear_8409_469)"/>
</mask>
<g mask="url(#mask0_8409_469)">
<path d="M9.93746 18.3333L17.177 14.1667L9.93746 10L2.69788 14.1667L9.93746 18.3333Z" fill="#2D2D31"/>
</g>
<mask id="mask1_8409_469" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="9" y="1" width="9" height="14">
<path d="M17.177 14.1667V5.83335L9.93738 1.66669V10L17.177 14.1667Z" fill="url(#paint1_linear_8409_469)"/>
</mask>
<g mask="url(#mask1_8409_469)">
<path d="M17.177 14.1667V5.83335L9.93738 1.66669V10L17.177 14.1667Z" fill="#2D2D31"/>
</g>
<mask id="mask2_8409_469" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="1" width="8" height="14">
<path d="M9.93746 1.66669L2.69788 5.83335V14.1667L9.93746 10V1.66669Z" fill="url(#paint2_linear_8409_469)"/>
</mask>
<g mask="url(#mask2_8409_469)">
<path d="M9.93746 1.66669L2.69788 5.83335V14.1667L9.93746 10V1.66669Z" fill="#2D2D31"/>
</g>
<path d="M17.177 5.83331L9.93738 18.3333V9.99998L17.177 5.83331Z" fill="#2D2D31"/>
<path d="M17.177 5.83331L9.93738 18.3333V9.99998L17.177 5.83331Z" fill="white" fill-opacity="0.48"/>
<path d="M17.177 5.83331L9.93746 9.99998L2.69788 5.83331H17.177Z" fill="#2D2D31"/>
<defs>
<linearGradient id="paint0_linear_8409_469" x1="9.93746" y1="10" x2="9.93746" y2="18.3333" gradientUnits="userSpaceOnUse">
<stop offset="0.16" stop-opacity="0.39"/>
<stop offset="0.658" stop-opacity="0.8"/>
</linearGradient>
<linearGradient id="paint1_linear_8409_469" x1="17.177" y1="5.85905" x2="9.93738" y2="10.1042" gradientUnits="userSpaceOnUse">
<stop offset="0.182" stop-opacity="0.31"/>
<stop offset="0.715" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_8409_469" x1="9.93746" y1="1.66669" x2="2.69788" y2="14.1667" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0.6"/>
<stop offset="0.667" stop-opacity="0.22"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.62112 12.97L8.10468 10.9839L8.16326 10.8114L8.10468 10.7155H7.93493L7.35274 10.679L5.3623 10.6244L3.63606 10.5515L1.96362 10.4604L1.54282 10.3693L1.14832 9.84087L1.18896 9.57727L1.54282 9.33554L2.04969 9.38048L3.16983 9.45823L4.85065 9.57606L6.07001 9.64894L7.87635 9.83965H8.16326L8.2039 9.72183L8.10588 9.64894L8.02937 9.57606L6.28998 8.37832L4.40713 7.11257L3.42088 6.38372L2.88771 6.01444L2.61873 5.66824L2.50277 4.91267L2.98693 4.3709L3.63726 4.41585L3.80343 4.46079L4.46212 4.97584L5.86918 6.08247L7.70659 7.45755L7.97557 7.68471L8.08316 7.60696L8.09631 7.5523L7.97557 7.34701L6.97617 5.51154L5.90982 3.64449L5.43523 2.8707L5.3097 2.40667C5.26547 2.21595 5.23319 2.05561 5.23319 1.86004L5.7843 1.09961L6.08914 1L6.82435 1.09961L7.13397 1.37293L7.59063 2.43461L8.33062 4.10609L9.47826 6.37886L9.81419 7.05304L9.9935 7.67742L10.0605 7.86813H10.1764V7.75881L10.2709 6.47847L10.4454 4.9066L10.6151 2.88406L10.6737 2.31435L10.9511 1.63166L11.5022 1.26238L11.9325 1.47132L12.2864 1.98637L12.2374 2.31921L12.027 3.70887L11.6145 5.88568L11.3456 7.34337H11.5022L11.6815 7.16116L12.4071 6.18208L13.6265 4.63328L14.1645 4.01863L14.7921 3.33959L15.1949 3.01647H15.9564L16.5171 3.86314L16.2661 4.73775L15.4818 5.74841L14.8315 6.6048L13.8991 7.88028L13.3169 8.90066L13.3707 8.98205L13.5093 8.96869L15.6157 8.51316L16.7538 8.30422L18.1119 8.06735L18.7263 8.35889L18.7933 8.65528L18.5518 9.26144L17.0993 9.62586L15.3958 9.97206L12.859 10.5819L12.8279 10.6049L12.8638 10.6499L14.0067 10.7592L14.4956 10.7859H15.6923L17.9206 10.9548L18.5028 11.3459L18.8518 11.8245L18.7933 12.189L17.8967 12.653L16.6869 12.3615L13.8632 11.6788L12.8949 11.4334H12.761V11.5148L13.5679 12.3165L15.0467 13.6734L16.8985 15.4226L16.9929 15.855L16.755 16.1964L16.504 16.1599L14.8769 14.916L14.2493 14.3561L12.8279 13.1401H12.7335V13.2676L13.061 13.7548L14.7909 16.3968L14.8805 17.207L14.755 17.4706L14.3067 17.6298L13.8142 17.5387L12.8016 16.0943L11.7568 14.4678L10.914 13.0101L10.8112 13.0696L10.3139 18.5129L10.0808 18.7911L9.54282 19L9.09452 18.6538L8.85663 18.0938L9.09452 16.9872L9.38143 15.5429L9.61454 14.3949L9.82495 12.9688L9.95047 12.4951L9.9421 12.4635L9.83929 12.4769L8.78131 13.9528L7.17222 16.1624L5.89906 17.5472L5.59422 17.6699L5.06583 17.3917L5.11484 16.8949L5.41012 16.4527L7.17222 14.1751L8.23499 12.7635L8.92118 11.9484L8.9164 11.8306H8.87575L4.19554 14.9185L3.3623 15.0278L3.00367 14.6865L3.0479 14.1265L3.21765 13.9443L4.62471 12.9603L4.61992 12.9652L4.62112 12.97Z" fill="#EDEDF0"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,37 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_8409_615" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="10" width="16" height="9">
<path d="M9.93746 18.3333L17.177 14.1667L9.93746 10L2.69788 14.1667L9.93746 18.3333Z" fill="url(#paint0_linear_8409_615)"/>
</mask>
<g mask="url(#mask0_8409_615)">
<path d="M9.93746 18.3333L17.177 14.1667L9.93746 10L2.69788 14.1667L9.93746 18.3333Z" fill="#EDEDF0"/>
</g>
<mask id="mask1_8409_615" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="9" y="1" width="9" height="14">
<path d="M17.177 14.1667V5.83335L9.93738 1.66669V10L17.177 14.1667Z" fill="url(#paint1_linear_8409_615)"/>
</mask>
<g mask="url(#mask1_8409_615)">
<path d="M17.177 14.1667V5.83335L9.93738 1.66669V10L17.177 14.1667Z" fill="#EDEDF0"/>
</g>
<mask id="mask2_8409_615" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="1" width="8" height="14">
<path d="M9.93746 1.66669L2.69788 5.83335V14.1667L9.93746 10V1.66669Z" fill="url(#paint2_linear_8409_615)"/>
</mask>
<g mask="url(#mask2_8409_615)">
<path d="M9.93746 1.66669L2.69788 5.83335V14.1667L9.93746 10V1.66669Z" fill="#EDEDF0"/>
</g>
<path d="M17.177 5.83331L9.93738 18.3333V9.99998L17.177 5.83331Z" fill="#EDEDF0"/>
<path d="M17.177 5.83331L9.93738 18.3333V9.99998L17.177 5.83331Z" fill="white" fill-opacity="0.48"/>
<path d="M17.177 5.83331L9.93746 9.99998L2.69788 5.83331H17.177Z" fill="#EDEDF0"/>
<defs>
<linearGradient id="paint0_linear_8409_615" x1="9.93746" y1="10" x2="9.93746" y2="18.3333" gradientUnits="userSpaceOnUse">
<stop offset="0.16" stop-opacity="0.39"/>
<stop offset="0.658" stop-opacity="0.8"/>
</linearGradient>
<linearGradient id="paint1_linear_8409_615" x1="17.177" y1="5.85905" x2="9.93738" y2="10.1042" gradientUnits="userSpaceOnUse">
<stop offset="0.182" stop-opacity="0.31"/>
<stop offset="0.715" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_8409_615" x1="9.93746" y1="1.66669" x2="2.69788" y2="14.1667" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0.6"/>
<stop offset="0.667" stop-opacity="0.22"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7515 3.09828C13.7515 2.62741 13.2243 2.6274 12.99 2.68626C13.318 2.42728 13.7027 2.49007 13.8394 2.56855L17.0072 4.12186C17.309 4.26986 17.5005 4.57782 17.5005 4.9153V15.1338C17.5005 15.4758 17.3039 15.7871 16.996 15.9327L14.0151 17.3422C13.8101 17.4304 13.3649 17.6482 12.99 17.3421C13.4586 17.4304 13.7125 17.0969 13.7515 16.8713V3.09828Z" fill="#EDEDF0"/>
<path d="M13.0273 2.67773C13.2759 2.62858 13.7508 2.65137 13.7508 3.09824V6.61557L4.14654 13.8813C3.97866 14.0083 3.74241 13.9873 3.59942 13.8325L2.60957 12.7614C2.4545 12.5936 2.46535 12.331 2.63372 12.1767L12.9892 2.68622L13.0273 2.67773Z" fill="#EDEDF0"/>
<path d="M13.7508 13.4129L4.14654 6.14708C3.97866 6.02008 3.74241 6.04115 3.59942 6.19587L2.60957 7.26698C2.4545 7.43478 2.46535 7.69744 2.63372 7.85175L12.9892 17.3422C13.4579 17.4305 13.7117 17.0969 13.7508 16.8713V13.4129Z" fill="#EDEDF0"/>
</svg>

After

Width:  |  Height:  |  Size: 978 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.38283 10.7525L5.39611 7.99401C5.22398 7.51281 5.57208 7.00214 6.06732 7.02528C11.5964 7.2832 14.4331 9.29146 14.1687 15.0827C13.2071 11.0382 8.5951 10.7525 6.38283 10.7525Z" fill="#EDEDF0"/>
<path d="M4.89231 5.67595L3.86522 3.00302C3.67933 2.51927 4.02719 1.99404 4.53013 2.00005C10.1767 2.06749 14.1193 2.44873 14.1193 9.95706C13.1577 5.91252 7.10459 5.67595 4.89231 5.67595Z" fill="#EDEDF0"/>
<path d="M7.91135 15.9917L6.85283 13.0398C6.68118 12.5612 7.02451 12.0516 7.51714 12.0777C12.0282 12.3168 14.2845 14.2758 14.0232 20C13.0616 15.9555 10.1396 15.9917 7.91135 15.9917Z" fill="#EDEDF0"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7514 3.09828C13.7514 2.62741 13.2242 2.6274 12.9899 2.68626C13.3179 2.42728 13.7026 2.49007 13.8392 2.56855L17.0071 4.12186C17.3089 4.26986 17.5004 4.57782 17.5004 4.9153V15.1338C17.5004 15.4758 17.3038 15.7871 16.9958 15.9327L14.015 17.3422C13.81 17.4304 13.3648 17.6482 12.9899 17.3421C13.4585 17.4304 13.7123 17.0969 13.7514 16.8713V3.09828Z" fill="#2D2D31"/>
<path d="M13.0273 2.67773C13.2759 2.62858 13.7508 2.65137 13.7508 3.09824V6.61557L4.14654 13.8813C3.97866 14.0083 3.74241 13.9873 3.59942 13.8325L2.60957 12.7614C2.4545 12.5936 2.46535 12.331 2.63372 12.1767L12.9892 2.68622L13.0273 2.67773Z" fill="#2D2D31"/>
<path d="M13.7508 13.4129L4.14654 6.14708C3.97866 6.02008 3.74241 6.04115 3.59942 6.19587L2.60957 7.26698C2.4545 7.43478 2.46535 7.69744 2.63372 7.85175L12.9892 17.3422C13.4579 17.4305 13.7117 17.0969 13.7508 16.8713V13.4129Z" fill="#2D2D31"/>
</svg>

After

Width:  |  Height:  |  Size: 982 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.38283 10.7525L5.39611 7.99401C5.22398 7.51281 5.57208 7.00214 6.06732 7.02528C11.5964 7.2832 14.4331 9.29146 14.1687 15.0827C13.2071 11.0382 8.5951 10.7525 6.38283 10.7525Z" fill="#2D2D31"/>
<path d="M4.89231 5.67595L3.86522 3.00302C3.67933 2.51927 4.02719 1.99404 4.53013 2.00005C10.1767 2.06749 14.1193 2.44873 14.1193 9.95706C13.1577 5.91252 7.10459 5.67595 4.89231 5.67595Z" fill="#2D2D31"/>
<path d="M7.91135 15.9917L6.85283 13.0398C6.68118 12.5612 7.02451 12.0516 7.51714 12.0777C12.0282 12.3168 14.2845 14.2758 14.0232 20C13.0616 15.9555 10.1396 15.9917 7.91135 15.9917Z" fill="#2D2D31"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

@@ -0,0 +1,7 @@
<script lang="ts">
import { app } from '$lib/stores/app';
import Light from '../assets/claude.svg';
import Dark from '../assets/dark/claude.svg';
</script>
<img src={$app.themeInUse === 'dark' ? Dark : Light} width="16" height="16" alt="" />
@@ -0,0 +1,7 @@
<script lang="ts">
import { app } from '$lib/stores/app';
import Light from '../assets/cursor-ai.svg';
import Dark from '../assets/dark/cursor-ai.svg';
</script>
<img src={$app.themeInUse === 'dark' ? Dark : Light} width="16" height="16" alt="" />
@@ -0,0 +1,7 @@
<script lang="ts">
import { app } from '$lib/stores/app';
import Light from '../assets/vscode.svg';
import Dark from '../assets/dark/vscode.svg';
</script>
<img src={$app.themeInUse === 'dark' ? Dark : Light} width="16" height="16" alt="" />
@@ -0,0 +1,7 @@
<script lang="ts">
import { app } from '$lib/stores/app';
import Light from '../assets/windsurf.svg';
import Dark from '../assets/dark/windsurf.svg';
</script>
<img src={$app.themeInUse === 'dark' ? Dark : Light} width="16" height="16" alt="" />
@@ -12,6 +12,8 @@
} from '@appwrite.io/pink-svelte';
import { addPlatform, continuePlatform } from './platforms/+page.svelte';
import { app } from '$lib/stores/app';
import AuthPreview from './assets/auth-preview.svg';
import AuthPreviewDark from './assets/auth-preview-dark.svg';
import {
IconArrowRight,
IconNodeJs,
@@ -20,10 +22,9 @@
} from '@appwrite.io/pink-icons-svelte';
import DatabaseImgSource from './assets/database.png';
import DatabaseImgSourceDark from './assets/database-dark.png';
import UsersImgSource from './assets/users.svg';
import UsersImgSourceDark from './assets/users-dark.svg';
import DiscordImgSource from './assets/discord.png';
import DiscordImgSourceDark from './assets/discord-dark.png';
import { mcpTools } from '../store';
import PlatformIosImgSource from './assets/platform-ios.svg';
import PlatformIosImgSourceDark from './assets/platform-ios-dark.svg';
import PlatformAndroidImgSource from './assets/platform-android.svg';
@@ -102,9 +103,7 @@
</Layout.Stack>
</div>
<Layout.Stack gap="l">
<Layout.Stack
gap="l"
direction={$isSmallViewport ? 'column' : 'row'}>
<Layout.Stack gap="l" direction="row">
<Card.Button
on:click={() => {
openPlatformWizard(0, platformMap.get('Web'));
@@ -479,10 +478,12 @@
<div class="double-width-card">
<Card.Base padding="s"
><div class="full-height-card">
<Layout.Stack direction="row">
<Layout.Stack
direction={$isSmallViewport ? 'column' : 'row'}>
<Layout.Stack
gap="xl"
justifyContent="space-between">
justifyContent="space-between"
style={`flex: ${$isSmallViewport ? '1 1 auto' : '0 0 30%'}; min-width: ${$isSmallViewport ? 'auto' : '240px'}`}>
<Typography.Title size="s"
>Set up Auth</Typography.Title>
<Layout.Stack
@@ -519,33 +520,95 @@
</Layout.Stack>
<div
class="auth-image"
style="flex: 1 1 auto"
style:background-image={`url('${
$app.themeInUse === 'dark'
? UsersImgSourceDark
: UsersImgSource
? AuthPreviewDark
: AuthPreview
}')`}>
</div>
</Layout.Stack>
</div></Card.Base>
</div>
</Layout.Stack>
<Layout.Stack
gap="l"
direction={$isSmallViewport ? 'column' : 'row'}>
<Card.Base
padding="s"
style={`flex: ${$isSmallViewport ? '1 1 auto' : '1 1 70%'};`}>
<div class="full-height-card">
<Layout.Stack gap="xl" justifyContent="space-between">
<Layout.Stack gap="s">
<Typography.Title
color="--fgcolor-neutral-secondary"
size="s">MCP server</Typography.Title>
<Typography.Text
color="--fgcolor-neutral-secondary">
Deploy the Appwrite MCP server with a single
click, or view the <Link.Anchor
href="https://appwrite.io/docs"
target="_blank">docs</Link.Anchor> for instructions.
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="s">
<Typography.Text
color="--fgcolor-neutral-tertiary"
size="s">One-click install</Typography.Text>
<Layout.Stack
direction="row"
gap="s"
wrap="wrap">
{#each mcpTools as tool}
<Button.Anchor
href={tool.href}
target="_blank"
rel="noreferrer"
size="s"
variant="secondary">
<Icon
slot="start"
icon={tool.icon}
size="xs" />
{tool.label}
</Button.Anchor>
{/each}
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</div>
</Card.Base>
<Card.Link
href="https://appwrite.io/discord"
padding="s"
on:click={() => {
trackEvent(Click.OnboardingDiscordClick);
}}>
}}
style={`flex: ${$isSmallViewport ? '1 1 auto' : '1 1 28%'};`}>
<div class="full-height-card">
<Layout.Stack gap="xs" justifyContent="space-between">
<Layout.Stack direction="column" gap="xxxs">
<img
src={$app.themeInUse === 'dark'
? DiscordImgSourceDark
: DiscordImgSource}
class="discord"
alt="" />
<Typography.Title size="s"
>Discord</Typography.Title>
<Layout.Stack
direction="row"
alignItems="center"
justifyContent="space-between"
class="discord-header">
<Layout.Stack
direction="row"
alignItems="flex-start"
gap="xs">
<img
src={$app.themeInUse === 'dark'
? DiscordImgSourceDark
: DiscordImgSource}
class="discord"
alt="" />
<Typography.Title size="s"
>Discord</Typography.Title>
</Layout.Stack>
<div class="arrow-icon arrow-icon-discord">
<Icon icon={IconArrowRight} size="s" />
</div>
</Layout.Stack>
<Layout.Stack
direction="row"
@@ -556,9 +619,6 @@
color="--fgcolor-neutral-secondary">
Join our Discord for support, tips and
product updates</Typography.Text>
<div class="arrow-icon arrow-icon-discord">
<Icon icon={IconArrowRight} size="s" />
</div>
</Layout.Stack>
</Layout.Stack>
</div>
@@ -635,7 +695,7 @@
margin-bottom: calc(-1 * var(--base-16, 16px));
align-self: flex-end;
background-size: cover;
background-position: left bottom;
background-position: right bottom;
background-repeat: no-repeat;
}
:global(.full-height-card div) {
@@ -647,6 +707,30 @@
width: 250%;
}
}
.double-width-card .full-height-card {
height: 240px;
width: 100%;
@media (max-width: 1024px) {
height: 220px;
}
@media (max-width: 768px) {
height: auto;
}
}
.auth-image {
@media (max-width: 1024px) {
height: 180px;
background-position: center bottom;
}
@media (max-width: 768px) {
height: 160px;
margin-right: 0;
}
}
.arrow-icon {
color: var(--border-neutral-strong);
@@ -656,7 +740,7 @@
}
}
.arrow-icon-discord {
align-items: flex-end;
align-items: flex-start;
}
.platform-image {
@@ -58,7 +58,7 @@
<svelte:fragment slot="actions">
<Button
external
href="#/"
href="https://appwrite.io/docs/advanced/platform/custom-domains"
text
event="empty_documentation"
size="s"
@@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { Avatar, CardGrid, PaginationInline } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Button as FormButton } from '$lib/elements/forms';
import { getApiEndpoint } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import GitDisconnectModal from './GitDisconnectModal.svelte';
@@ -15,8 +15,10 @@
Icon,
Layout,
Popover,
Table
Table,
Button
} from '@appwrite.io/pink-svelte';
import { Link as PinkLink } from '@appwrite.io/pink-svelte';
import {
IconExternalLink,
IconGithub,
@@ -27,7 +29,7 @@
import { Click, trackEvent } from '$lib/actions/analytics';
import type { ComponentType } from 'svelte';
import { Link } from '$lib/elements';
import { regionalConsoleVariables } from '../store';
import { regionalConsoleVariables, mcpTools } from '../store';
export let total: number;
export let limit: number;
@@ -83,7 +85,7 @@
{#if total > 0}
<Layout.Stack gap="l">
<Layout.Stack direction="row" justifyContent="flex-end">
<Button
<FormButton
secondary
href={configureGitHub()}
on:click={() => {
@@ -91,7 +93,7 @@
}}>
<Icon icon={IconPlus} slot="start" size="s" />
Add installation
</Button>
</FormButton>
</Layout.Stack>
<Table.Root
@@ -171,10 +173,10 @@
Before installing Git in a locally hosted Appwrite project, ensure your environment
variables are configured.
<svelte:fragment slot="actions">
<Button
<FormButton
compact
href="https://appwrite.io/docs/advanced/self-hosting/configuration/version-control"
external>Learn more</Button>
external>Learn more</FormButton>
</svelte:fragment>
</Alert.Inline>
{:else}
@@ -184,10 +186,10 @@
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
<svelte:fragment slot="actions">
<Button secondary href={configureGitHub()} external>
<FormButton secondary href={configureGitHub()} external>
<Icon icon={IconGithub} size="s" slot="start" />
Connect to GitHub
</Button>
</FormButton>
</svelte:fragment>
</Empty>
</Card.Base>
@@ -198,3 +200,36 @@
{#if showGitDisconnect}
<GitDisconnectModal bind:showGitDisconnect {selectedInstallation} />
{/if}
<CardGrid>
<svelte:fragment slot="title">MCP server</svelte:fragment>
Spin up an MCP server to stream rich project context to AI tooling, unlocking deeper code understanding
and better completions.
<svelte:fragment slot="aside">
<Card.Base padding="none" border="dashed">
<Empty type="secondary" title="MCP installation">
<svelte:fragment slot="description">
Deploy the Appwrite MCP server with a single click, or view the <PinkLink.Anchor
href="https://appwrite.io/docs"
target="_blank"
rel="noreferrer">docs</PinkLink.Anchor> for instructions.
</svelte:fragment>
<svelte:fragment slot="actions">
<Layout.Stack direction="row" gap="s" wrap="wrap" justifyContent="center">
{#each mcpTools as tool}
<Button.Anchor
href={tool.href}
target="_blank"
rel="noreferrer"
size="s"
variant="secondary">
<Icon slot="start" icon={tool.icon} size="xs" />
{tool.label}
</Button.Anchor>
{/each}
</Layout.Stack>
</svelte:fragment>
</Empty>
</Card.Base>
</svelte:fragment>
</CardGrid>
@@ -56,8 +56,10 @@
$effect(() => {
domainIsValid; /* silences lint for unused var */
if (domain !== newDomain) {
domainStatus = 'pending';
if (newDomain) {
if (domain !== newDomain) {
domainStatus = 'pending';
}
checkDomain(newDomain);
}
});
@@ -53,7 +53,7 @@
let domain = data.domain;
let domainIsValid = true;
let framework = data?.template?.frameworks[0];
let branch = 'main';
let branch = '';
let rootDir = './';
let connectBehaviour: 'now' | 'later' = 'now';
let repositoryBehaviour: 'new' | 'existing' = 'new';
@@ -19,6 +19,7 @@
import { Copy } from '$lib/components';
import { LogsRequest, LogsResponse } from '$lib/components/logs';
import { site } from '../store';
import { timer } from '$lib/actions/timer';
export let open = false;
export let selectedLogId: string;
@@ -112,7 +113,12 @@
Duration
</Typography.Text>
<Typography.Text variant="m-400">
{calculateTime(selectedLog.duration)}
{#if ['processing', 'waiting'].includes(selectedLog.status)}
<span use:timer={{ start: selectedLog.$createdAt }}
></span>
{:else}
{calculateTime(selectedLog.duration)}
{/if}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
@@ -14,6 +14,7 @@
import { calculateTime } from '$lib/helpers/timeConversion';
import { getBadgeTypeFromStatusCode } from '$lib/helpers/httpStatus';
import { Button } from '$lib/elements/forms';
import { timer } from '$lib/actions/timer';
export let columns: Column[];
export let logs: Models.ExecutionList;
@@ -83,7 +84,11 @@
{log.requestMethod}
</Typography.Code>
{:else if column.id === 'duration'}
{calculateTime(log.duration)}
{#if ['processing', 'waiting'].includes(log.status)}
<span use:timer={{ start: log.$createdAt }}></span>
{:else}
{calculateTime(log.duration)}
{/if}
{:else if column.id === 'responseStatusCode'}
<div>
<Badge
@@ -3,6 +3,10 @@ import type { BarSeriesOption } from 'echarts/charts';
import { derived, get, writable } from 'svelte/store';
import { regions } from '$lib/stores/organization';
import { page } from '$app/stores';
import CursorIcon from './overview/components/CursorIcon.svelte';
import ClaudeIcon from './overview/components/ClaudeIcon.svelte';
import VSCodeIcon from './overview/components/VSCodeIcon.svelte';
import WindsurfIcon from './overview/components/WindsurfIcon.svelte';
export const project = derived(page, ($page) => $page.data.project as Models.Project);
@@ -73,3 +77,26 @@ function createStats() {
}
export const stats = createStats();
export const mcpTools = [
{
href: 'https://apwr.dev/docs-mcp-cursor?ref=docs',
icon: CursorIcon,
label: 'Cursor'
},
{
href: 'https://appwrite.io/docs/tooling/mcp/claude',
icon: ClaudeIcon,
label: 'Claude Desktop'
},
{
href: 'https://apwr.dev/docs-mcp-vscode?ref=docs',
icon: VSCodeIcon,
label: 'VS code'
},
{
href: 'https://appwrite.io/docs/tooling/mcp/windsurf',
icon: WindsurfIcon,
label: 'Windsurf editor'
}
];
@@ -7,13 +7,14 @@
import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg';
import GithubLogoDark from '$lib/images/github-logo-dark.svg';
import GithubLogoLight from '$lib/images/github-logo-light.svg';
import { base } from '$app/paths';
function onGithubLogin() {
localStorage.setItem('githubEducationProgram', 'true');
sdk.forConsole.account.createOAuth2Session({
provider: OAuthProvider.Github,
success: window.location.origin + '/console/education?success',
failure: window.location.origin + '/console/education?failure',
success: window.location.origin + base + '/education?success',
failure: window.location.origin + base + '/education?failure',
scopes: ['read:user', 'user:email']
});
}
@@ -20,12 +20,35 @@
import { redirectTo } from '$routes/store';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { Layout, Link, Typography } from '@appwrite.io/pink-svelte';
import { getRandomTestimonial } from '$lib/data/testimonials';
export let data;
let name: string, mail: string, pass: string, disabled: boolean;
let terms = false;
const randomTestimonial = getRandomTestimonial();
const testimonialCampaign = {
$id: 'testimonial-signup',
template: 'review',
title: randomTestimonial.headline,
description: 'Join thousands of developers building amazing apps with Appwrite',
reviews: [
{
name: randomTestimonial.name,
image: randomTestimonial.avatar,
description: randomTestimonial.title,
review: randomTestimonial.blurb
}
]
};
trackEvent(Submit.TestimonialView, {
testimonial_id: randomTestimonial.id,
testimonial_name: randomTestimonial.name,
testimonial_company: randomTestimonial.title
});
async function register() {
try {
disabled = true;
@@ -43,7 +66,9 @@
trackEvent(Submit.AccountCreate, {
campaign_name: data?.couponData?.code,
email: mail,
name: name
name: name,
testimonial_id: randomTestimonial.id,
testimonial_name: randomTestimonial.name
});
if (data?.couponData?.code) {
@@ -94,7 +119,7 @@
<title>Sign up - Appwrite</title>
</svelte:head>
<Unauthenticated coupon={data?.couponData} campaign={data?.campaign}>
<Unauthenticated coupon={data?.couponData} campaign={data?.campaign || testimonialCampaign}>
<svelte:fragment slot="title">Sign up</svelte:fragment>
<svelte:fragment>
<Form onSubmit={register}>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { loading } from '$routes/store';
import { Typography } from '@appwrite.io/pink-svelte';
@@ -16,13 +17,13 @@
<Typography.Eyebrow color="--fgcolor-neutral-secondary">POWERED BY</Typography.Eyebrow>
{#if $app.themeInUse === 'dark'}
<img
src="/console/images/appwrite-logo-dark.svg"
src="{base}/images/appwrite-logo-dark.svg"
width="120"
height="22"
alt="Appwrite Logo" />
{:else}
<img
src="/console/images/appwrite-logo-light.svg"
src="{base}/images/appwrite-logo-light.svg"
width="120"
height="22"
alt="Appwrite Logo" />
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
let { children } = $props();
@@ -11,13 +12,13 @@
<footer>
{#if $app.themeInUse === 'dark'}
<img
src="/console/images/appwrite-logo-dark.svg"
src="{base}/images/appwrite-logo-dark.svg"
width="120"
height="22"
alt="Appwrite Logo" />
{:else}
<img
src="/console/images/appwrite-logo-light.svg"
src="{base}/images/appwrite-logo-light.svg"
width="120"
height="22"
alt="Appwrite Logo" />
@@ -5,13 +5,14 @@ import { isCloud } from '$lib/system.js';
import { error, redirect } from '@sveltejs/kit';
import type { OrganizationList } from '$lib/stores/organization.js';
import { redirectTo } from '$routes/store.js';
import { base } from '$app/paths';
export const load = async ({ parent, url, params }) => {
const { account } = await parent();
if (!account && !isCloud) {
redirectTo.set(url.pathname + url.search);
redirect(302, '/console/login?redirect=' + url.pathname + url.search);
redirect(302, base + '/login?redirect=' + url.pathname + url.search);
}
if (!url.searchParams.has('type')) {
+2 -2
View File
@@ -163,14 +163,14 @@
{#each preloadFonts as font}
<link rel="preload" href={font} as="font" type="font/woff2" crossorigin="anonymous" />
{/each}
<link rel="preload" as="style" type="text/css" href="/console/fonts/main.css" />
<link rel="preload" as="style" type="text/css" href="{base}/fonts/main.css" />
<link rel="stylesheet" href={`${base}/fonts/main.css`} />
{#if isCloud}
{#each preloadFontsCloud as font}
<link rel="preload" href={font} as="font" type="font/woff2" crossorigin="anonymous" />
{/each}
<link rel="preload" as="style" type="text/css" href="/console/fonts/cloud.css" />
<link rel="preload" as="style" type="text/css" href="{base}/fonts/cloud.css" />
<link rel="stylesheet" href={`${base}/fonts/cloud.css`} />
{/if}
</svelte:head>
-16
View File
@@ -1,16 +0,0 @@
{
"redirects": [
{
"source": "/",
"destination": "/console/",
"permanent": true
}
],
"rewrites": [
{
"source": "/console/(.*)",
"destination": "/$1"
}
],
"installCommand": "pnpm install --frozen-lockfile"
}