From 7cd5f0612218bc598b41076c298e08c37ec500bd Mon Sep 17 00:00:00 2001
From: reya <123083837+reyamir@users.noreply.github.com>
Date: Tue, 23 Jul 2024 14:18:40 +0700
Subject: [PATCH] feat: add login screen
---
index.html | 5 +-
package.json | 12 +-
pnpm-lock.yaml | 254 +++++++++++++++++++++++++++++++
src/App.css | 89 +++++++++++
src/commons.ts | 23 +++
src/components/spinner.tsx | 47 ++++++
src/components/user/about.tsx | 12 ++
src/components/user/avatar.tsx | 40 +++++
src/components/user/cover.tsx | 36 +++++
src/components/user/index.ts | 15 ++
src/components/user/name.tsx | 21 +++
src/components/user/provider.tsx | 71 +++++++++
src/components/user/root.tsx | 12 ++
src/main.tsx | 13 +-
src/routes.gen.ts | 52 ++++++-
src/routes/$account.chats.tsx | 5 +
src/routes/__root.tsx | 9 +-
src/routes/index.lazy.tsx | 5 -
src/routes/index.tsx | 111 ++++++++++++++
src/routes/new.lazy.tsx | 5 +
tsconfig.json | 24 ++-
vite.config.ts | 3 +-
22 files changed, 837 insertions(+), 27 deletions(-)
create mode 100644 src/commons.ts
create mode 100644 src/components/spinner.tsx
create mode 100644 src/components/user/about.tsx
create mode 100644 src/components/user/avatar.tsx
create mode 100644 src/components/user/cover.tsx
create mode 100644 src/components/user/index.ts
create mode 100644 src/components/user/name.tsx
create mode 100644 src/components/user/provider.tsx
create mode 100644 src/components/user/root.tsx
create mode 100644 src/routes/$account.chats.tsx
delete mode 100644 src/routes/index.lazy.tsx
create mode 100644 src/routes/index.tsx
create mode 100644 src/routes/new.lazy.tsx
diff --git a/index.html b/index.html
index be7b850..841d357 100644
--- a/index.html
+++ b/index.html
@@ -6,9 +6,8 @@
Coop
-
-
-
+
+
diff --git a/package.json b/package.json
index 0e88455..c629b74 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,16 @@
"tauri": "tauri"
},
"dependencies": {
+ "@phosphor-icons/react": "^2.1.7",
+ "@radix-ui/react-avatar": "^1.1.0",
+ "@tanstack/react-query": "^5.51.11",
"@tanstack/react-router": "^1.45.8",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
+ "minidenticons": "^4.2.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "virtua": "^0.33.3"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
@@ -24,9 +29,12 @@
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
+ "clsx": "^2.1.1",
"postcss": "^8.4.39",
+ "tailwind-merge": "^2.4.0",
"tailwindcss": "^3.4.6",
"typescript": "^5.2.2",
- "vite": "^5.3.1"
+ "vite": "^5.3.1",
+ "vite-tsconfig-paths": "^4.3.2"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 756307d..5073c01 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,15 @@ importers:
.:
dependencies:
+ '@phosphor-icons/react':
+ specifier: ^2.1.7
+ version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-avatar':
+ specifier: ^1.1.0
+ version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@tanstack/react-query':
+ specifier: ^5.51.11
+ version: 5.51.11(react@18.3.1)
'@tanstack/react-router':
specifier: ^1.45.8
version: 1.45.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -17,12 +26,18 @@ importers:
'@tauri-apps/plugin-shell':
specifier: '>=2.0.0-beta.0'
version: 2.0.0-beta.8
+ minidenticons:
+ specifier: ^4.2.1
+ version: 4.2.1
react:
specifier: ^18.2.0
version: 18.3.1
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
+ virtua:
+ specifier: ^0.33.3
+ version: 0.33.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@biomejs/biome':
specifier: 1.8.3
@@ -45,9 +60,15 @@ importers:
autoprefixer:
specifier: ^10.4.19
version: 10.4.19(postcss@8.4.39)
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
postcss:
specifier: ^8.4.39
version: 8.4.39
+ tailwind-merge:
+ specifier: ^2.4.0
+ version: 2.4.0
tailwindcss:
specifier: ^3.4.6
version: 3.4.6
@@ -57,6 +78,9 @@ importers:
vite:
specifier: ^5.3.1
version: 5.3.4
+ vite-tsconfig-paths:
+ specifier: ^4.3.2
+ version: 4.3.2(typescript@5.5.4)(vite@5.3.4)
packages:
@@ -408,10 +432,88 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@phosphor-icons/react@2.1.7':
+ resolution: {integrity: sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: '>= 16.8'
+ react-dom: '>= 16.8'
+
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
+ '@radix-ui/react-avatar@1.1.0':
+ resolution: {integrity: sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-compose-refs@1.1.0':
+ resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context@1.1.0':
+ resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-primitive@2.0.0':
+ resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.1.0':
+ resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.0':
+ resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.0':
+ resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@rollup/rollup-android-arm-eabi@4.19.0':
resolution: {integrity: sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==}
cpu: [arm]
@@ -496,6 +598,14 @@ packages:
resolution: {integrity: sha512-n4XXInV9irIq0obRvINIkESkGk280Q+xkIIbswmM0z9nAu2wsIRZNvlmPrtYh6bgNWtItOWWoihFUjLTW8g6Jg==}
engines: {node: '>=12'}
+ '@tanstack/query-core@5.51.9':
+ resolution: {integrity: sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==}
+
+ '@tanstack/react-query@5.51.11':
+ resolution: {integrity: sha512-4Kq2x0XpDlpvSnaLG+8pHNH60zEc3mBvb3B2tOMDjcPCi/o+Du3p/9qpPLwJOTliVxxPJAP27fuIhLrsRdCr7A==}
+ peerDependencies:
+ react: ^18.0.0
+
'@tanstack/react-router@1.45.8':
resolution: {integrity: sha512-hLJOKDK5lGHteoMjpF6COQrlhsl4C6GyBCzmSJHFcoh26GBa7tv/94li0H1a3deJpzMNpSvmSXrQDpxj9h9bNA==}
engines: {node: '>=12'}
@@ -712,6 +822,10 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -830,6 +944,9 @@ packages:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
+ globrex@0.1.2:
+ resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
@@ -914,6 +1031,10 @@ packages:
resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
engines: {node: '>=8.6'}
+ minidenticons@4.2.1:
+ resolution: {integrity: sha512-oWfFivA0lOx/V/bO/YIJbthB26lV8JXYvhnv9zM2hNd3fzsHTXQ6c6bWZPcvhD3nnOB+lQk/D9lF43BXixrN8g==}
+ engines: {node: '>=15.14.0'}
+
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -1118,6 +1239,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ tailwind-merge@2.4.0:
+ resolution: {integrity: sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==}
+
tailwindcss@3.4.6:
resolution: {integrity: sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==}
engines: {node: '>=14.0.0'}
@@ -1147,6 +1271,16 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+ tsconfck@3.1.1:
+ resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
typescript@5.5.4:
resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
engines: {node: '>=14.17'}
@@ -1170,6 +1304,34 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ virtua@0.33.3:
+ resolution: {integrity: sha512-Zxr2hhmTHARMHdZjs5fvd17bHH2YJ1uZZGaw4SKmynDEXtHFzJn/pL9xYJeXWZ8UfXNIBbPvGlHGpruCRbLHIg==}
+ peerDependencies:
+ react: '>=16.14.0'
+ react-dom: '>=16.14.0'
+ solid-js: '>=1.0'
+ svelte: '>=4.0'
+ vue: '>=3.2'
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ solid-js:
+ optional: true
+ svelte:
+ optional: true
+ vue:
+ optional: true
+
+ vite-tsconfig-paths@4.3.2:
+ resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
+ peerDependencies:
+ vite: '*'
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
vite@5.3.4:
resolution: {integrity: sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -1535,9 +1697,66 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
+ '@phosphor-icons/react@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
'@pkgjs/parseargs@0.11.0':
optional: true
+ '@radix-ui/react-avatar@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+
+ '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
+
+ '@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
+
+ '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+
+ '@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
+
+ '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
+
+ '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.3
+
'@rollup/rollup-android-arm-eabi@4.19.0':
optional: true
@@ -1588,6 +1807,13 @@ snapshots:
'@tanstack/history@1.45.3': {}
+ '@tanstack/query-core@5.51.9': {}
+
+ '@tanstack/react-query@5.51.11(react@18.3.1)':
+ dependencies:
+ '@tanstack/query-core': 5.51.9
+ react: 18.3.1
+
'@tanstack/react-router@1.45.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/history': 1.45.3
@@ -1814,6 +2040,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ clsx@2.1.1: {}
+
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -1935,6 +2163,8 @@ snapshots:
globals@11.12.0: {}
+ globrex@0.1.2: {}
+
has-flag@3.0.0: {}
hasown@2.0.2:
@@ -1998,6 +2228,8 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
+ minidenticons@4.2.1: {}
+
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
@@ -2190,6 +2422,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ tailwind-merge@2.4.0: {}
+
tailwindcss@3.4.6:
dependencies:
'@alloc/quick-lru': 5.2.0
@@ -2237,6 +2471,10 @@ snapshots:
ts-interface-checker@0.1.13: {}
+ tsconfck@3.1.1(typescript@5.5.4):
+ optionalDependencies:
+ typescript: 5.5.4
+
typescript@5.5.4: {}
unplugin@1.11.0:
@@ -2258,6 +2496,22 @@ snapshots:
util-deprecate@1.0.2: {}
+ virtua@0.33.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ optionalDependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ vite-tsconfig-paths@4.3.2(typescript@5.5.4)(vite@5.3.4):
+ dependencies:
+ debug: 4.3.5
+ globrex: 0.1.2
+ tsconfck: 3.1.1(typescript@5.5.4)
+ optionalDependencies:
+ vite: 5.3.4
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
vite@5.3.4:
dependencies:
esbuild: 0.21.5
diff --git a/src/App.css b/src/App.css
index b5c61c9..f72aac9 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,3 +1,92 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+html {
+ font-size: 14px;
+}
+
+a {
+ @apply cursor-default no-underline !important;
+}
+
+button {
+ @apply cursor-default focus:outline-none;
+}
+
+input::-ms-reveal,
+input::-ms-clear {
+ display: none;
+}
+
+::-webkit-input-placeholder {
+ line-height: normal;
+}
+
+.spinner-leaf {
+ position: absolute;
+ top: 0;
+ left: calc(50% - 12.5% / 2);
+ width: 12.5%;
+ height: 100%;
+ animation: spinner-leaf-fade 800ms linear infinite;
+
+ &::before {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 30%;
+ background-color: currentColor;
+ @apply rounded;
+ }
+
+ &:where(:nth-child(1)) {
+ transform: rotate(0deg);
+ animation-delay: -800ms;
+ }
+
+ &:where(:nth-child(2)) {
+ transform: rotate(45deg);
+ animation-delay: -700ms;
+ }
+
+ &:where(:nth-child(3)) {
+ transform: rotate(90deg);
+ animation-delay: -600ms;
+ }
+
+ &:where(:nth-child(4)) {
+ transform: rotate(135deg);
+ animation-delay: -500ms;
+ }
+
+ &:where(:nth-child(5)) {
+ transform: rotate(180deg);
+ animation-delay: -400ms;
+ }
+
+ &:where(:nth-child(6)) {
+ transform: rotate(225deg);
+ animation-delay: -300ms;
+ }
+
+ &:where(:nth-child(7)) {
+ transform: rotate(270deg);
+ animation-delay: -200ms;
+ }
+
+ &:where(:nth-child(8)) {
+ transform: rotate(315deg);
+ animation-delay: -100ms;
+ }
+}
+
+@keyframes spinner-leaf-fade {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0.25;
+ }
+}
diff --git a/src/commons.ts b/src/commons.ts
new file mode 100644
index 0000000..ab18028
--- /dev/null
+++ b/src/commons.ts
@@ -0,0 +1,23 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+export function npub(pubkey: string, len: number) {
+ if (pubkey.length <= len) return pubkey;
+
+ const separator = " ... ";
+
+ const sepLen = separator.length;
+ const charsToShow = len - sepLen;
+ const frontChars = Math.ceil(charsToShow / 2);
+ const backChars = Math.floor(charsToShow / 2);
+
+ return (
+ pubkey.substring(0, frontChars) +
+ separator +
+ pubkey.substring(pubkey.length - backChars)
+ );
+}
diff --git a/src/components/spinner.tsx b/src/components/spinner.tsx
new file mode 100644
index 0000000..3342142
--- /dev/null
+++ b/src/components/spinner.tsx
@@ -0,0 +1,47 @@
+import { cn } from "@/commons";
+import type { ReactNode } from "react";
+
+export function Spinner({
+ children,
+ className,
+}: {
+ children?: ReactNode;
+ className?: string;
+}) {
+ const spinner = (
+
+
+
+
+
+
+
+
+
+
+ );
+
+ if (children === undefined) return spinner;
+
+ return (
+
+
+ {/**
+ * `display: contents` removes the content from the accessibility tree in some browsers,
+ * so we force remove it with `aria-hidden`
+ */}
+
+ {children}
+
+
+ {spinner}
+
+
+
+ );
+}
diff --git a/src/components/user/about.tsx b/src/components/user/about.tsx
new file mode 100644
index 0000000..29ba682
--- /dev/null
+++ b/src/components/user/about.tsx
@@ -0,0 +1,12 @@
+import { cn } from "@/commons";
+import { useUserContext } from "./provider";
+
+export function UserAbout({ className }: { className?: string }) {
+ const user = useUserContext();
+
+ return (
+
+ {user.profile?.about?.trim() || "No bio"}
+
+ );
+}
diff --git a/src/components/user/avatar.tsx b/src/components/user/avatar.tsx
new file mode 100644
index 0000000..9c4e5bd
--- /dev/null
+++ b/src/components/user/avatar.tsx
@@ -0,0 +1,40 @@
+import { cn } from "@/commons";
+import * as Avatar from "@radix-ui/react-avatar";
+import { minidenticon } from "minidenticons";
+import { useMemo } from "react";
+import { useUserContext } from "./provider";
+
+export function UserAvatar({ className }: { className?: string }) {
+ const user = useUserContext();
+ const fallback = useMemo(
+ () =>
+ `data:image/svg+xml;utf8,${encodeURIComponent(
+ minidenticon(user.pubkey, 60, 50),
+ )}`,
+ [user.pubkey],
+ );
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/user/cover.tsx b/src/components/user/cover.tsx
new file mode 100644
index 0000000..0097b32
--- /dev/null
+++ b/src/components/user/cover.tsx
@@ -0,0 +1,36 @@
+import { cn } from "@/commons";
+import { useUserContext } from "./provider";
+
+export function UserCover({ className }: { className?: string }) {
+ const user = useUserContext();
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ if (user && !user.profile?.banner) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/user/index.ts b/src/components/user/index.ts
new file mode 100644
index 0000000..9bbe41d
--- /dev/null
+++ b/src/components/user/index.ts
@@ -0,0 +1,15 @@
+import { UserAbout } from "./about";
+import { UserAvatar } from "./avatar";
+import { UserCover } from "./cover";
+import { UserName } from "./name";
+import { UserProvider } from "./provider";
+import { UserRoot } from "./root";
+
+export const User = {
+ Provider: UserProvider,
+ Root: UserRoot,
+ Avatar: UserAvatar,
+ Cover: UserCover,
+ Name: UserName,
+ About: UserAbout,
+};
diff --git a/src/components/user/name.tsx b/src/components/user/name.tsx
new file mode 100644
index 0000000..0de6465
--- /dev/null
+++ b/src/components/user/name.tsx
@@ -0,0 +1,21 @@
+import { cn, npub } from "@/commons";
+import { useUserContext } from "./provider";
+
+export function UserName({
+ className,
+ prefix,
+}: {
+ className?: string;
+ prefix?: string;
+}) {
+ const user = useUserContext();
+
+ return (
+
+ {prefix}
+ {user.profile?.display_name ||
+ user.profile?.name ||
+ npub(user.pubkey, 16)}
+
+ );
+}
diff --git a/src/components/user/provider.tsx b/src/components/user/provider.tsx
new file mode 100644
index 0000000..995c2c6
--- /dev/null
+++ b/src/components/user/provider.tsx
@@ -0,0 +1,71 @@
+import { useQuery } from "@tanstack/react-query";
+import { invoke } from "@tauri-apps/api/core";
+import { type ReactNode, createContext, useContext } from "react";
+
+type Metadata = {
+ name?: string;
+ display_name?: string;
+ about?: string;
+ website?: string;
+ picture?: string;
+ banner?: string;
+ nip05?: string;
+ lud06?: string;
+ lud16?: string;
+};
+
+type UserContext = {
+ pubkey: string;
+ isLoading: boolean;
+ isError: boolean;
+ profile: Metadata | undefined;
+};
+
+const UserContext = createContext(null);
+
+export function UserProvider({
+ pubkey,
+ children,
+}: {
+ pubkey: string;
+ children: ReactNode;
+}) {
+ const {
+ isLoading,
+ isError,
+ data: profile,
+ } = useQuery({
+ queryKey: ["profile", pubkey],
+ queryFn: async () => {
+ try {
+ const normalizePubkey = pubkey
+ .replace("nostr:", "")
+ .replace(/[^\w\s]/gi, "");
+
+ const query: string = await invoke("get_profile", {
+ id: normalizePubkey,
+ });
+
+ return JSON.parse(query) as Metadata;
+ } catch (e) {
+ throw new Error(String(e));
+ }
+ },
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ staleTime: Number.POSITIVE_INFINITY,
+ retry: 2,
+ });
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useUserContext() {
+ const context = useContext(UserContext);
+ return context;
+}
diff --git a/src/components/user/root.tsx b/src/components/user/root.tsx
new file mode 100644
index 0000000..14887c1
--- /dev/null
+++ b/src/components/user/root.tsx
@@ -0,0 +1,12 @@
+import { cn } from "@/commons";
+import type { ReactNode } from "react";
+
+export function UserRoot({
+ children,
+ className,
+}: {
+ children: ReactNode;
+ className?: string;
+}) {
+ return {children}
;
+}
diff --git a/src/main.tsx b/src/main.tsx
index 56e2cae..180d554 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -3,11 +3,18 @@ import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import "./app.css";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Import the generated route tree
import { routeTree } from "./routes.gen";
// Create a new router instance
-const router = createRouter({ routeTree });
+const queryClient = new QueryClient();
+const router = createRouter({
+ routeTree,
+ context: {
+ queryClient,
+ },
+});
// Register the router instance for type safety
declare module "@tanstack/react-router" {
@@ -22,7 +29,9 @@ if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
-
+
+
+
,
);
}
diff --git a/src/routes.gen.ts b/src/routes.gen.ts
index e8428b4..9331b58 100644
--- a/src/routes.gen.ts
+++ b/src/routes.gen.ts
@@ -13,17 +13,29 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes
import { Route as rootRoute } from './routes/__root'
+import { Route as IndexImport } from './routes/index'
+import { Route as AccountChatsImport } from './routes/$account.chats'
// Create Virtual Routes
-const IndexLazyImport = createFileRoute('/')()
+const NewLazyImport = createFileRoute('/new')()
// Create/Update Routes
-const IndexLazyRoute = IndexLazyImport.update({
+const NewLazyRoute = NewLazyImport.update({
+ path: '/new',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
+
+const IndexRoute = IndexImport.update({
path: '/',
getParentRoute: () => rootRoute,
-} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
+} as any)
+
+const AccountChatsRoute = AccountChatsImport.update({
+ path: '/$account/chats',
+ getParentRoute: () => rootRoute,
+} as any)
// Populate the FileRoutesByPath interface
@@ -33,7 +45,21 @@ declare module '@tanstack/react-router' {
id: '/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof IndexLazyImport
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/new': {
+ id: '/new'
+ path: '/new'
+ fullPath: '/new'
+ preLoaderRoute: typeof NewLazyImport
+ parentRoute: typeof rootRoute
+ }
+ '/$account/chats': {
+ id: '/$account/chats'
+ path: '/$account/chats'
+ fullPath: '/$account/chats'
+ preLoaderRoute: typeof AccountChatsImport
parentRoute: typeof rootRoute
}
}
@@ -41,7 +67,11 @@ declare module '@tanstack/react-router' {
// Create and export the route tree
-export const routeTree = rootRoute.addChildren({ IndexLazyRoute })
+export const routeTree = rootRoute.addChildren({
+ IndexRoute,
+ NewLazyRoute,
+ AccountChatsRoute,
+})
/* prettier-ignore-end */
@@ -51,11 +81,19 @@ export const routeTree = rootRoute.addChildren({ IndexLazyRoute })
"__root__": {
"filePath": "__root.tsx",
"children": [
- "/"
+ "/",
+ "/new",
+ "/$account/chats"
]
},
"/": {
- "filePath": "index.lazy.tsx"
+ "filePath": "index.tsx"
+ },
+ "/new": {
+ "filePath": "new.lazy.tsx"
+ },
+ "/$account/chats": {
+ "filePath": "$account.chats.tsx"
}
}
}
diff --git a/src/routes/$account.chats.tsx b/src/routes/$account.chats.tsx
new file mode 100644
index 0000000..23f1026
--- /dev/null
+++ b/src/routes/$account.chats.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/$account/chats')({
+ component: () => Hello /$account/chats!
+})
\ No newline at end of file
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 143143f..fb5f5ae 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,5 +1,10 @@
-import { Outlet, createRootRoute } from "@tanstack/react-router";
+import type { QueryClient } from "@tanstack/react-query";
+import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
-export const Route = createRootRoute({
+interface RouterContext {
+ queryClient: QueryClient;
+}
+
+export const Route = createRootRouteWithContext()({
component: () => ,
});
diff --git a/src/routes/index.lazy.tsx b/src/routes/index.lazy.tsx
deleted file mode 100644
index e9aa002..0000000
--- a/src/routes/index.lazy.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { createLazyFileRoute } from '@tanstack/react-router'
-
-export const Route = createLazyFileRoute('/')({
- component: () => Hello /!
-})
\ No newline at end of file
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 0000000..61e2d30
--- /dev/null
+++ b/src/routes/index.tsx
@@ -0,0 +1,111 @@
+import { npub } from "@/commons";
+import { Spinner } from "@/components/spinner";
+import { User } from "@/components/user";
+import { Plus } from "@phosphor-icons/react";
+import { Link, createFileRoute, redirect } from "@tanstack/react-router";
+import { invoke } from "@tauri-apps/api/core";
+import { useMemo, useState } from "react";
+
+export const Route = createFileRoute("/")({
+ beforeLoad: async () => {
+ const accounts: string[] = await invoke("get_accounts");
+
+ if (!accounts.length) {
+ throw redirect({
+ to: "/new",
+ replace: true,
+ });
+ }
+
+ return { accounts };
+ },
+ component: Screen,
+});
+
+function Screen() {
+ const context = Route.useRouteContext();
+ const navigate = Route.useNavigate();
+
+ const currentDate = useMemo(
+ () =>
+ new Date().toLocaleString("default", {
+ weekday: "long",
+ month: "long",
+ day: "numeric",
+ }),
+ [],
+ );
+
+ const [loading, setLoading] = useState({ npub: "", status: false });
+
+ const login = async (npub: string) => {
+ try {
+ setLoading({ npub, status: true });
+
+ const status = await invoke("login", { id: npub });
+
+ if (status) {
+ return navigate({
+ to: "/$account/chats",
+ params: { account: npub },
+ replace: true,
+ });
+ }
+ } catch (e) {
+ setLoading({ npub: "", status: false });
+ }
+ };
+
+ return (
+
+
+
+
+ {currentDate}
+
+ Welcome back!
+
+
+ {context.accounts.map((account) => (
+
login(account)}
+ onKeyDown={() => login(account)}
+ className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
+ >
+
+
+
+
+
+
+ {npub(account, 16)}
+
+
+
+
+
+ {loading.npub === account && loading.status ? (
+
+ ) : null}
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/routes/new.lazy.tsx b/src/routes/new.lazy.tsx
new file mode 100644
index 0000000..4344d2f
--- /dev/null
+++ b/src/routes/new.lazy.tsx
@@ -0,0 +1,5 @@
+import { createLazyFileRoute } from '@tanstack/react-router'
+
+export const Route = createLazyFileRoute('/new')({
+ component: () => Hello /new!
+})
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index a7fc6fb..3b5fd27 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,10 +2,19 @@
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": [
+ "ES2020",
+ "DOM",
+ "DOM.Iterable"
+ ],
"module": "ESNext",
"skipLibCheck": true,
-
+ "baseUrl": "./",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ },
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -13,13 +22,18 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
-
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["src"],
- "references": [{ "path": "./tsconfig.node.json" }]
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
}
diff --git a/vite.config.ts b/vite.config.ts
index 38a164c..00a70cd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,11 @@
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig(async () => ({
- plugins: [TanStackRouterVite(), react()],
+ plugins: [TanStackRouterVite(), tsconfigPaths(), react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//