mirror of
https://github.com/emailerfacu-spec/minix-front.git
synced 2026-04-01 13:10:44 -03:00
@@ -1,4 +1,5 @@
|
||||
# Package Managers
|
||||
src/lib/components/ui/
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
|
||||
53
README.md
53
README.md
@@ -1,38 +1,23 @@
|
||||
# sv
|
||||
# Minix - Front
|
||||
Este repositorio consiste del repo que contiene el codigo para poder hacer deploy de una instancia de minix
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
# ¿Que es Minix?
|
||||
Intentamos hacer algo parecido a x.com pero adaptado a nuestra vision.
|
||||
|
||||
## Creating a project
|
||||
# Galeria
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/2ebe2983-04dc-4cca-ab46-e361ec6f72ee" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/49529256-2c36-4a40-bbab-d02228028def" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/38045721-c350-4e06-a5e1-4aa271139894" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/5c228110-feac-4d52-8451-222f04034f94" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/9618162d-fcde-42ff-bd7e-fe334498e9c7" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/580f0ad3-89ed-4686-98c2-f8dea04f80de" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/ce40fd1a-bac0-443f-b01e-d861c335236c" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/ea24be47-f35a-4d68-a9d6-f6796e656911" />
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/adf1f792-31e0-4f10-a4f3-e8590b4f0582" />
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
# ¿Que tecnologias usamos?
|
||||
- svelte(kit) (framework)
|
||||
- shadcn-svelte (ui)
|
||||
- firebase/auth
|
||||
- vercel (host)
|
||||
|
||||
```sh
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
|
||||
176
bun.lock
176
bun.lock
@@ -5,7 +5,9 @@
|
||||
"": {
|
||||
"name": "mini-x-front",
|
||||
"dependencies": {
|
||||
"@firebase/auth": "^1.12.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"firebase": "^12.8.0",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"satori": "^0.18.3",
|
||||
},
|
||||
@@ -85,12 +87,104 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@firebase/ai": ["@firebase/ai@2.7.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-types": "0.x" } }, "sha512-PwpCz+TtAMWICM7uQNO0mkSPpUKwrMV4NSwHkbVKDvPKoaQmSlO96vIz+Suw2Ao1EaUUsxYb5LGImHWt/fSnRQ=="],
|
||||
|
||||
"@firebase/analytics": ["@firebase/analytics@0.10.19", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg=="],
|
||||
|
||||
"@firebase/analytics-compat": ["@firebase/analytics-compat@0.2.25", "", { "dependencies": { "@firebase/analytics": "0.10.19", "@firebase/analytics-types": "0.8.3", "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q=="],
|
||||
|
||||
"@firebase/analytics-types": ["@firebase/analytics-types@0.8.3", "", {}, "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg=="],
|
||||
|
||||
"@firebase/app": ["@firebase/app@0.14.7", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "idb": "7.1.1", "tslib": "^2.1.0" } }, "sha512-o3ZfnOx0AWBD5n/36p2zPoB0rDDxQP8H/A60zDLvvfRLtW8b3LfCyV97GKpJaAVV1JMMl/BC89EDzMyzxFZxTw=="],
|
||||
|
||||
"@firebase/app-check": ["@firebase/app-check@0.11.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w=="],
|
||||
|
||||
"@firebase/app-check-compat": ["@firebase/app-check-compat@0.4.0", "", { "dependencies": { "@firebase/app-check": "0.11.0", "@firebase/app-check-types": "0.5.3", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g=="],
|
||||
|
||||
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.3", "", {}, "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A=="],
|
||||
|
||||
"@firebase/app-check-types": ["@firebase/app-check-types@0.5.3", "", {}, "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng=="],
|
||||
|
||||
"@firebase/app-compat": ["@firebase/app-compat@0.5.7", "", { "dependencies": { "@firebase/app": "0.14.7", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-MO+jfap8IBZQ+K8L2QCiHObyMgpYHrxo4Hc7iJgfb9hjGRW/z1y6LWVdT9wBBK+VJ7cRP2DjAiWQP+thu53hHA=="],
|
||||
|
||||
"@firebase/app-types": ["@firebase/app-types@0.9.3", "", {}, "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="],
|
||||
|
||||
"@firebase/auth": ["@firebase/auth@1.12.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@react-native-async-storage/async-storage": "^2.2.0" }, "optionalPeers": ["@react-native-async-storage/async-storage"] }, "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA=="],
|
||||
|
||||
"@firebase/auth-compat": ["@firebase/auth-compat@0.6.2", "", { "dependencies": { "@firebase/auth": "1.12.0", "@firebase/auth-types": "0.13.0", "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-8UhCzF6pav9bw/eXA8Zy1QAKssPRYEYXaWagie1ewLTwHkXv6bKp/j6/IwzSYQP67sy/BMFXIFaCCsoXzFLr7A=="],
|
||||
|
||||
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.4", "", {}, "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA=="],
|
||||
|
||||
"@firebase/auth-types": ["@firebase/auth-types@0.13.0", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg=="],
|
||||
|
||||
"@firebase/component": ["@firebase/component@0.7.0", "", { "dependencies": { "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg=="],
|
||||
|
||||
"@firebase/data-connect": ["@firebase/data-connect@0.3.12", "", { "dependencies": { "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g=="],
|
||||
|
||||
"@firebase/database": ["@firebase/database@1.1.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg=="],
|
||||
|
||||
"@firebase/database-compat": ["@firebase/database-compat@2.1.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/database": "1.1.0", "@firebase/database-types": "1.0.16", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg=="],
|
||||
|
||||
"@firebase/database-types": ["@firebase/database-types@1.0.16", "", { "dependencies": { "@firebase/app-types": "0.9.3", "@firebase/util": "1.13.0" } }, "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw=="],
|
||||
|
||||
"@firebase/firestore": ["@firebase/firestore@4.10.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "@firebase/webchannel-wrapper": "1.0.5", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-fgF6EbpoagGWh5Vwfu/7/jYgBFwUCwTlPNVF/aSjHcoEDRXpRsIqVfAFTp1LD+dWAUcAKEK3h+osk8spMJXtxA=="],
|
||||
|
||||
"@firebase/firestore-compat": ["@firebase/firestore-compat@0.4.4", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/firestore": "4.10.0", "@firebase/firestore-types": "3.0.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-JvxxIgi+D5v9BecjLA1YomdyF7LA6CXhJuVK10b4GtRrB3m2O2hT1jJWbKYZYHUAjTaajkvnos+4U5VNxqkI2w=="],
|
||||
|
||||
"@firebase/firestore-types": ["@firebase/firestore-types@3.0.3", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q=="],
|
||||
|
||||
"@firebase/functions": ["@firebase/functions@0.13.1", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/messaging-interop-types": "0.2.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw=="],
|
||||
|
||||
"@firebase/functions-compat": ["@firebase/functions-compat@0.4.1", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/functions": "0.13.1", "@firebase/functions-types": "0.6.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ=="],
|
||||
|
||||
"@firebase/functions-types": ["@firebase/functions-types@0.6.3", "", {}, "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg=="],
|
||||
|
||||
"@firebase/installations": ["@firebase/installations@0.6.19", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q=="],
|
||||
|
||||
"@firebase/installations-compat": ["@firebase/installations-compat@0.2.19", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/installations-types": "0.5.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ=="],
|
||||
|
||||
"@firebase/installations-types": ["@firebase/installations-types@0.5.3", "", { "peerDependencies": { "@firebase/app-types": "0.x" } }, "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA=="],
|
||||
|
||||
"@firebase/logger": ["@firebase/logger@0.5.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g=="],
|
||||
|
||||
"@firebase/messaging": ["@firebase/messaging@0.12.23", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/messaging-interop-types": "0.2.3", "@firebase/util": "1.13.0", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg=="],
|
||||
|
||||
"@firebase/messaging-compat": ["@firebase/messaging-compat@0.2.23", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/messaging": "0.12.23", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg=="],
|
||||
|
||||
"@firebase/messaging-interop-types": ["@firebase/messaging-interop-types@0.2.3", "", {}, "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q=="],
|
||||
|
||||
"@firebase/performance": ["@firebase/performance@0.7.9", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0", "web-vitals": "^4.2.4" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ=="],
|
||||
|
||||
"@firebase/performance-compat": ["@firebase/performance-compat@0.2.22", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/performance": "0.7.9", "@firebase/performance-types": "0.2.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg=="],
|
||||
|
||||
"@firebase/performance-types": ["@firebase/performance-types@0.2.3", "", {}, "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ=="],
|
||||
|
||||
"@firebase/remote-config": ["@firebase/remote-config@0.8.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/installations": "0.6.19", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ=="],
|
||||
|
||||
"@firebase/remote-config-compat": ["@firebase/remote-config-compat@0.2.21", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/remote-config": "0.8.0", "@firebase/remote-config-types": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw=="],
|
||||
|
||||
"@firebase/remote-config-types": ["@firebase/remote-config-types@0.5.0", "", {}, "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg=="],
|
||||
|
||||
"@firebase/storage": ["@firebase/storage@0.14.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA=="],
|
||||
|
||||
"@firebase/storage-compat": ["@firebase/storage-compat@0.4.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/storage": "0.14.0", "@firebase/storage-types": "0.8.3", "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g=="],
|
||||
|
||||
"@firebase/storage-types": ["@firebase/storage-types@0.8.3", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg=="],
|
||||
|
||||
"@firebase/util": ["@firebase/util@1.13.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ=="],
|
||||
|
||||
"@firebase/webchannel-wrapper": ["@firebase/webchannel-wrapper@1.0.5", "", {}, "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@grpc/grpc-js": ["@grpc/grpc-js@1.9.15", "", { "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" } }, "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ=="],
|
||||
|
||||
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
|
||||
|
||||
"@internationalized/date": ["@internationalized/date@3.10.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
@@ -115,6 +209,26 @@
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||
|
||||
"@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="],
|
||||
|
||||
"@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="],
|
||||
@@ -275,6 +389,8 @@
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
@@ -309,7 +425,7 @@
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"emoji-regex-xs": ["emoji-regex-xs@2.0.1", "", {}, "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g=="],
|
||||
|
||||
@@ -317,6 +433,8 @@
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
@@ -325,24 +443,34 @@
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"faye-websocket": ["faye-websocket@0.11.4", "", { "dependencies": { "websocket-driver": ">=0.5.1" } }, "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"firebase": ["firebase@12.8.0", "", { "dependencies": { "@firebase/ai": "2.7.0", "@firebase/analytics": "0.10.19", "@firebase/analytics-compat": "0.2.25", "@firebase/app": "0.14.7", "@firebase/app-check": "0.11.0", "@firebase/app-check-compat": "0.4.0", "@firebase/app-compat": "0.5.7", "@firebase/app-types": "0.9.3", "@firebase/auth": "1.12.0", "@firebase/auth-compat": "0.6.2", "@firebase/data-connect": "0.3.12", "@firebase/database": "1.1.0", "@firebase/database-compat": "2.1.0", "@firebase/firestore": "4.10.0", "@firebase/firestore-compat": "0.4.4", "@firebase/functions": "0.13.1", "@firebase/functions-compat": "0.4.1", "@firebase/installations": "0.6.19", "@firebase/installations-compat": "0.2.19", "@firebase/messaging": "0.12.23", "@firebase/messaging-compat": "0.2.23", "@firebase/performance": "0.7.9", "@firebase/performance-compat": "0.2.22", "@firebase/remote-config": "0.8.0", "@firebase/remote-config-compat": "0.2.21", "@firebase/storage": "0.14.0", "@firebase/storage-compat": "0.4.0", "@firebase/util": "1.13.0" } }, "sha512-S1tCIR3ENecee0tY2cfTHfMkXqkitHfbsvqpCtvsT0Zi9vDB7A4CodAjHfHCjVvu/XtGy1LHLjOasVcF10rCVw=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hex-rgb": ["hex-rgb@4.3.0", "", {}, "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw=="],
|
||||
|
||||
"http-parser-js": ["http-parser-js@0.5.10", "", {}, "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.6", "", {}, "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
@@ -385,6 +513,10 @@
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
@@ -437,8 +569,12 @@
|
||||
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||
|
||||
"rollup": ["rollup@4.53.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.2", "@rollup/rollup-android-arm64": "4.53.2", "@rollup/rollup-darwin-arm64": "4.53.2", "@rollup/rollup-darwin-x64": "4.53.2", "@rollup/rollup-freebsd-arm64": "4.53.2", "@rollup/rollup-freebsd-x64": "4.53.2", "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", "@rollup/rollup-linux-arm-musleabihf": "4.53.2", "@rollup/rollup-linux-arm64-gnu": "4.53.2", "@rollup/rollup-linux-arm64-musl": "4.53.2", "@rollup/rollup-linux-loong64-gnu": "4.53.2", "@rollup/rollup-linux-ppc64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-musl": "4.53.2", "@rollup/rollup-linux-s390x-gnu": "4.53.2", "@rollup/rollup-linux-x64-gnu": "4.53.2", "@rollup/rollup-linux-x64-musl": "4.53.2", "@rollup/rollup-openharmony-arm64": "4.53.2", "@rollup/rollup-win32-arm64-msvc": "4.53.2", "@rollup/rollup-win32-ia32-msvc": "4.53.2", "@rollup/rollup-win32-x64-gnu": "4.53.2", "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g=="],
|
||||
@@ -447,6 +583,8 @@
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"satori": ["satori@0.18.3", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.17", "css-to-react-native": "^3.0.0", "emoji-regex-xs": "^2.0.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-layout": "^3.2.1" } }, "sha512-T3DzWNmnrfVmk2gCIlAxLRLbGkfp3K7TyRva+Byyojqu83BNvnMeqVeYRdmUw4TKCsyH4RiQ/KuF/I4yEzgR5A=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
@@ -463,7 +601,7 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
@@ -515,8 +653,14 @@
|
||||
|
||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
|
||||
"web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"websocket-driver": ["websocket-driver@0.7.4", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="],
|
||||
|
||||
"websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
@@ -525,12 +669,20 @@
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q=="],
|
||||
@@ -543,28 +695,40 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
|
||||
|
||||
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
"vite": "^7.1.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@firebase/auth": "^1.12.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"firebase": "^12.8.0",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"satori": "^0.18.3"
|
||||
}
|
||||
|
||||
@@ -16,16 +16,18 @@
|
||||
let {
|
||||
post,
|
||||
variant = 'icon-lg'
|
||||
}: { post: Omit<Partial<Post>, 'authorId'> & { authorId: string }; variant?: string } = $props();
|
||||
}: {
|
||||
post: Omit<Partial<Post>, 'authorId'> & { authorId: string; id: string };
|
||||
variant?: 'icon-lg' | 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm';
|
||||
} = $props();
|
||||
|
||||
let seguido: Boolean | null = $state(null);
|
||||
let seguido: boolean | null = $state(null);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('followCacheUpdated', ((
|
||||
event: CustomEvent<{ userId: string; isFollowed: boolean } | { clearAll: true }>
|
||||
) => {
|
||||
if ('clearAll' in event.detail && event.detail.clearAll === true) {
|
||||
cargarSeguido();
|
||||
} else if ('userId' in event.detail && event.detail.userId === post.authorId) {
|
||||
seguido = event.detail.isFollowed;
|
||||
}
|
||||
@@ -41,9 +43,11 @@
|
||||
async function cargarSeguido() {
|
||||
let a = cacheSeguidos.get(post.authorId);
|
||||
if (a === undefined) {
|
||||
const seguidoStatus = await esSeguido(post);
|
||||
cacheSeguidos.set(post.authorId, seguidoStatus.isFollowing || false);
|
||||
seguido = seguidoStatus.isFollowing || false;
|
||||
const seguidoStatus = await esSeguido(post as Post);
|
||||
if (seguidoStatus) {
|
||||
cacheSeguidos.set(post.authorId, seguidoStatus.isFollowing || false);
|
||||
seguido = seguidoStatus.isFollowing || false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
seguido = a;
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
import Badge from './ui/badge/badge.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Tooltip } from './ui/tooltip';
|
||||
import TooltipTrigger from './ui/tooltip/tooltip-trigger.svelte';
|
||||
import TooltipContent from './ui/tooltip/tooltip-content.svelte';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
@@ -101,10 +104,10 @@
|
||||
</div>
|
||||
|
||||
<h1 class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl">
|
||||
{usu.displayName}
|
||||
{data.displayName}
|
||||
<p class="ml-2 text-2xl font-medium text-muted-foreground">@{data.username}</p>
|
||||
</h1>
|
||||
{#if usu.bio}
|
||||
{#if data.bio}
|
||||
<p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground">
|
||||
{@html contenido()}
|
||||
<!-- {usu.bio.replaceAll('<', '')} -->
|
||||
@@ -126,10 +129,10 @@
|
||||
</Avatar>
|
||||
</div>
|
||||
<h1 class="mt-10 scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-5xl">
|
||||
{usu.displayName}
|
||||
{data.displayName}
|
||||
<p class="ml-2 text-2xl font-medium text-muted-foreground">@{data.username}</p>
|
||||
</h1>
|
||||
{#if usu.bio}
|
||||
{#if data.bio}
|
||||
<p class="mt-4 rounded-4xl bg-accent p-4 text-center text-muted-foreground">
|
||||
{@html usu.bio.replaceAll('\n', '<br>')}
|
||||
</p>
|
||||
@@ -151,14 +154,30 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex -space-x-2">
|
||||
{#each data.seguidos.response as seguidos (seguidos.id)}
|
||||
<a href={resolve('/[perfil]', { perfil: seguidos.username })}>
|
||||
<Avatar class="h-8 w-8 border-2 border-background">
|
||||
<AvatarImage src={seguidos.imageUrl} alt={seguidos.username} />
|
||||
<AvatarFallback class="text-xs">
|
||||
{seguidos.displayName?.[0] || ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</a>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<a href={resolve('/[perfil]', { perfil: seguidos.username })}>
|
||||
<Avatar class="h-8 w-8 border-2 border-background">
|
||||
<AvatarImage src={seguidos.imageUrl} alt={seguidos.username} />
|
||||
<AvatarFallback class="text-xs">
|
||||
{seguidos.displayName?.[0] || ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span class="flex items-center gap-2">
|
||||
<Avatar class="h-8 w-8 border-2 border-background">
|
||||
<AvatarImage src={seguidos.imageUrl} alt={seguidos.username} />
|
||||
<AvatarFallback class="text-xs">
|
||||
{seguidos.displayName?.[0] || ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span class="text-lg">{seguidos.displayName}</span>
|
||||
<span class="text-lg text-muted-foreground">@{seguidos.username}</span>
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
{#if data.seguidos.response?.length < data.countSeguidos}
|
||||
@@ -188,14 +207,30 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex -space-x-2">
|
||||
{#each data.seguidores.response as seguidores (seguidores.id)}
|
||||
<a href={resolve('/[perfil]', { perfil: seguidores.username })}>
|
||||
<Avatar class="h-8 w-8 border-2 border-background">
|
||||
<AvatarImage src={seguidores.imageUrl} alt={seguidores.username} />
|
||||
<AvatarFallback class="text-xs">
|
||||
{seguidores.displayName?.[0] || ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</a>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<a href={resolve('/[perfil]', { perfil: seguidores.username })}>
|
||||
<Avatar class="h-8 w-8 border-2 border-background">
|
||||
<AvatarImage src={seguidores.imageUrl} alt={seguidores.username} />
|
||||
<AvatarFallback class="text-xs">
|
||||
{seguidores.displayName?.[0] || ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span class="flex items-center gap-2">
|
||||
<Avatar class="h-8 w-8 border-2 border-background">
|
||||
<AvatarImage src={seguidores.imageUrl} alt={seguidores.username} />
|
||||
<AvatarFallback class="text-xs">
|
||||
{seguidores.displayName?.[0] || ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span class="text-lg">{seguidores.displayName}</span>
|
||||
<span class="text-lg text-muted-foreground">@{seguidores.username}</span>
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
{#if data.seguidores.response?.length < data.countSeguidores}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import UserPen from '@lucide/svelte/icons/user-pen';
|
||||
import Button, { buttonVariants } from './ui/button/button.svelte';
|
||||
import { Dialog } from './ui/dialog';
|
||||
import DialogTrigger from './ui/dialog/dialog-trigger.svelte';
|
||||
@@ -18,7 +17,7 @@
|
||||
import { invalidate, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
let { data = $bindable(), children } = $props();
|
||||
|
||||
let usuario: UserResponseDto = $state({
|
||||
id: data.id,
|
||||
@@ -43,9 +42,9 @@
|
||||
});
|
||||
cargando = false;
|
||||
open = false;
|
||||
// invalidateAll();
|
||||
await invalidate(page.url);
|
||||
await invalidate('perfil:general');
|
||||
await invalidateAll();
|
||||
// await invalidate(page.url);
|
||||
// await invalidate('perfil:general');
|
||||
}
|
||||
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
@@ -57,14 +56,10 @@
|
||||
|
||||
<Dialog bind:open>
|
||||
<DialogTrigger>
|
||||
<div class="fixed right-8 bottom-8">
|
||||
<Button variant="default" size="icon-lg">
|
||||
<UserPen />
|
||||
</Button>
|
||||
</div>
|
||||
{@render children?.()}
|
||||
</DialogTrigger>
|
||||
<form {onsubmit}>
|
||||
<DialogContent>
|
||||
<DialogContent>
|
||||
<form {onsubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<h1 class="text-2xl font-bold">Modificar Usuario</h1>
|
||||
@@ -79,20 +74,14 @@
|
||||
<FieldLabel>bio</FieldLabel>
|
||||
<Textarea id="bio" bind:value={usuario.bio}></Textarea>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Email</FieldLabel>
|
||||
<Input id="email" type="email" bind:value={usuario.email} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<DialogFooter>
|
||||
<Button type="submit">
|
||||
{#if cargando}
|
||||
<Spinner />
|
||||
{:else}
|
||||
Modificar
|
||||
{/if}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</form>
|
||||
<Button type="submit" class="mt-2">
|
||||
{#if cargando}
|
||||
<Spinner />
|
||||
{:else}
|
||||
Modificar
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
115
src/lib/components/DialogResetPassword.svelte
Normal file
115
src/lib/components/DialogResetPassword.svelte
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import Button from './ui/button/button.svelte';
|
||||
import { Dialog } from './ui/dialog';
|
||||
import DialogTrigger from './ui/dialog/dialog-trigger.svelte';
|
||||
import DialogContent from './ui/dialog/dialog-content.svelte';
|
||||
import DialogHeader from './ui/dialog/dialog-header.svelte';
|
||||
import DialogTitle from './ui/dialog/dialog-title.svelte';
|
||||
import Input from './ui/input/input.svelte';
|
||||
import Field from './ui/field/field.svelte';
|
||||
import FieldLabel from './ui/field/field-label.svelte';
|
||||
import FieldGroup from './ui/field/field-group.svelte';
|
||||
import DialogFooter from './ui/dialog/dialog-footer.svelte';
|
||||
import Spinner from './ui/spinner/spinner.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { cambiarContraseñaUsuario } from '@/hooks/cambiarContraseñaUsuario';
|
||||
|
||||
let { data = $bindable(), children } = $props();
|
||||
|
||||
let passwordData = $state({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
let cargando = $state(false);
|
||||
let open = $state(false);
|
||||
|
||||
let mensajeError = $state('');
|
||||
|
||||
const coinsiden = $derived(
|
||||
passwordData.newPassword === passwordData.confirmPassword &&
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9])[A-Za-z\d\W_]*$/.test(
|
||||
passwordData.newPassword
|
||||
) &&
|
||||
passwordData.newPassword.length > 8
|
||||
);
|
||||
|
||||
async function onsubmit(e: SubmitEvent | null) {
|
||||
if (e != null) e.preventDefault();
|
||||
if (!coinsiden) {
|
||||
mensajeError = 'Las contraseñas no coinciden o no cumplen con los requisitos';
|
||||
return;
|
||||
}
|
||||
cargando = true;
|
||||
try {
|
||||
await cambiarContraseñaUsuario(passwordData.oldPassword, passwordData.newPassword, data.id);
|
||||
cargando = false;
|
||||
open = false;
|
||||
passwordData.oldPassword = '';
|
||||
passwordData.newPassword = '';
|
||||
passwordData.confirmPassword = '';
|
||||
await invalidate(page.url);
|
||||
} catch (error) {
|
||||
cargando = false;
|
||||
mensajeError = 'Error al cambiar la contraseña';
|
||||
}
|
||||
}
|
||||
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
onsubmit(null);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog bind:open>
|
||||
<DialogTrigger>
|
||||
{@render children?.()}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<form {onsubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<h1 class="text-2xl font-bold">Resetear Contraseña</h1>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{#if mensajeError}
|
||||
<div class="mb-4 rounded bg-red-100 p-4 text-red-700">
|
||||
{mensajeError}
|
||||
</div>
|
||||
{/if}
|
||||
<FieldGroup {onkeydown}>
|
||||
<Field>
|
||||
<FieldLabel>Contraseña Actual</FieldLabel>
|
||||
<Input id="oldPassword" type="password" bind:value={passwordData.oldPassword} required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Nueva Contraseña</FieldLabel>
|
||||
<Input id="newPassword" type="password" bind:value={passwordData.newPassword} required />
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
La contraseña debe contener al menos una mayúscula, una minúscula, un número y un
|
||||
carácter especial. Además de 8 caracteres de longitud.
|
||||
</p>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Confirmar Contraseña</FieldLabel>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
bind:value={passwordData.confirmPassword}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Button type="submit" class="mt-6" disabled={!coinsiden || cargando}>
|
||||
{#if cargando}
|
||||
<Spinner />
|
||||
{:else}
|
||||
Resetear Contraseña
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
205
src/lib/components/FireBaseButton.svelte
Normal file
205
src/lib/components/FireBaseButton.svelte
Normal file
@@ -0,0 +1,205 @@
|
||||
<script>
|
||||
import { getFirebaseAuth } from '$lib/stores/firebase.ts';
|
||||
import { loginFirebase } from '@/hooks/loginFirebase';
|
||||
import { register } from '@/hooks/register';
|
||||
import { registerFirebase } from '@/hooks/registerFirebase';
|
||||
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
||||
|
||||
let { mode } = $props();
|
||||
|
||||
const handleGoogleSignIn = async () => {
|
||||
const auth = getFirebaseAuth();
|
||||
const provider = new GoogleAuthProvider();
|
||||
let ret;
|
||||
try {
|
||||
switch (mode) {
|
||||
case 'register':
|
||||
ret = await signInWithPopup(auth, provider);
|
||||
|
||||
/** @type {import('../../types').RegisterSsoDto} */
|
||||
let dto2 = {
|
||||
displayName: ret.user.displayName || '',
|
||||
email: ret.user.email || '',
|
||||
token: ret.user.accessToken || '',
|
||||
uid: ret.user.uid,
|
||||
username: ret.user.displayName?.replaceAll(' ', '_') || ''
|
||||
};
|
||||
registerFirebase(dto2, () => {});
|
||||
|
||||
break;
|
||||
case 'login':
|
||||
ret = await signInWithPopup(auth, provider);
|
||||
/** @type {import('../../types').LoginSsoDto} */
|
||||
let dto = {
|
||||
uid: ret.user.uid,
|
||||
accessToken: ret.user.accessToken
|
||||
};
|
||||
// console.log(ret);
|
||||
await loginFirebase(dto, () => {});
|
||||
// console.log(dto);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown mode:', mode);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error signing in with Google:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script src="https://accounts.google.com/gsi/client" async></script>
|
||||
</svelte:head>
|
||||
|
||||
<button class="gsi-material-button w-full!" onclick={handleGoogleSignIn}>
|
||||
<div class="gsi-material-button-state"></div>
|
||||
<div class="gsi-material-button-content-wrapper">
|
||||
<div class="gsi-material-button-icon">
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 48 48"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
style="display: block;"
|
||||
>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
|
||||
></path>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
|
||||
></path>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
|
||||
></path>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
|
||||
></path>
|
||||
<path fill="none" d="M0 0h48v48H0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="gsi-material-button-contents">Continue with Google</span>
|
||||
<span style="display: none;">Continue with Google</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.gsi-material-button {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
user-select: none;
|
||||
background-color: WHITE;
|
||||
background-image: none;
|
||||
border: 1px solid #747775;
|
||||
-webkit-border-radius: 20px;
|
||||
border-radius: 20px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: #1f1f1f;
|
||||
cursor: pointer;
|
||||
font-family: 'Roboto', arial, sans-serif;
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
letter-spacing: 0.25px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
-webkit-transition:
|
||||
background-color 0.218s,
|
||||
border-color 0.218s,
|
||||
box-shadow 0.218s;
|
||||
transition:
|
||||
background-color 0.218s,
|
||||
border-color 0.218s,
|
||||
box-shadow 0.218s;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
.gsi-material-button .gsi-material-button-icon {
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.gsi-material-button .gsi-material-button-content-wrapper {
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-flex-wrap: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gsi-material-button .gsi-material-button-contents {
|
||||
-webkit-flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
font-family: 'Roboto', arial, sans-serif;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.gsi-material-button .gsi-material-button-state {
|
||||
-webkit-transition: opacity 0.218s;
|
||||
transition: opacity 0.218s;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.gsi-material-button:disabled {
|
||||
cursor: default;
|
||||
background-color: #ffffff61;
|
||||
border-color: #1f1f1f1f;
|
||||
}
|
||||
|
||||
.gsi-material-button:disabled .gsi-material-button-contents {
|
||||
opacity: 38%;
|
||||
}
|
||||
|
||||
.gsi-material-button:disabled .gsi-material-button-icon {
|
||||
opacity: 38%;
|
||||
}
|
||||
|
||||
.gsi-material-button:not(:disabled):active .gsi-material-button-state,
|
||||
.gsi-material-button:not(:disabled):focus .gsi-material-button-state {
|
||||
background-color: #303030;
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
.gsi-material-button:not(:disabled):hover {
|
||||
-webkit-box-shadow:
|
||||
0 1px 2px 0 rgba(60, 64, 67, 0.3),
|
||||
0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
||||
box-shadow:
|
||||
0 1px 2px 0 rgba(60, 64, 67, 0.3),
|
||||
0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
||||
}
|
||||
|
||||
.gsi-material-button:not(:disabled):hover .gsi-material-button-state {
|
||||
background-color: #303030;
|
||||
opacity: 8%;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
import Button from './ui/button/button.svelte';
|
||||
import KeyIcon from '@lucide/svelte/icons/key';
|
||||
import UserPen from '@lucide/svelte/icons/user-pen';
|
||||
import Shield from '@lucide/svelte/icons/shield';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import { Tooltip } from './ui/tooltip';
|
||||
@@ -27,6 +28,8 @@
|
||||
import InputGroupAddon from './ui/input-group/input-group-addon.svelte';
|
||||
import InputGroupInput from './ui/input-group/input-group-input.svelte';
|
||||
import AgregarUsuario from './admin/AgregarUsuario.svelte';
|
||||
import DarAdmin from './admin/DarAdmin.svelte';
|
||||
import { busquedaAdminUsuarios } from '@/hooks/busquedaAdminUsuarios';
|
||||
|
||||
interface Props {
|
||||
usuarios: UserResponseDto[];
|
||||
@@ -36,14 +39,14 @@
|
||||
|
||||
let open = $state(false);
|
||||
let openModificarUsuario = $state(false);
|
||||
|
||||
let openDarAdmin = $state(false);
|
||||
let openBorrar = $state(false);
|
||||
let opencrearUsuario = $state(false);
|
||||
|
||||
let usuarioBorrar: UserResponseDto | null = $state(null);
|
||||
|
||||
//si ponia contraseña en español quedaba muy largo el nombre
|
||||
let usuarioCambioPass: UserResponseDto | null = $state(null);
|
||||
|
||||
let usuarioModificar: UserResponseDto | null = $state(null);
|
||||
let usuarioDarAdmin: UserResponseDto | null = $state(null);
|
||||
|
||||
let search = $state('');
|
||||
|
||||
@@ -51,6 +54,8 @@
|
||||
let sortBy = $state<SortKey | null>(null);
|
||||
let sortDirection = $state<'asc' | 'desc'>('asc');
|
||||
|
||||
let usuariosFiltrados = $state(usuarios);
|
||||
|
||||
function ordenarPor(campo: SortKey) {
|
||||
if (sortBy === campo) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
@@ -58,38 +63,27 @@
|
||||
sortBy = campo;
|
||||
sortDirection = 'asc';
|
||||
}
|
||||
usuariosFiltrados = usuariosFiltrados.toSorted((a, b) => {
|
||||
if (!sortBy) return 0;
|
||||
|
||||
const key: SortKey = sortBy;
|
||||
|
||||
if (key === 'createdAt') {
|
||||
const ta = new Date(a.createdAt).getTime();
|
||||
const tb = new Date(b.createdAt).getTime();
|
||||
return sortDirection === 'asc' ? ta - tb : tb - ta;
|
||||
}
|
||||
|
||||
if (key === 'postsCount') {
|
||||
return sortDirection === 'asc' ? a.postsCount - b.postsCount : b.postsCount - a.postsCount;
|
||||
}
|
||||
|
||||
const sa = a[key].toString().toLowerCase();
|
||||
const sb = b[key].toString().toLowerCase();
|
||||
return sortDirection === 'asc' ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}
|
||||
|
||||
let usuariosFiltrados = $derived(
|
||||
usuarios
|
||||
.filter(
|
||||
(u) =>
|
||||
u.username.toLowerCase().startsWith(search.toLowerCase()) ||
|
||||
u.displayName.toLowerCase().startsWith(search.toLowerCase())
|
||||
)
|
||||
.toSorted((a, b) => {
|
||||
if (!sortBy) return 0;
|
||||
|
||||
const key: SortKey = sortBy;
|
||||
|
||||
if (key === 'createdAt') {
|
||||
const ta = new Date(a.createdAt).getTime();
|
||||
const tb = new Date(b.createdAt).getTime();
|
||||
return sortDirection === 'asc' ? ta - tb : tb - ta;
|
||||
}
|
||||
|
||||
if (key === 'postsCount') {
|
||||
return sortDirection === 'asc'
|
||||
? a.postsCount - b.postsCount
|
||||
: b.postsCount - a.postsCount;
|
||||
}
|
||||
|
||||
const sa = a[key].toString().toLowerCase();
|
||||
const sb = b[key].toString().toLowerCase();
|
||||
return sortDirection === 'asc' ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
})
|
||||
);
|
||||
|
||||
function getSortIcon(campo: SortKey) {
|
||||
if (sortBy !== campo) return '';
|
||||
return sortDirection === 'asc' ? '↑' : '↓';
|
||||
@@ -110,16 +104,48 @@
|
||||
usuarioBorrar = usuario;
|
||||
}
|
||||
|
||||
let opencrearUsuario = $state(false);
|
||||
function handleDarAdmin(usuario: UserResponseDto) {
|
||||
openDarAdmin = true;
|
||||
usuarioDarAdmin = usuario;
|
||||
}
|
||||
|
||||
// $inspect(usuarios);
|
||||
let timeoutId: ReturnType<typeof setTimeout> | number | undefined;
|
||||
|
||||
function buscarUsuarios() {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(async () => {
|
||||
if (search === '') {
|
||||
usuariosFiltrados = usuarios;
|
||||
return;
|
||||
}
|
||||
usuariosFiltrados = await busquedaAdminUsuarios(search);
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-4 flex gap-2">
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-start"><Search></Search></InputGroupAddon>
|
||||
<InputGroupInput type="text" placeholder="Buscar usuario..." bind:value={search} />
|
||||
<InputGroupInput
|
||||
type="text"
|
||||
placeholder="Buscar usuario..."
|
||||
bind:value={search}
|
||||
oninput={() => buscarUsuarios()}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Button onclick={() =>opencrearUsuario = !opencrearUsuario} variant="secondary" class="bg-blue-500/20"><Plus /></Button>
|
||||
<Button
|
||||
onclick={() => (opencrearUsuario = !opencrearUsuario)}
|
||||
variant="secondary"
|
||||
class="bg-blue-500/20"><Plus /></Button
|
||||
>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
@@ -191,6 +217,24 @@ let opencrearUsuario = $state(false);
|
||||
{/if}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
onclick={() => handleDarAdmin(usuario)}
|
||||
variant={usuario.isAdmin ? 'destructive' : 'default'}
|
||||
>
|
||||
<Shield />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{#if usuario.isAdmin}
|
||||
Sacar admin
|
||||
{:else}
|
||||
Dar Admin
|
||||
{/if}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
@@ -201,3 +245,4 @@ let opencrearUsuario = $state(false);
|
||||
<RecuperarContraseña bind:open usuario={usuarioCambioPass} />
|
||||
<ModificarUsuario bind:open={openModificarUsuario} bind:usuario={usuarioModificar} />
|
||||
<AgregarUsuario bind:open={opencrearUsuario} />
|
||||
<DarAdmin bind:open={openDarAdmin} usuario={usuarioDarAdmin} />
|
||||
|
||||
@@ -35,8 +35,5 @@
|
||||
<BotonSeguir post={{ authorId: usu.id }} />
|
||||
</div>
|
||||
</div>
|
||||
{#if usu.bio}
|
||||
<div class="mt-4 rounded-full bg-accent p-4 text-muted-foreground">{usu.bio}</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
if (error == '') {
|
||||
invalidate('admin:load');
|
||||
open = false;
|
||||
dto = {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
displayName: ''
|
||||
};
|
||||
}
|
||||
|
||||
cargando = false;
|
||||
|
||||
93
src/lib/components/admin/DarAdmin.svelte
Normal file
93
src/lib/components/admin/DarAdmin.svelte
Normal file
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { apiBase } from '@/stores/url';
|
||||
import Button from '../ui/button/button.svelte';
|
||||
import DialogContent from '../ui/dialog/dialog-content.svelte';
|
||||
import DialogHeader from '../ui/dialog/dialog-header.svelte';
|
||||
import DialogTitle from '../ui/dialog/dialog-title.svelte';
|
||||
import Dialog from '../ui/dialog/dialog.svelte';
|
||||
import type { UserResponseDto } from '../../../types';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
usuario: UserResponseDto | null;
|
||||
}
|
||||
|
||||
let { open = $bindable(), usuario }: Props = $props();
|
||||
|
||||
let mensajeResultado = $state('');
|
||||
let mostrarResultado = $state(false);
|
||||
let esExitoso = $state(false);
|
||||
</script>
|
||||
|
||||
{#if mostrarResultado}
|
||||
<Dialog
|
||||
open={mostrarResultado}
|
||||
onOpenChange={() => {
|
||||
mostrarResultado = false;
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<div
|
||||
class={esExitoso
|
||||
? 'rounded border border-green-400 bg-green-100/10 px-4 py-3 text-green-700'
|
||||
: 'rounded border border-red-400 bg-red-100/10 px-4 py-3 text-red-700'}
|
||||
>
|
||||
{mensajeResultado}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/if}
|
||||
<Dialog {open} onOpenChange={() => (open = false)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirmar Admin</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const req = await fetch(`${$apiBase}/api/admin/give`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isAdmin: usuario?.isAdmin || false, id: usuario?.id || '' }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${$sesionStore?.accessToken}`
|
||||
}
|
||||
});
|
||||
if (req.ok) {
|
||||
esExitoso = true;
|
||||
mensajeResultado = 'Operación realizada con éxito';
|
||||
mostrarResultado = true;
|
||||
invalidate('admin:load');
|
||||
} else {
|
||||
const res = await req.json();
|
||||
esExitoso = false;
|
||||
mensajeResultado = res.message || 'Error desconocido';
|
||||
mostrarResultado = true;
|
||||
}
|
||||
} catch {
|
||||
esExitoso = false;
|
||||
mensajeResultado = 'Error de conexión';
|
||||
mostrarResultado = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if usuario}
|
||||
<p>
|
||||
{usuario.isAdmin
|
||||
? '¿Estás seguro que quieres sacarle acceso de administrador al usuario '
|
||||
: '¿Estás seguro que quieres dar acceso de administrador al usuario '}
|
||||
<strong>{usuario.displayName}</strong>
|
||||
(@{usuario.username})?
|
||||
</p>
|
||||
{/if}
|
||||
<div class="mt-4 flex justify-between gap-2">
|
||||
<Button type="submit">Confirmar</Button>
|
||||
<Button variant="secondary" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -78,7 +78,6 @@
|
||||
{/if}
|
||||
</Label>
|
||||
</div>
|
||||
<hr class="my-2" />
|
||||
<div class="flex justify-between">
|
||||
<Button type="submit" disabled={cargando}>
|
||||
{#if cargando}
|
||||
|
||||
23
src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
Normal file
23
src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
class={cn("flex size-9 items-center justify-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<EllipsisIcon class="size-4" />
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
20
src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
Normal file
20
src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLLiAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-item"
|
||||
class={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
31
src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
Normal file
31
src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import type { Snippet } from "svelte";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
href = undefined,
|
||||
child,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
|
||||
} = $props();
|
||||
|
||||
const attrs = $derived({
|
||||
"data-slot": "breadcrumb-link",
|
||||
class: cn("hover:text-foreground transition-colors", className),
|
||||
href,
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: attrs })}
|
||||
{:else}
|
||||
<a bind:this={ref} {...attrs}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
23
src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
Normal file
23
src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLOlAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLOlAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<ol
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-list"
|
||||
class={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ol>
|
||||
23
src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
Normal file
23
src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
class={cn("text-foreground font-normal", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
27
src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte
Normal file
27
src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLLiAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
class={cn("[&>svg]:size-3.5", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<ChevronRightIcon />
|
||||
{/if}
|
||||
</li>
|
||||
21
src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal file
21
src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<nav
|
||||
bind:this={ref}
|
||||
data-slot="breadcrumb"
|
||||
class={className}
|
||||
aria-label="breadcrumb"
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
25
src/lib/components/ui/breadcrumb/index.ts
Normal file
25
src/lib/components/ui/breadcrumb/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Root from "./breadcrumb.svelte";
|
||||
import Ellipsis from "./breadcrumb-ellipsis.svelte";
|
||||
import Item from "./breadcrumb-item.svelte";
|
||||
import Separator from "./breadcrumb-separator.svelte";
|
||||
import Link from "./breadcrumb-link.svelte";
|
||||
import List from "./breadcrumb-list.svelte";
|
||||
import Page from "./breadcrumb-page.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Ellipsis,
|
||||
Item,
|
||||
Separator,
|
||||
Link,
|
||||
List,
|
||||
Page,
|
||||
//
|
||||
Root as Breadcrumb,
|
||||
Ellipsis as BreadcrumbEllipsis,
|
||||
Item as BreadcrumbItem,
|
||||
Separator as BreadcrumbSeparator,
|
||||
Link as BreadcrumbLink,
|
||||
List as BreadcrumbList,
|
||||
Page as BreadcrumbPage,
|
||||
};
|
||||
15
src/lib/components/ui/input-otp/index.ts
Normal file
15
src/lib/components/ui/input-otp/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Root from "./input-otp.svelte";
|
||||
import Group from "./input-otp-group.svelte";
|
||||
import Slot from "./input-otp-slot.svelte";
|
||||
import Separator from "./input-otp-separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Slot,
|
||||
Separator,
|
||||
Root as InputOTP,
|
||||
Group as InputOTPGroup,
|
||||
Slot as InputOTPSlot,
|
||||
Separator as InputOTPSeparator,
|
||||
};
|
||||
20
src/lib/components/ui/input-otp/input-otp-group.svelte
Normal file
20
src/lib/components/ui/input-otp/input-otp-group.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="input-otp-group"
|
||||
class={cn("flex items-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/input-otp/input-otp-separator.svelte
Normal file
19
src/lib/components/ui/input-otp/input-otp-separator.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "$lib/utils.js";
|
||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} data-slot="input-otp-separator" role="separator" {...restProps}>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<MinusIcon />
|
||||
{/if}
|
||||
</div>
|
||||
31
src/lib/components/ui/input-otp/input-otp-slot.svelte
Normal file
31
src/lib/components/ui/input-otp/input-otp-slot.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { PinInput as InputOTPPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
cell,
|
||||
class: className,
|
||||
...restProps
|
||||
}: InputOTPPrimitive.CellProps = $props();
|
||||
</script>
|
||||
|
||||
<InputOTPPrimitive.Cell
|
||||
{cell}
|
||||
bind:ref
|
||||
data-slot="input-otp-slot"
|
||||
class={cn(
|
||||
"border-input aria-invalid:border-destructive dark:bg-input/30 relative flex size-9 items-center justify-center border-y border-e text-sm transition-all outline-none first:rounded-s-md first:border-s last:rounded-e-md",
|
||||
cell.isActive &&
|
||||
"border-ring ring-ring/50 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 ring-offset-background z-10 ring-[3px]",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{cell.char}
|
||||
{#if cell.hasFakeCaret}
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</InputOTPPrimitive.Cell>
|
||||
22
src/lib/components/ui/input-otp/input-otp.svelte
Normal file
22
src/lib/components/ui/input-otp/input-otp.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { PinInput as InputOTPPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: InputOTPPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<InputOTPPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="input-otp"
|
||||
class={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50 [&_input]:disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -36,7 +36,7 @@
|
||||
<Field>
|
||||
<div class="flex items-center">
|
||||
<FieldLabel for="password-{id}">Contraseña</FieldLabel>
|
||||
<a href="##" class="ml-auto inline-block text-sm underline">
|
||||
<a href="/password-reset" class="ml-auto inline-block text-sm underline">
|
||||
Te Olvidaste la contraseña?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { busquedaHashtags } from '@/hooks/busquedaHashtags';
|
||||
import Separator from '@/components/ui/separator/separator.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let search: string = $state('');
|
||||
let open = $state(false);
|
||||
@@ -36,7 +37,7 @@
|
||||
}
|
||||
// $inspect(usuarios, loading);
|
||||
|
||||
let timeoutId: number | undefined;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | number | undefined;
|
||||
function buscar() {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -76,6 +77,12 @@
|
||||
placeholder="Buscar Usuario o Hashtag"
|
||||
bind:value={search}
|
||||
oninput={buscar}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
open = false;
|
||||
goto(resolve(`/search/${encodeURIComponent(search)}`));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if search}
|
||||
<ul class="m-2 space-y-2">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
onMount(() => {
|
||||
sesionStore.subscribe((value) => {
|
||||
showCerrarSesion = !!value?.accessToken;
|
||||
showCerrarSesion = !!value?.username;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<header class="border-b bg-background/95 backdrop-blur">
|
||||
<div class="mx-4 ms-2 flex h-12 items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="mr-6 flex items-center space-x-2">
|
||||
<a data-sveltekit-preload-data={false} href="/" class="mr-6 flex items-center space-x-2">
|
||||
<Avatar
|
||||
class="h-8 w-8 transform rounded-sm! transition-transform duration-300 ease-in-out hover:scale-130 hover:rotate-12"
|
||||
>
|
||||
|
||||
21
src/lib/hooks/busquedaAdminUsuarios.ts
Normal file
21
src/lib/hooks/busquedaAdminUsuarios.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function busquedaAdminUsuarios(q: string) {
|
||||
try {
|
||||
const response = await fetch(get(apiBase) + '/api/admin/users?q=' + q, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
28
src/lib/hooks/cambiarContraseñaUsuario.ts
Normal file
28
src/lib/hooks/cambiarContraseñaUsuario.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { get } from 'svelte/store';
|
||||
import type { UserResponseDto } from '../../types';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
|
||||
export async function cambiarContraseñaUsuario(
|
||||
oldPassword: string,
|
||||
newPassword: string,
|
||||
id: number
|
||||
) {
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/users/${id}/password`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
},
|
||||
body: JSON.stringify({ currentPassword: oldPassword, newPassword })
|
||||
});
|
||||
if (req.ok) {
|
||||
return '';
|
||||
}
|
||||
const data = await req.json();
|
||||
return data.message;
|
||||
} catch {
|
||||
return 'No se pudo alcanzar el servidor';
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { apiBase } from "@/stores/url";
|
||||
import { get } from "svelte/store";
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function checkEmail(email: string) {
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/users/check-email/${email}`, {
|
||||
method: "GET"
|
||||
});
|
||||
if (req.ok){
|
||||
return (await req.json()).available;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/users/check-email/${email}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
if (req.ok) {
|
||||
return (await req.json()).available;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { apiBase } from "@/stores/url";
|
||||
import { get } from "svelte/store";
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function checkUsername(username: string) {
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/users/check-username/${username}`, {
|
||||
method: "GET"
|
||||
});
|
||||
if (req.ok){
|
||||
return (await req.json()).available;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const req = await fetch(`${get(apiBase)}/api/users/check-username/${username}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
if (req.ok) {
|
||||
return (await req.json()).available;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { apiBase } from "@/stores/url";
|
||||
import { sesionStore } from "@/stores/usuario";
|
||||
import { get } from "svelte/store";
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Post } from '../../types';
|
||||
import { PAGE_SIZE } from '../stores/posts';
|
||||
|
||||
export async function getPosts() {
|
||||
export async function getPosts(page: number = 1): Promise<Post[]> {
|
||||
const token = get(sesionStore)?.accessToken;
|
||||
|
||||
const headers: HeadersInit = {};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const req = await fetch(`${get(apiBase)}/timeline?pageSize=20`,{
|
||||
headers: {
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
|
||||
}
|
||||
const res = await fetch(`${get(apiBase)}/timeline?page=${page}&pageSize=${PAGE_SIZE}`, {
|
||||
headers
|
||||
});
|
||||
if (req.ok) {
|
||||
return await req.json();
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error('Error cargando posts');
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { sesionStore } from '@/stores/usuario';
|
||||
import type { Post } from '../../types';
|
||||
|
||||
export async function likePost(post: Post) {
|
||||
let method = post.isLiked ? "DELETE" : "POST";
|
||||
let method = post.isLiked ? 'DELETE' : 'POST';
|
||||
try {
|
||||
const req = await fetch(get(apiBase) + `/api/posts/${post.id}/like`, {
|
||||
method: method,
|
||||
|
||||
32
src/lib/hooks/loadMorePosts.ts
Normal file
32
src/lib/hooks/loadMorePosts.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { page, loadingPosts, PAGE_SIZE } from '@/stores/posts';
|
||||
import { appendPosts } from '@/stores/posts';
|
||||
import { getPosts } from './getPosts';
|
||||
|
||||
let finished = false;
|
||||
|
||||
export async function loadMorePosts() {
|
||||
if (get(loadingPosts) || finished) return;
|
||||
|
||||
loadingPosts.set(true);
|
||||
|
||||
try {
|
||||
const currentPage = get(page);
|
||||
const newPosts = await getPosts(currentPage);
|
||||
|
||||
if (newPosts.length === 0) {
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
appendPosts(newPosts);
|
||||
|
||||
if (newPosts.length < PAGE_SIZE) {
|
||||
finished = true;
|
||||
} else {
|
||||
page.update((p) => p + 1);
|
||||
}
|
||||
} finally {
|
||||
loadingPosts.set(false);
|
||||
}
|
||||
}
|
||||
29
src/lib/hooks/loginFirebase.ts
Normal file
29
src/lib/hooks/loginFirebase.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { apiBase } from '@/stores/url';
|
||||
import type { LoginSsoDto, Sesion } from '../../types';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function loginFirebase(dto: LoginSsoDto, callbackfn: () => void) {
|
||||
if (dto.accessToken == '' || dto.uid == '') return;
|
||||
try {
|
||||
const req = await fetch(get(apiBase) + '/api/auth/login/sso', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(dto)
|
||||
});
|
||||
if (req.ok) {
|
||||
const token: Sesion = await req.json();
|
||||
sesionStore.set({ ...token, accessToken: dto.accessToken });
|
||||
goto('/');
|
||||
} else {
|
||||
callbackfn();
|
||||
}
|
||||
} catch {
|
||||
// callbackfn();
|
||||
console.error('fallo al intentar alcanzar el servidor');
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,13 @@ import type { Post } from '../../types';
|
||||
export async function obtenerRespuestasPorId(
|
||||
id: string,
|
||||
fetch2?: Function,
|
||||
depends?: Function
|
||||
depends?: Function,
|
||||
page: number = 1
|
||||
): Promise<string | Post[] | null> {
|
||||
if (depends) depends('post:respuestas');
|
||||
const fetchFn = fetch2 ? fetch2 : fetch;
|
||||
try {
|
||||
const req = await fetchFn(`${get(apiBase)}/api/posts/${id}/replies`, {
|
||||
const req = await fetchFn(`${get(apiBase)}/api/posts/${id}/replies?page=${page}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import type { UsersResponseDto } from '../../types';
|
||||
import { get } from 'svelte/store';
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function obtenerSeguidoresPorUsuario(
|
||||
id: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
fetch2: Function
|
||||
fetch2?: Function
|
||||
): Promise<UsersResponseDto | null> {
|
||||
try {
|
||||
const fetchFunc = fetch2 || fetch;
|
||||
const response = await fetchFunc(`${get(apiBase)}/api/users/${id}/followers?limit=${limit}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const response = await fetchFunc(
|
||||
`${get(apiBase)}/api/users/${id}/followers?skip=${skip}&limit=${limit}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
|
||||
@@ -5,26 +5,31 @@ import { get } from 'svelte/store';
|
||||
|
||||
export async function obtenerSeguidosPorUsuario(
|
||||
id: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
fetch2?: Function
|
||||
): Promise<UsersResponseDto | null> {
|
||||
try {
|
||||
const fetchFunc = fetch2 || fetch;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const response = await fetchFunc(`${get(apiBase)}/api/users/${id}/following?limit=${limit}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
const response = await fetchFunc(
|
||||
`${get(apiBase)}/api/users/${id}/following?skip=${skip}&limit=${limit}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const users: UsersResponseDto = await response.json();
|
||||
return users;
|
||||
const data: UsersResponseDto = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { addPost } from "@/stores/posts";
|
||||
import { apiBase } from "@/stores/url";
|
||||
import { sesionStore } from "@/stores/usuario";
|
||||
import { get } from "svelte/store";
|
||||
import { addPost } from '@/stores/posts';
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function publicarPost(formData: FormData){
|
||||
try{
|
||||
const req = fetch(get(apiBase) + '/api/posts', {
|
||||
method: 'POST',
|
||||
//credentials: 'include',
|
||||
headers: {
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
export async function publicarPost(formData: FormData) {
|
||||
try {
|
||||
const req = fetch(get(apiBase) + '/api/posts', {
|
||||
method: 'POST',
|
||||
//credentials: 'include',
|
||||
headers: {
|
||||
Authorization: `Bearer ${get(sesionStore)?.accessToken}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const res = await req;
|
||||
if (res.ok) {
|
||||
const post = await res.json();
|
||||
addPost(post);
|
||||
return '';
|
||||
}
|
||||
return 'No se pudo crear el post.';
|
||||
} catch {
|
||||
return 'Fallo al alcanzar el servidor';
|
||||
const res = await req;
|
||||
if (res.ok) {
|
||||
const post = await res.json();
|
||||
addPost(post);
|
||||
return '';
|
||||
}
|
||||
return 'No se pudo crear el post.';
|
||||
} catch {
|
||||
return 'Fallo al alcanzar el servidor';
|
||||
}
|
||||
}
|
||||
|
||||
30
src/lib/hooks/registerFirebase.ts
Normal file
30
src/lib/hooks/registerFirebase.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { RegisterDto, RegisterSsoDto } from '../../types';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function registerFirebase(
|
||||
dto: RegisterSsoDto,
|
||||
callbackfn: () => void,
|
||||
admin: boolean = false
|
||||
) {
|
||||
if (dto.uid == '' || dto.token == '' || !dto.email?.includes('@') || dto.username == '') return;
|
||||
try {
|
||||
const req = await fetch(get(apiBase) + '/api/auth/register/sso', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(dto)
|
||||
});
|
||||
if (req.ok) {
|
||||
const data = await req.json();
|
||||
if (!admin) goto('/login?msg=' + data.message);
|
||||
} else {
|
||||
callbackfn();
|
||||
}
|
||||
} catch {
|
||||
callbackfn();
|
||||
console.error('fallo al intentar alcanzar el servidor');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
export async function updateImagenDePerfil(){
|
||||
try{
|
||||
|
||||
}catch{
|
||||
|
||||
}
|
||||
export async function updateImagenDePerfil() {
|
||||
try {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { sesionStore } from '@/stores/usuario';
|
||||
|
||||
export async function updatePost(post: Post, callbackfn: Function, message: string) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("content", post.content);
|
||||
formData.append("image", post.image||"");
|
||||
const formData = new FormData();
|
||||
formData.append('content', post.content);
|
||||
formData.append('image', post.image || '');
|
||||
|
||||
const req = await fetch(get(apiBase) + `/api/posts/${post.id}`, {
|
||||
method: 'PUT',
|
||||
|
||||
37
src/lib/stores/firebase.ts
Normal file
37
src/lib/stores/firebase.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { initializeApp, type FirebaseApp } from 'firebase/app';
|
||||
import { getAuth, type Auth } from 'firebase/auth';
|
||||
|
||||
import {
|
||||
PUBLIC_f_apiKey,
|
||||
PUBLIC_f_appId,
|
||||
PUBLIC_f_authDomain,
|
||||
PUBLIC_f_messagingSenderId,
|
||||
PUBLIC_f_projectId,
|
||||
PUBLIC_f_storageBucket
|
||||
} from '$env/static/public';
|
||||
|
||||
let app: FirebaseApp | null = null;
|
||||
let auth: Auth | null = null;
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: PUBLIC_f_apiKey,
|
||||
authDomain: PUBLIC_f_authDomain,
|
||||
projectId: PUBLIC_f_projectId,
|
||||
storageBucket: PUBLIC_f_storageBucket,
|
||||
messagingSenderId: PUBLIC_f_messagingSenderId,
|
||||
appId: PUBLIC_f_appId
|
||||
};
|
||||
|
||||
export function getFirebaseApp(): FirebaseApp {
|
||||
if (!app) {
|
||||
app = initializeApp(firebaseConfig);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
export function getFirebaseAuth(): Auth {
|
||||
if (!auth) {
|
||||
auth = getAuth(getFirebaseApp());
|
||||
}
|
||||
return auth;
|
||||
}
|
||||
@@ -1,29 +1,35 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Post } from '../../types';
|
||||
|
||||
export const posts = writable<Post[] | undefined>(undefined);
|
||||
export const posts = writable<Post[]>([]);
|
||||
export const loadingPosts = writable(false);
|
||||
export const page = writable(1);
|
||||
|
||||
export const PAGE_SIZE = 20;
|
||||
|
||||
export const setPosts = (newPosts: Post[]) => {
|
||||
posts.set(newPosts);
|
||||
};
|
||||
|
||||
export const appendPosts = (newPosts: Post[]) => {
|
||||
posts.update((current) => [...current, ...newPosts]);
|
||||
};
|
||||
|
||||
export const addPost = (post: Post) => {
|
||||
posts.update((currentPosts) => [post, ...currentPosts]);
|
||||
posts.update((current) => [post, ...current]);
|
||||
};
|
||||
|
||||
export const updatePostStore = (postId: string, updatedData: Partial<Post>) => {
|
||||
posts.update((currentPosts) =>
|
||||
currentPosts.map((post) => (post.id === postId ? { ...post, ...updatedData } : post))
|
||||
posts.update((current) =>
|
||||
current.map((post) => (post.id === postId ? { ...post, ...updatedData } : post))
|
||||
);
|
||||
};
|
||||
|
||||
export const removePost = (postId: string) => {
|
||||
posts.update((currentPosts) => {
|
||||
const a = currentPosts.filter((post) => post.id !== postId);
|
||||
return a;
|
||||
});
|
||||
posts.update((current) => current.filter((post) => post.id !== postId));
|
||||
};
|
||||
|
||||
export const resetPosts = () => {
|
||||
posts.set(undefined);
|
||||
posts.set([]);
|
||||
page.set(1);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { get, writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
import type { Sesion } from '../../types';
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { getFirebaseApp, getFirebaseAuth } from './firebase';
|
||||
|
||||
const { subscribe } = apiBase;
|
||||
let baseUrl: string = '';
|
||||
@@ -21,6 +22,10 @@ export const sesionStore = {
|
||||
reset: () => currentSesion.set(null)
|
||||
};
|
||||
|
||||
// sesionStore.subscribe((value) => {
|
||||
// console.log(value);
|
||||
// });
|
||||
|
||||
if (browser) {
|
||||
currentSesion.subscribe((value) => {
|
||||
localStorage.setItem('sesion', JSON.stringify(value));
|
||||
@@ -45,9 +50,32 @@ if (browser) {
|
||||
}
|
||||
};
|
||||
|
||||
const shouldRefreshToken = (sesion: Sesion | null): boolean => {
|
||||
const shouldRefreshToken = async (sesion: Sesion | null): Promise<boolean> => {
|
||||
if (!sesion || !sesion.accessToken) return false;
|
||||
|
||||
if (sesion.isFirebase) {
|
||||
try {
|
||||
getFirebaseApp();
|
||||
|
||||
const auth = getFirebaseAuth();
|
||||
const user = auth.currentUser;
|
||||
|
||||
if (user) {
|
||||
const token = await user.getIdToken(true);
|
||||
currentSesion.update((s) => {
|
||||
if (s) {
|
||||
return { ...s, accessToken: token };
|
||||
}
|
||||
return s;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error obteniendo token de Firebase:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const decoded = decodeJWT(sesion.accessToken);
|
||||
if (!decoded || !decoded.exp) return false;
|
||||
|
||||
@@ -61,28 +89,31 @@ if (browser) {
|
||||
const refreshAccessToken = async () => {
|
||||
try {
|
||||
const sesion = get(currentSesion);
|
||||
if (!shouldRefreshToken(sesion)) return;
|
||||
if (!(await shouldRefreshToken(sesion))) return;
|
||||
|
||||
console.log('refrescando token');
|
||||
const response = await fetch(get(apiBase) + '/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
currentSesion.update((sesion) => {
|
||||
if (sesion) {
|
||||
return { ...sesion, accessToken: data.accessToken };
|
||||
}
|
||||
return sesion;
|
||||
// Solo ejecutar este código si NO es Firebase
|
||||
if (!sesion?.isFirebase) {
|
||||
console.log('refrescando token');
|
||||
const response = await fetch(get(apiBase) + '/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
} else {
|
||||
console.error('Error refrescando token:', response.statusText);
|
||||
currentSesion.set(null);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
currentSesion.update((sesion) => {
|
||||
if (sesion) {
|
||||
return { ...sesion, accessToken: data.accessToken };
|
||||
}
|
||||
return sesion;
|
||||
});
|
||||
} else {
|
||||
console.error('Error refrescando token:', response.statusText);
|
||||
currentSesion.set(null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refrescando token:', error);
|
||||
@@ -90,6 +121,9 @@ if (browser) {
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(refreshAccessToken, 30 * 1000); // Check every 30 seconds
|
||||
setInterval(() => {
|
||||
refreshAccessToken();
|
||||
}, 30 * 1000); // Check every 30 seconds
|
||||
|
||||
refreshAccessToken();
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
|
||||
export function filtrarImagen(file: File) {
|
||||
if (file) {
|
||||
if (file) {
|
||||
const allowed = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/webp'];
|
||||
if (allowed.includes(file.type)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import CardDescription from '@/components/ui/card/card-description.svelte';
|
||||
import { page } from '$app/state';
|
||||
import TablaUsuarios from '@/components/TablaUsuarios.svelte';
|
||||
import CardTitle from '@/components/ui/card/card-title.svelte';
|
||||
import CardHeader from '@/components/ui/card/card-header.svelte';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import Header from '@/head/Header.svelte';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
@@ -17,3 +18,11 @@
|
||||
<TooltipProvider>
|
||||
{@render children()}
|
||||
</TooltipProvider>
|
||||
<footer
|
||||
class="fixed right-0 bottom-0 left-0 border-t bg-background p-2 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
<p>
|
||||
© {new Date().getFullYear()} Mini X. Todos los derechos reservados.
|
||||
<a class="text-blue-500 underline" href={resolve('/about')}>Sobre Nosotros</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
@@ -1,40 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { replaceState } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import { Content } from '@/components/ui/card';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import CrearPost from '@/components/crear-post.svelte';
|
||||
import { posts, resetPosts, setPosts, updatePostStore } from '@/stores/posts';
|
||||
import PostCard from '@/components/PostCard.svelte';
|
||||
import type { Post } from '../types';
|
||||
import ModalEditar from './[perfil]/modalEditar.svelte';
|
||||
import { updatePost } from '@/hooks/updatePost';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { getPosts } from '@/hooks/getPosts';
|
||||
import Spinner from '@/components/ui/spinner/spinner.svelte';
|
||||
|
||||
$effect(() => {
|
||||
resetPosts();
|
||||
(async () => {
|
||||
setPosts(await getPosts());
|
||||
})();
|
||||
});
|
||||
import Dialog from '@/components/ui/dialog/dialog.svelte';
|
||||
import DialogContent from '@/components/ui/dialog/dialog-content.svelte';
|
||||
import CrearPost from '@/components/crear-post.svelte';
|
||||
import PostCard from '@/components/PostCard.svelte';
|
||||
import ModalEditar from './[perfil]/modalEditar.svelte';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { posts, updatePostStore, loadingPosts, resetPosts } from '@/stores/posts';
|
||||
import { updatePost } from '@/hooks/updatePost';
|
||||
import { loadMorePosts } from '@/hooks/loadMorePosts';
|
||||
import type { Post } from '../types';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
|
||||
let postAModificar: Post | null = $state(null);
|
||||
let mensajeError = $state('');
|
||||
let sentinel: HTMLDivElement;
|
||||
|
||||
resetPosts();
|
||||
$effect(() => {
|
||||
loadMorePosts();
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
loadMorePosts();
|
||||
}
|
||||
},
|
||||
{ rootMargin: '200px' }
|
||||
);
|
||||
|
||||
observer.observe(sentinel);
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
|
||||
async function handleEditar(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (postAModificar == null) return;
|
||||
if (!postAModificar) return;
|
||||
|
||||
await updatePost(
|
||||
postAModificar,
|
||||
(postnuevo: Post) => updatePostStore(postAModificar!.id, postnuevo),
|
||||
|
||||
(postNuevo: Post) => updatePostStore(postAModificar!.id, postNuevo),
|
||||
mensajeError
|
||||
);
|
||||
postAModificar = null;
|
||||
}
|
||||
|
||||
let from = page.url.searchParams.get('from');
|
||||
|
||||
if (from) {
|
||||
replaceState('/', {});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if from === 'cambio_contraseña'}
|
||||
<Dialog open>
|
||||
<DialogContent>Se cambió la contraseña del usuario exitosamente</DialogContent>
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
<meta property="og:title" content="Mini-x" />
|
||||
<meta property="og:description" content="Pagina Principal" />
|
||||
@@ -46,22 +73,22 @@
|
||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||
<div class="w-full max-w-6xl">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if $sesionStore !== null}
|
||||
{#if $sesionStore}
|
||||
<CrearPost />
|
||||
{/if}
|
||||
<hr />
|
||||
|
||||
{#if $posts === undefined}
|
||||
{#if $posts.length === 0 && $loadingPosts}
|
||||
<Card>
|
||||
<Content class="flex items-center justify-center gap-2">
|
||||
<Spinner class="h-10 w-10" />
|
||||
<p>Cargando</p>
|
||||
</Content>
|
||||
</Card>
|
||||
{:else if $posts.length <= 0}
|
||||
{:else if $posts.length === 0}
|
||||
<Card>
|
||||
<Content>
|
||||
<p class=" text-center leading-7 not-first:mt-6">No hay Posts que mostrar</p>
|
||||
<p class="text-center leading-7">No hay Posts que mostrar</p>
|
||||
</Content>
|
||||
</Card>
|
||||
{:else}
|
||||
@@ -72,6 +99,13 @@
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div bind:this={sentinel} class="h-1"></div>
|
||||
|
||||
{#if $loadingPosts && $posts.length > 0}
|
||||
<div class="flex justify-center py-4">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if postAModificar}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
import { resetPosts } from '@/stores/posts';
|
||||
|
||||
//export const ssr = true;
|
||||
// export async function load({}) {}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { apiBase } from '@/stores/url';
|
||||
import PenLine from '@lucide/svelte/icons/pen-line';
|
||||
import type { Post } from '../../types.js';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import PostCard from '@/components/PostCard.svelte';
|
||||
@@ -9,7 +8,7 @@
|
||||
import ModalEditar from './modalEditar.svelte';
|
||||
import { page } from '$app/state';
|
||||
import Button from '@/components/ui/button/button.svelte';
|
||||
import { Dialog } from '@/components/ui/dialog/index.js';
|
||||
import { Dialog } from '@/components/ui/dialog';
|
||||
import CrearPost from '@/components/crear-post.svelte';
|
||||
import DialogContent from '@/components/ui/dialog/dialog-content.svelte';
|
||||
import DialogTitle from '@/components/ui/dialog/dialog-title.svelte';
|
||||
@@ -19,50 +18,108 @@
|
||||
import CardPerfil from '@/components/CardPerfil.svelte';
|
||||
import DialogModificarUsuario from '@/components/DialogModificarUsuario.svelte';
|
||||
import BotonSeguir from '@/components/BotonSeguir.svelte';
|
||||
import DialogResetPassword from '@/components/DialogResetPassword.svelte';
|
||||
import Key from '@lucide/svelte/icons/key';
|
||||
import UserPen from '@lucide/svelte/icons/user-pen';
|
||||
import PenLine from '@lucide/svelte/icons/pen-line';
|
||||
|
||||
let { params } = $props();
|
||||
|
||||
let cargando = $state(true);
|
||||
setPosts([]);
|
||||
|
||||
let cargando = $state(false);
|
||||
let finished = $state(false);
|
||||
let pageNumber = $state(1);
|
||||
let sentinel = $state<HTMLDivElement | null>(null);
|
||||
|
||||
let mensajeError = $state('');
|
||||
let postAModificar: Post | null = $state(null);
|
||||
|
||||
let showCrearPost = $state(false);
|
||||
|
||||
let data = $derived(page.data);
|
||||
// $inspect(data);
|
||||
|
||||
$effect(() => {
|
||||
obtenerPosts();
|
||||
});
|
||||
let fetching = false;
|
||||
// svelte-ignore state_referenced_locally
|
||||
let currentProfile = $state(params.perfil);
|
||||
|
||||
async function obtenerPosts() {
|
||||
if (fetching || finished) return;
|
||||
|
||||
fetching = true;
|
||||
cargando = true;
|
||||
|
||||
try {
|
||||
const req = await fetch($apiBase + '/api/posts/user/' + params.perfil, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${$sesionStore?.accessToken}`
|
||||
const res = await fetch(
|
||||
`${$apiBase}/api/posts/user/${params.perfil}?page=${pageNumber}&pageSize=20`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${$sesionStore?.accessToken}`
|
||||
}
|
||||
}
|
||||
});
|
||||
if (req.ok) {
|
||||
setPosts(await req.json());
|
||||
);
|
||||
const nuevosPosts: Post[] = await res.json();
|
||||
|
||||
if (nuevosPosts.length === 0) {
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
mensajeError = 'Fallo al obtener los datos';
|
||||
} catch {
|
||||
mensajeError = 'No se alcanzo el servidor';
|
||||
|
||||
posts.update((actuales = []) => [...actuales, ...nuevosPosts]);
|
||||
|
||||
pageNumber++;
|
||||
|
||||
if (nuevosPosts.length < 20) {
|
||||
finished = true;
|
||||
}
|
||||
} catch (error) {
|
||||
mensajeError = 'Error al cargar los posts';
|
||||
} finally {
|
||||
fetching = false;
|
||||
cargando = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (currentProfile !== params.perfil) {
|
||||
currentProfile = params.perfil;
|
||||
setPosts([]);
|
||||
pageNumber = 1;
|
||||
finished = false;
|
||||
mensajeError = '';
|
||||
fetching = false;
|
||||
|
||||
obtenerPosts();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!sentinel || finished) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !fetching && !finished) {
|
||||
obtenerPosts();
|
||||
}
|
||||
},
|
||||
{ rootMargin: '100px' }
|
||||
);
|
||||
|
||||
observer.observe(sentinel);
|
||||
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
|
||||
async function handleEditar(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (postAModificar == null) return;
|
||||
if (!postAModificar) return;
|
||||
|
||||
await updatePost(
|
||||
postAModificar,
|
||||
(postnuevo: Post) => updatePostStore(postAModificar!.id, postnuevo),
|
||||
|
||||
(postNuevo: Post) => updatePostStore(postAModificar!.id, postNuevo),
|
||||
mensajeError
|
||||
);
|
||||
|
||||
postAModificar = null;
|
||||
}
|
||||
</script>
|
||||
@@ -70,9 +127,9 @@
|
||||
<!-- {$inspect(data)} -->
|
||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||
<div class="w-full max-w-6xl">
|
||||
{#key data}
|
||||
<CardPerfil bind:data />
|
||||
{/key}
|
||||
<!-- {#key data.id} -->
|
||||
<CardPerfil bind:data />
|
||||
<!-- {/key} -->
|
||||
<h1
|
||||
class="mt-10 flex scroll-m-20 justify-between text-3xl font-extrabold tracking-tight lg:text-3xl"
|
||||
>
|
||||
@@ -89,14 +146,12 @@
|
||||
<PenLine />
|
||||
</Button>
|
||||
{:else if $posts?.length == 0}
|
||||
<BotonSeguir post={{ authorId: data.id }} />
|
||||
<BotonSeguir post={{ authorId: data.id, id: data.id }} />
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
<hr class="mb-8" />
|
||||
{#if cargando}
|
||||
<CardCargando />
|
||||
{:else if mensajeError !== ''}
|
||||
{#if mensajeError !== ''}
|
||||
<CardError {mensajeError} />
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -105,6 +160,16 @@
|
||||
<PostCard {post} bind:postAModificar />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div bind:this={sentinel} class="h-1"></div>
|
||||
|
||||
{#if cargando && !finished}
|
||||
<CardCargando />
|
||||
{/if}
|
||||
|
||||
{#if finished && $posts.length === 0}
|
||||
<p class="text-center text-muted-foreground">No hay posts para mostrar</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -131,7 +196,20 @@
|
||||
</div>
|
||||
|
||||
{#if $sesionStore?.isAdmin || $sesionStore?.username == params.perfil}
|
||||
<DialogModificarUsuario bind:data />
|
||||
<div class="fixed right-8 bottom-12 flex flex-col gap-2">
|
||||
<DialogModificarUsuario bind:data>
|
||||
<Button variant="default" size="icon-lg">
|
||||
<UserPen />
|
||||
</Button>
|
||||
</DialogModificarUsuario>
|
||||
{#if !$sesionStore.isFirebase}
|
||||
<DialogResetPassword bind:data>
|
||||
<Button variant="default" size="icon-lg">
|
||||
<Key />
|
||||
</Button>
|
||||
</DialogResetPassword>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario.js';
|
||||
import type { User, UserResponseDto } from '../../types.js';
|
||||
import type { UserResponseDto } from '../../types.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario.js';
|
||||
import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario.js';
|
||||
import { obtenerCantidadDeSeguidores } from '@/hooks/obtenerCantidadDeSeguidores.js';
|
||||
import { obtenerCantidadDeSeguidos } from '@/hooks/obtenerCantidadDeSeguidos.js';
|
||||
|
||||
export async function load({ params, depends, fetch }) {
|
||||
export const load: PageLoad = async ({ params, depends, fetch }) => {
|
||||
depends('perfil:general');
|
||||
const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch);
|
||||
if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil);
|
||||
|
||||
const [seguidos, seguidores, countSeguidores, countSeguidos] = await Promise.all([
|
||||
obtenerSeguidosPorUsuario(usuario.id, 5, fetch),
|
||||
obtenerSeguidoresPorUsuario(usuario.id, 5, fetch),
|
||||
obtenerSeguidosPorUsuario(usuario.id, 1, 5, fetch),
|
||||
obtenerSeguidoresPorUsuario(usuario.id, 1, 5, fetch),
|
||||
obtenerCantidadDeSeguidores(usuario.id, fetch),
|
||||
obtenerCantidadDeSeguidos(usuario.id, fetch)
|
||||
]);
|
||||
@@ -25,4 +26,4 @@ export async function load({ params, depends, fetch }) {
|
||||
countSeguidores: countSeguidores.count,
|
||||
countSeguidos: countSeguidos.count
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
<script lang="ts">
|
||||
import ArrowLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import type { UserResponseDto } from '../../../types';
|
||||
import UserCard from '@/components/UserCard.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario';
|
||||
|
||||
type Data = {
|
||||
usuario: UserResponseDto;
|
||||
seguidores: UserResponseDto[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
let currentPage = $state(1);
|
||||
let isLoading = $state(false);
|
||||
const limit = 100;
|
||||
|
||||
let totalPages = $derived(Math.ceil(data.totalCount / limit));
|
||||
|
||||
async function loadPage(page: number) {
|
||||
if (isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
const response = await obtenerSeguidoresPorUsuario(data.usuario.id, page, limit);
|
||||
|
||||
if (response) {
|
||||
data.seguidores = response.response as UserResponseDto[];
|
||||
data.totalCount = response.totalCount;
|
||||
currentPage = page;
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||
@@ -25,9 +50,14 @@
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
</div>
|
||||
{#if data.seguidores.length === 0}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="py-8 text-center text-muted-foreground">
|
||||
<p>No hay seguidores para mostrar.</p>
|
||||
<p>Cargando...</p>
|
||||
</div>
|
||||
{:else if data.seguidores.length === 0}
|
||||
<div class="py-8 text-center text-muted-foreground">
|
||||
<p>No hay seguidos para mostrar.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col sm:grid" style="grid-template-columns: repeat(2, 1fr); gap: 1rem;">
|
||||
@@ -38,5 +68,29 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="mt-6 flex items-center justify-center gap-2">
|
||||
<button
|
||||
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onclick={() => loadPage(currentPage - 1)}
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
>
|
||||
<ChevronLeft class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<span class="px-4 text-sm text-muted-foreground">
|
||||
Página {currentPage} de {totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onclick={() => loadPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages || isLoading}
|
||||
>
|
||||
<ChevronRight class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { obtenerSeguidoresPorUsuario } from '@/hooks/obtenerSeguidoresPorUsuario';
|
||||
import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import type { UserResponseDto, UsersResponseDto } from '../../../types';
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch);
|
||||
if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil);
|
||||
|
||||
const seguidoresResponse: UsersResponseDto | null = await obtenerSeguidoresPorUsuario(
|
||||
usuario.id,
|
||||
1,
|
||||
100,
|
||||
fetch
|
||||
);
|
||||
@@ -17,4 +19,4 @@ export async function load({ params, fetch }) {
|
||||
usuario,
|
||||
seguidores: seguidoresResponse?.response || []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
<script lang="ts">
|
||||
import ArrowLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import type { UserResponseDto } from '../../../types';
|
||||
import UserCard from '@/components/UserCard.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario';
|
||||
|
||||
type Data = {
|
||||
usuario: UserResponseDto;
|
||||
seguidos: UserResponseDto[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
let currentPage = $state(1);
|
||||
let isLoading = $state(false);
|
||||
const limit = 100;
|
||||
|
||||
let totalPages = $derived(Math.ceil(data.totalCount / limit));
|
||||
|
||||
async function loadPage(page: number) {
|
||||
if (isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
const response = await obtenerSeguidosPorUsuario(data.usuario.id, page, limit);
|
||||
|
||||
if (response) {
|
||||
data.seguidos = response.response as UserResponseDto[];
|
||||
data.totalCount = response.totalCount;
|
||||
currentPage = page;
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||
@@ -25,7 +50,12 @@
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
</div>
|
||||
{#if data.seguidos.length === 0}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="py-8 text-center text-muted-foreground">
|
||||
<p>Cargando...</p>
|
||||
</div>
|
||||
{:else if data.seguidos.length === 0}
|
||||
<div class="py-8 text-center text-muted-foreground">
|
||||
<p>No hay seguidos para mostrar.</p>
|
||||
</div>
|
||||
@@ -38,5 +68,29 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="mt-6 flex items-center justify-center gap-2">
|
||||
<button
|
||||
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onclick={() => loadPage(currentPage - 1)}
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
>
|
||||
<ChevronLeft class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<span class="px-4 text-sm text-muted-foreground">
|
||||
Página {currentPage} de {totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="rounded-md border bg-card p-2 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onclick={() => loadPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages || isLoading}
|
||||
>
|
||||
<ChevronRight class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { obtenerUsuarioPorUsername } from '@/hooks/obtenerUsuario';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import type { UserResponseDto, UsersResponseDto } from '../../../types';
|
||||
import { obtenerSeguidosPorUsuario } from '@/hooks/obtenerSeguidosPorUsuario';
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
const usuario: UserResponseDto | null = await obtenerUsuarioPorUsername(params.perfil, fetch);
|
||||
if (!usuario) error(404, 'No se encontro el usuario, ' + params.perfil);
|
||||
|
||||
const seguidosResponse: UsersResponseDto | null = await obtenerSeguidosPorUsuario(
|
||||
usuario.id,
|
||||
1,
|
||||
100,
|
||||
fetch
|
||||
);
|
||||
|
||||
return {
|
||||
usuario,
|
||||
seguidos: seguidosResponse?.response || []
|
||||
seguidos: seguidosResponse?.response || [],
|
||||
totalCount: seguidosResponse?.totalCount ?? 0
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
66
src/routes/about/+page.svelte
Normal file
66
src/routes/about/+page.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import AvatarFallback from '@/components/ui/avatar/avatar-fallback.svelte';
|
||||
import AvatarImage from '@/components/ui/avatar/avatar-image.svelte';
|
||||
import Avatar from '@/components/ui/avatar/avatar.svelte';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import CardDescription from '@/components/ui/card/card-description.svelte';
|
||||
import CardHeader from '@/components/ui/card/card-header.svelte';
|
||||
import CardTitle from '@/components/ui/card/card-title.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import Separator from '@/components/ui/separator/separator.svelte';
|
||||
import Github from '@lucide/svelte/icons/github';
|
||||
|
||||
let devs = [
|
||||
{
|
||||
name: 'Fede',
|
||||
role: 'Desarrollador Full Stack',
|
||||
pic: 'https://avatars.githubusercontent.com/u/61921832?s=64&v=4',
|
||||
gh: 'https://github.com/fedpo2'
|
||||
},
|
||||
{
|
||||
name: 'Luca',
|
||||
role: 'Desarrollador Frontend',
|
||||
pic: 'https://avatars.githubusercontent.com/u/141507149?s=64&v=4',
|
||||
gh: 'https://github.com/TroianoLuca2am'
|
||||
},
|
||||
{
|
||||
name: 'Fran',
|
||||
role: 'Desarrollador Backend',
|
||||
pic: 'https://avatars.githubusercontent.com/u/126514899?s=64&v=4',
|
||||
gh: 'https://github.com/franciscorosecerna'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||
<div class="w-full max-w-6xl">
|
||||
<h1 class="mb-2 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Sobre Nosotros
|
||||
</h1>
|
||||
|
||||
<Separator class="my-2" />
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{#each devs as dev}
|
||||
<Card>
|
||||
<CardHeader class="flex flex-col items-center justify-center gap-2">
|
||||
<Avatar class="h-24 w-24">
|
||||
<AvatarImage src={dev.pic} class="h-24 w-24" />
|
||||
<AvatarFallback class="flex h-24 w-24 items-center justify-center text-3xl">
|
||||
{dev.name.toLocaleUpperCase()[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<CardTitle>{dev.name}</CardTitle>
|
||||
<CardDescription>{dev.role}</CardDescription>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<a href={dev.gh}><Github /></a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,7 +10,6 @@
|
||||
import { updatePost } from '@/hooks/updatePost';
|
||||
import Separator from '@/components/ui/separator/separator.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface props {
|
||||
data: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { obtenerCantidadDeUsosdeHtag } from '@/hooks/obtenerCantidadDeUsosdeHtag.js';
|
||||
export const ssr = false;
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
let { htag } = params;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
import AlertCircleIcon from '@lucide/svelte/icons/alert-circle';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import Info from '@lucide/svelte/icons/info';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import FireBaseButton from '@/components/FireBaseButton.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -41,6 +44,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
<LoginForm bind:showAlert id="1" />
|
||||
|
||||
<Card class="mt-2">
|
||||
<CardContent>
|
||||
<FireBaseButton mode="login" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{#if showAlert}
|
||||
<div class="mt-2" transition:fade>
|
||||
<Alert.Root variant="destructive">
|
||||
|
||||
35
src/routes/password-reset/+page.svelte
Normal file
35
src/routes/password-reset/+page.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import BreadcrumbItem from '@/components/ui/breadcrumb/breadcrumb-item.svelte';
|
||||
import BreadcrumbList from '@/components/ui/breadcrumb/breadcrumb-list.svelte';
|
||||
import BreadcrumbSeparator from '@/components/ui/breadcrumb/breadcrumb-separator.svelte';
|
||||
import Breadcrumb from '@/components/ui/breadcrumb/breadcrumb.svelte';
|
||||
|
||||
import Pasos from './Pasos.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import IngresarEmail from './IngresarEmail.svelte';
|
||||
import Otp from './Otp.svelte';
|
||||
import NuevaPass from './NuevaPass.svelte';
|
||||
|
||||
let estado: 'email' | 'otp' | 'nuevapass' = $state('email');
|
||||
let email: string = $state('');
|
||||
let otp: string = $state('');
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||
<div class="w-full max-w-6xl">
|
||||
<Pasos {estado} />
|
||||
<div class="mt-6">
|
||||
{#if estado === 'email'}
|
||||
<IngresarEmail bind:estado bind:email />
|
||||
{/if}
|
||||
{#if estado === 'otp'}
|
||||
<Otp bind:estado {email} bind:otp />
|
||||
{/if}
|
||||
{#if estado === 'nuevapass'}
|
||||
<NuevaPass {otp} {email} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
101
src/routes/password-reset/IngresarEmail.svelte
Normal file
101
src/routes/password-reset/IngresarEmail.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import CardError from '@/components/CardError.svelte';
|
||||
import Button from '@/components/ui/button/button.svelte';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import DialogContent from '@/components/ui/dialog/dialog-content.svelte';
|
||||
import Dialog from '@/components/ui/dialog/dialog.svelte';
|
||||
import Input from '@/components/ui/input/input.svelte';
|
||||
import Spinner from '@/components/ui/spinner/spinner.svelte';
|
||||
import { checkEmail } from '@/hooks/checkEmail';
|
||||
import { apiBase } from '@/stores/url';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import Cross from '@lucide/svelte/icons/x';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let { estado = $bindable(), email = $bindable() } = $props();
|
||||
|
||||
let checkeado = $state<Boolean | null>(null);
|
||||
let esEmailExistente = $state<boolean>(false);
|
||||
let mensajeError = $state('');
|
||||
let lastemail: string;
|
||||
|
||||
$effect(() => {
|
||||
if (email == '' || !email.includes('@')) {
|
||||
checkeado = null;
|
||||
return;
|
||||
}
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
(async () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(async () => {
|
||||
checkeado = true;
|
||||
await checkEmaill();
|
||||
checkeado = false;
|
||||
}, 1000);
|
||||
})();
|
||||
});
|
||||
|
||||
async function checkEmaill() {
|
||||
try {
|
||||
if (lastemail == email) return;
|
||||
lastemail = email;
|
||||
esEmailExistente = !(await checkEmail(email));
|
||||
} catch {
|
||||
esEmailExistente = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if mensajeError}
|
||||
<Dialog open={!!mensajeError} onOpenChange={() => (mensajeError = '')}>
|
||||
<DialogContent>
|
||||
<CardError {mensajeError} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/if}
|
||||
<div transition:slide>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="flex items-center justify-between text-xl font-semibold">
|
||||
Ingresa tu correo electrónico
|
||||
{#if checkeado == null}
|
||||
<div hidden></div>
|
||||
{:else if checkeado == true}
|
||||
<Spinner></Spinner>
|
||||
{:else if esEmailExistente}
|
||||
<Check class="text-green-500" />
|
||||
{:else}
|
||||
<Cross class="text-red-500" />
|
||||
{/if}
|
||||
</h2>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('email', email);
|
||||
const req = await fetch(`${$apiBase}/api/password-reset/otp`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (req.ok) {
|
||||
estado = 'otp';
|
||||
return;
|
||||
}
|
||||
mensajeError = await req.text();
|
||||
} catch {
|
||||
mensajeError = 'No se pudo alcanzar el servidor';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Input type="email" placeholder="correo@ejemplo.com" bind:value={email} />
|
||||
<Button type="submit" disabled={!esEmailExistente}>Enviar código</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
96
src/routes/password-reset/NuevaPass.svelte
Normal file
96
src/routes/password-reset/NuevaPass.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import CardError from '@/components/CardError.svelte';
|
||||
import Button from '@/components/ui/button/button.svelte';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import DialogContent from '@/components/ui/dialog/dialog-content.svelte';
|
||||
import Dialog from '@/components/ui/dialog/dialog.svelte';
|
||||
import Input from '@/components/ui/input/input.svelte';
|
||||
import Label from '@/components/ui/label/label.svelte';
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { sesionStore } from '@/stores/usuario';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let { otp, email } = $props();
|
||||
|
||||
let pass = $state('');
|
||||
let pass2 = $state('');
|
||||
let coinsiden = $derived(
|
||||
pass === pass2 &&
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9])[A-Za-z\d\W_]*$/.test(pass) &&
|
||||
pass.length > 8
|
||||
);
|
||||
let mensajeError = $state('');
|
||||
</script>
|
||||
|
||||
{#if mensajeError}
|
||||
<Dialog open={!!mensajeError} onOpenChange={() => (mensajeError = '')}>
|
||||
<DialogContent>
|
||||
<CardError {mensajeError} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
<div transition:slide>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<h2 class="mb-4 text-2xl font-bold">Crear una Nueva Contraseña</h2>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData();
|
||||
formData.append('otp', otp);
|
||||
formData.append('email', email);
|
||||
formData.append('newpass', pass);
|
||||
try {
|
||||
const req = await fetch(`${$apiBase}/api/password-reset/change`, {
|
||||
method: 'PATCH',
|
||||
body: formData
|
||||
});
|
||||
if (req.ok) {
|
||||
const token = await req.json();
|
||||
sesionStore.set(token);
|
||||
goto('/?from=cambio_contraseña');
|
||||
return;
|
||||
}
|
||||
const data = await req.text();
|
||||
mensajeError = data;
|
||||
} catch {
|
||||
mensajeError = 'No se pudo alcanzar el servidor';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label for="password" class="mb-1 block text-sm font-medium">Contraseña</Label>
|
||||
<Input
|
||||
type="password"
|
||||
id="password"
|
||||
class="w-full px-3 py-2"
|
||||
placeholder="Ingresa tu nueva contraseña"
|
||||
bind:value={pass}
|
||||
/>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
La contraseña debe contener al menos una mayúscula, una minúscula, un número y un
|
||||
carácter especial. Además de 8 chars de longitud.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="confirmPassword" class="mb-1 block text-sm font-medium"
|
||||
>Repetir Contraseña</Label
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
class="w-full px-3 py-2"
|
||||
placeholder="Repite tu nueva contraseña"
|
||||
bind:value={pass2}
|
||||
/>
|
||||
</div>
|
||||
<Button disabled={!coinsiden} type="submit">Establecer Contraseña</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
92
src/routes/password-reset/Otp.svelte
Normal file
92
src/routes/password-reset/Otp.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script>
|
||||
import CardError from '@/components/CardError.svelte';
|
||||
import Button from '@/components/ui/button/button.svelte';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import DialogContent from '@/components/ui/dialog/dialog-content.svelte';
|
||||
import Dialog from '@/components/ui/dialog/dialog.svelte';
|
||||
import InputOtpGroup from '@/components/ui/input-otp/input-otp-group.svelte';
|
||||
import InputOtpSlot from '@/components/ui/input-otp/input-otp-slot.svelte';
|
||||
import InputOtp from '@/components/ui/input-otp/input-otp.svelte';
|
||||
import { apiBase } from '@/stores/url';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let { estado = $bindable(), email, otp = $bindable() } = $props();
|
||||
|
||||
let checkeado = $state(false);
|
||||
let mensajeError = $state('');
|
||||
|
||||
$effect(() => {
|
||||
if (otp && otp.length === 6) {
|
||||
checkeado = true;
|
||||
} else {
|
||||
checkeado = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if mensajeError}
|
||||
<Dialog open={!!mensajeError} onOpenChange={() => (mensajeError = '')}>
|
||||
<DialogContent>
|
||||
<CardError {mensajeError} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
<div transition:slide>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div class="space-y-6 py-4">
|
||||
<h3 class="text-xl font-semibold">Verificación de correo</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Hemos enviado un código de verificación a tu correo electrónico.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-center">
|
||||
<InputOtp maxlength={6} bind:value={otp}>
|
||||
{#snippet children({ cells })}
|
||||
<InputOtpGroup>
|
||||
{#each cells as cell}
|
||||
<InputOtpSlot class="p-3!" {cell}></InputOtpSlot>
|
||||
{/each}
|
||||
</InputOtpGroup>
|
||||
{/snippet}
|
||||
</InputOtp>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<Button
|
||||
disabled={!checkeado}
|
||||
onclick={async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('otp', otp);
|
||||
formData.append('email', email);
|
||||
let req = await fetch(`${$apiBase}/api/password-reset/otp/check`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (req.ok) {
|
||||
estado = 'nuevapass';
|
||||
return;
|
||||
} else {
|
||||
mensajeError = await req.text();
|
||||
}
|
||||
} catch {
|
||||
mensajeError = 'No se pudo alcanzar el servidor';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Verificar
|
||||
</Button>
|
||||
|
||||
<Button variant="link" onclick={() => console.log('Reenviar código')}>
|
||||
Reenviar código de verificación
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
30
src/routes/password-reset/Pasos.svelte
Normal file
30
src/routes/password-reset/Pasos.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import BreadcrumbItem from '@/components/ui/breadcrumb/breadcrumb-item.svelte';
|
||||
import BreadcrumbList from '@/components/ui/breadcrumb/breadcrumb-list.svelte';
|
||||
import BreadcrumbSeparator from '@/components/ui/breadcrumb/breadcrumb-separator.svelte';
|
||||
import Breadcrumb from '@/components/ui/breadcrumb/breadcrumb.svelte';
|
||||
|
||||
let { estado } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex w-full justify-center">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<p class={`select-none ${estado === 'email' ? 'font-bold text-white' : ''}`}>
|
||||
Ingrese Email
|
||||
</p>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<p class={`select-none ${estado === 'otp' ? 'font-bold text-white' : ''}`}>Ingresar OTP</p>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<p class={`select-none ${estado === 'nuevapass' ? 'font-bold text-white' : ''}`}>
|
||||
Nueva Contraseña
|
||||
</p>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { Post } from '../../../types';
|
||||
import PostCard from '@/components/PostCard.svelte';
|
||||
import ModalEditar from '../../[perfil]/modalEditar.svelte';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { updatePost } from '@/hooks/updatePost';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import Separator from '@/components/ui/separator/separator.svelte';
|
||||
@@ -18,6 +18,14 @@
|
||||
import ThumbsUp from '@lucide/svelte/icons/thumbs-up';
|
||||
import { TamañoPantalla } from './TamañoPantalla.svelte';
|
||||
import BotonSeguir from '@/components/BotonSeguir.svelte';
|
||||
import Pen from '@lucide/svelte/icons/pen';
|
||||
import Trash_2 from '@lucide/svelte/icons/trash-2';
|
||||
import { Tooltip } from '@/components/ui/tooltip';
|
||||
import TooltipTrigger from '@/components/ui/tooltip/tooltip-trigger.svelte';
|
||||
import TooltipContent from '@/components/ui/tooltip/tooltip-content.svelte';
|
||||
import { deletePost } from '@/hooks/deletePost';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { obtenerRespuestasPorId } from '@/hooks/obtenerRespuestasPorId';
|
||||
|
||||
interface Prop {
|
||||
data: {
|
||||
@@ -29,7 +37,17 @@
|
||||
let tamaño = new TamañoPantalla();
|
||||
|
||||
let { data }: Prop = $props();
|
||||
// $inspect(data);
|
||||
|
||||
let respuestasPaginadas: Post[] = $state([]);
|
||||
let pagerespuestas: number = $state(1);
|
||||
|
||||
let seguirMostrandoMostrarMás = $derived.by(() => {
|
||||
if (data.post.repliesCount <= 20) return false;
|
||||
if (respuestasPaginadas.length == 0) return true;
|
||||
return data.respuestas.length + respuestasPaginadas.length < data.post.repliesCount;
|
||||
});
|
||||
// $inspect([respuestasPaginadas, seguirMostrandoMostrarMás]);
|
||||
|
||||
let postAModificar: Post | null = $state(null);
|
||||
|
||||
async function handleEditar(e: SubmitEvent) {
|
||||
@@ -97,14 +115,36 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each data.respuestas as respuesta (respuesta.id)}
|
||||
<!-- {#if tamaño.isMobile} -->
|
||||
<!-- {#if true} -->
|
||||
{@render Respuesta(respuesta)}
|
||||
<!-- {:else} -->
|
||||
<!-- <PostCard post={respuesta} bind:postAModificar update={() => invalidate('post:post')} /> -->
|
||||
<!-- {/if} -->
|
||||
{#each [...data.respuestas, ...respuestasPaginadas] as respuesta (respuesta.id)}
|
||||
<div transition:fade animate:flip>
|
||||
<!-- {#if tamaño.isMobile} -->
|
||||
<!-- {#if true} -->
|
||||
{@render Respuesta(respuesta)}
|
||||
<!-- {:else} -->
|
||||
<!-- <PostCard post={respuesta} bind:postAModificar update={() => invalidate('post:post')} /> -->
|
||||
<!-- {/if} -->
|
||||
</div>
|
||||
{/each}
|
||||
{#if seguirMostrandoMostrarMás}
|
||||
<Button
|
||||
variant="link"
|
||||
onclick={async () => {
|
||||
let ret = await obtenerRespuestasPorId(
|
||||
data.post.id,
|
||||
undefined,
|
||||
undefined,
|
||||
++pagerespuestas
|
||||
);
|
||||
if (ret == null) return;
|
||||
if (typeof ret == 'string') return;
|
||||
if (ret.length == 0) {
|
||||
seguirMostrandoMostrarMás = false;
|
||||
return;
|
||||
}
|
||||
respuestasPaginadas.push(...ret);
|
||||
}}>Cargar Más Respuestas</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,11 +174,46 @@
|
||||
<span class="text-sm font-semibold">@{post.authorName}</span>
|
||||
<span class="text-xs text-gray-500">{new Date(post.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
{#key $sesionStore?.accessToken}
|
||||
<div class="flex gap-2">
|
||||
<BotonSeguir {post} variant="icon-sm" />
|
||||
</div>
|
||||
{/key}
|
||||
<div class="flex gap-2">
|
||||
{#if $sesionStore?.username === post.authorName}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onclick={() => {
|
||||
postAModificar = post;
|
||||
}}
|
||||
>
|
||||
<Pen />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Editar</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon-sm"
|
||||
onclick={async () => {
|
||||
await deletePost(
|
||||
post,
|
||||
() => {
|
||||
invalidate('post:respuestas');
|
||||
},
|
||||
false,
|
||||
''
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Trash_2 />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Borrar</TooltipContent>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
<BotonSeguir {post} variant="icon-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<p class=" mt-1 line-clamp-2 rounded-md p-2 text-lg">
|
||||
{post.content}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { obtenerPostPorId } from '@/hooks/obtenerPostPorId.js';
|
||||
import { obtenerRespuestasPorId } from '@/hooks/obtenerRespuestasPorId';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const ssr = false;
|
||||
|
||||
export async function load({ params, fetch, depends }) {
|
||||
let ret = await obtenerPostPorId(params.idpost, fetch, depends);
|
||||
if (ret == null) return error(404, 'no existe un post con ese id.');
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import AlertCircleIcon from '@lucide/svelte/icons/alert-circle';
|
||||
import * as Alert from '@/components/ui/alert';
|
||||
import { fade } from 'svelte/transition';
|
||||
import FireBaseButton from '@/components/FireBaseButton.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
|
||||
let showAlert: boolean = $state(false);
|
||||
|
||||
@@ -21,6 +24,12 @@
|
||||
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
|
||||
<div class="w-full max-w-sm">
|
||||
<SignupForm bind:showAlert />
|
||||
|
||||
<Card class="mt-2">
|
||||
<CardContent>
|
||||
<FireBaseButton mode="register" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{#if showAlert}
|
||||
<div class="mt-2" transition:fade>
|
||||
<Alert.Root variant="destructive">
|
||||
|
||||
@@ -1,13 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import CardContent from '@/components/ui/card/card-content.svelte';
|
||||
import CardHeader from '@/components/ui/card/card-header.svelte';
|
||||
import CardTitle from '@/components/ui/card/card-title.svelte';
|
||||
import Card from '@/components/ui/card/card.svelte';
|
||||
import Separator from '@/components/ui/separator/separator.svelte';
|
||||
import UserCard from '@/components/UserCard.svelte';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-fit w-full flex-col items-center justify-center gap-2 p-6 md:p-10">
|
||||
{#each data.usuarios as usu}
|
||||
<UserCard {usu} />
|
||||
{/each}
|
||||
<div class="flex w-full max-w-6xl flex-col gap-2">
|
||||
{#if data.usuarios.length != 0}
|
||||
<h1 class="text-2xl font-bold">Usuarios</h1>
|
||||
<Separator></Separator>
|
||||
{#each data.usuarios as usu}
|
||||
<div class="w-full">
|
||||
<UserCard {usu} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if data.htags.length != 0}
|
||||
<div class="mt-4">
|
||||
<h2 class="mb-2 text-xl font-semibold">Hastags</h2>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
{#each data.htags as htag}
|
||||
<a
|
||||
href={`/htag/${htag}`}
|
||||
class="w-full rounded-lg bg-accent p-3 text-lg font-medium text-foreground hover:bg-muted"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
#{htag}
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { busquedaUsuarios } from '@/hooks/busquedaUsuarios';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { UserResponseDto } from '../../../types';
|
||||
import { busquedaHashtags } from '@/hooks/busquedaHashtags.js';
|
||||
|
||||
export async function load({ params }) {
|
||||
let usuarios: UserResponseDto[] = await busquedaUsuarios(params.user);
|
||||
let req1 = busquedaUsuarios(params.user);
|
||||
//usa el mismo param
|
||||
let req2 = busquedaHashtags(params.user);
|
||||
|
||||
let [usuarios, htags] = await Promise.all([req1, req2]);
|
||||
|
||||
if (usuarios == null) {
|
||||
return error(500, 'No se pudo alcanzar el servidor.');
|
||||
}
|
||||
|
||||
if (usuarios.length == 0) {
|
||||
return error(404, 'No se encontraron usuarios que coinsidan con la busqueda.');
|
||||
if (usuarios.length == 0 && htags.length == 0) {
|
||||
return error(404, 'No se encontraron usuarios ni hashtags que coinsidan con la busqueda.');
|
||||
}
|
||||
return { usuarios };
|
||||
return { usuarios, htags };
|
||||
}
|
||||
|
||||
15
src/types.d.ts
vendored
15
src/types.d.ts
vendored
@@ -39,6 +39,8 @@ export interface Sesion {
|
||||
displayName: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
email: string;
|
||||
isFirebase: boolean;
|
||||
}
|
||||
|
||||
export interface LoginDto {
|
||||
@@ -46,6 +48,11 @@ export interface LoginDto {
|
||||
password: string?;
|
||||
}
|
||||
|
||||
export interface LoginSsoDto {
|
||||
accessToken: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface RegisterDto {
|
||||
username: string;
|
||||
email: string;
|
||||
@@ -53,6 +60,14 @@ export interface RegisterDto {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface RegisterSsoDto {
|
||||
username: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
token: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface CreatePostDto {
|
||||
content: string;
|
||||
imageUrl: string?;
|
||||
|
||||
Reference in New Issue
Block a user