From 7eae6141c32c4f1d5247a3f74d56a8f89ba5e1ea Mon Sep 17 00:00:00 2001 From: TZGyn Date: Tue, 24 Dec 2024 03:07:11 +0800 Subject: [PATCH] file upload (currently tigris) + migrate old dashboard to new one --- frontend/.env.example | 11 +- frontend/bun.lockb | Bin 227354 -> 280927 bytes frontend/drizzle/0028_hard_red_shift.sql | 15 + frontend/drizzle/meta/0028_snapshot.json | 542 ++++++++++++++++++ frontend/drizzle/meta/_journal.json | 7 + frontend/package.json | 3 + .../src/lib/components/app-sidebar.svelte | 2 +- frontend/src/lib/components/nav-user.svelte | 2 +- .../src/lib/components/ui/input/input.svelte | 39 +- frontend/src/lib/db/schema.ts | 33 ++ frontend/src/lib/server/types.ts | 16 + frontend/src/lib/utils.ts | 29 + .../(app)/dashboard-new/+layout.server.ts | 6 - .../(app)/dashboard-new/+page.server.ts | 14 - .../account/settings/+page.server.ts | 6 - .../routes/(app)/dashboard/+layout.server.ts | 19 +- .../src/routes/(app)/dashboard/+layout.svelte | 127 ---- .../routes/(app)/dashboard/+page.server.ts | 24 +- .../src/routes/(app)/dashboard/+page.svelte | 47 -- .../settings/(components)/sidebar-nav.svelte | 6 +- .../account/settings/+layout.server.ts | 0 .../account/settings/+layout.svelte | 2 +- .../{ => account}/settings/+page.server.ts | 2 +- .../account/settings/account/+page.server.ts | 0 .../account/settings/account/+page.svelte | 0 .../account/settings/account/schema.ts | 0 .../settings/qr/(components)/DemoQR.svelte | 0 .../account/settings/qr/+page.server.ts | 0 .../account/settings/qr/+page.svelte | 0 .../account/settings/qr/schema.ts | 0 .../account/settings/security/+page.server.ts | 0 .../account/settings/security/+page.svelte | 0 .../account/settings/security/schema.ts | 0 .../dashboard/links/(components)/form.svelte | 269 --------- .../(app)/dashboard/links/+layout.server.ts | 14 - .../(app)/dashboard/links/+page.server.ts | 139 ----- .../routes/(app)/dashboard/links/+page.svelte | 375 ------------ .../links/[id]/edit/(components)/form.svelte | 204 ------- .../dashboard/links/[id]/edit/+page.server.ts | 116 ---- .../dashboard/links/[id]/edit/+page.svelte | 20 - .../(app)/dashboard/links/[id]/edit/schema.ts | 20 - .../links/[id]/qr/(components)/qr.svelte | 116 ---- .../dashboard/links/[id]/qr/+page.server.ts | 22 - .../dashboard/links/[id]/qr/+page.svelte | 40 -- .../routes/(app)/dashboard/links/schema.ts | 15 - .../project/+page.server.ts | 2 +- .../(components)/app-sidebar.svelte | 85 ++- .../(components)/project-switcher.svelte | 6 +- .../[project_id]/(components)/schema.ts | 0 .../project/[project_id]/+layout.server.ts | 4 +- .../project/[project_id]/+layout.svelte | 0 .../project/[project_id]/+page.server.ts | 0 .../project/[project_id]/+page.svelte | 2 +- .../[project_id]/[...catchall]/+page.svelte | 2 +- .../(components)/upload-file-card.svelte | 127 ++++ .../[project_id]/file_uploads/+page.server.ts | 35 ++ .../[project_id]/file_uploads/+page.svelte | 323 +++++++++++ .../(components)/DeleteShortenerDialog.svelte | 0 .../links/(components)/ShortenerCard.svelte | 42 +- .../links/(components)/form.svelte | 0 .../[project_id]/links/+layout.server.ts | 2 +- .../[project_id]/links/+page.server.ts | 0 .../project/[project_id]/links/+page.svelte | 0 .../links/[linkid]}/+page.server.ts | 2 +- .../[project_id]/links/[linkid]}/+page.svelte | 0 .../[linkid]/edit/(components)/form.svelte | 2 +- .../links/[linkid]/edit/+page.server.ts | 0 .../links/[linkid]/edit/+page.svelte | 0 .../links/[linkid]/edit/schema.ts | 0 .../links/[linkid]/qr/(components)/qr.svelte | 0 .../links/[linkid]/qr/+page.server.ts | 0 .../links/[linkid]/qr/+page.svelte | 0 .../project/[project_id]/links/schema.ts | 0 .../settings/(components)/DemoQR.svelte | 0 .../settings/(components)/dns-info.svelte | 0 .../settings/(components)/dns-tooltip.svelte | 0 .../settings/(components)/form.svelte | 0 .../[project_id]/settings/+page.server.ts | 4 +- .../[project_id]/settings/+page.svelte | 0 .../project/[project_id]/settings/schema.ts | 0 .../dashboard/projects/+layout.server.ts | 11 - .../(app)/dashboard/projects/+page.server.ts | 42 -- .../(app)/dashboard/projects/+page.svelte | 117 ---- .../projects/[id]/(components)/form.svelte | 232 -------- .../dashboard/projects/[id]/+layout.server.ts | 30 - .../dashboard/projects/[id]/+layout.svelte | 43 -- .../dashboard/projects/[id]/+page.server.ts | 193 ------- .../dashboard/projects/[id]/+page.svelte | 266 --------- .../projects/[id]/[...catchall]/+page.svelte | 8 - .../[linkid]/edit/(components)/form.svelte | 208 ------- .../[id]/links/[linkid]/edit/+page.server.ts | 141 ----- .../[id]/links/[linkid]/edit/+page.svelte | 26 - .../[id]/links/[linkid]/edit/schema.ts | 21 - .../links/[linkid]/qr/(components)/qr.svelte | 116 ---- .../[id]/links/[linkid]/qr/+page.server.ts | 25 - .../[id]/links/[linkid]/qr/+page.svelte | 39 -- .../(app)/dashboard/projects/[id]/schema.ts | 14 - .../[id]/settings/(components)/DemoQR.svelte | 88 --- .../settings/(components)/dns-info.svelte | 85 --- .../settings/(components)/dns-tooltip.svelte | 74 --- .../[id]/settings/(components)/form.svelte | 250 -------- .../projects/[id]/settings/+page.server.ts | 447 --------------- .../projects/[id]/settings/+page.svelte | 397 ------------- .../projects/[id]/settings/schema.ts | 41 -- .../routes/(app)/dashboard/projects/schema.ts | 7 - .../settings/(components)/sidebar-nav.svelte | 47 -- .../dashboard/settings/+layout.server.ts | 14 - .../(app)/dashboard/settings/+layout.svelte | 28 - .../settings/account/+page.server.ts | 72 --- .../dashboard/settings/account/+page.svelte | 118 ---- .../dashboard/settings/account/schema.ts | 10 - .../settings/qr/(components)/DemoQR.svelte | 88 --- .../dashboard/settings/qr/+page.server.ts | 87 --- .../(app)/dashboard/settings/qr/+page.svelte | 237 -------- .../(app)/dashboard/settings/qr/schema.ts | 26 - .../settings/security/+page.server.ts | 171 ------ .../dashboard/settings/security/+page.svelte | 190 ------ .../dashboard/settings/security/schema.ts | 22 - .../routes/(public)/(landing)/+layout.svelte | 9 +- .../routes/(public)/(landing)/+page.svelte | 27 +- .../(public)/(landing)/features/+page.svelte | 68 +++ .../features/file-upload/+page.svelte | 20 + .../api/project/[project_id]/file/+server.ts | 107 ++++ .../[project_id]/file/[file_id]/+server.ts | 109 ++++ .../src/routes/api/shortener/[id]/+server.ts | 13 + .../src/routes/api/shortener_new/+server.ts | 1 + .../routes/api/shortener_new/[id]/+server.ts | 14 + frontend/src/routes/webhook/s3/+server.ts | 110 ++++ frontend/src/routes/webhook/tigris/+server.ts | 128 +++++ frontend/static/features/file-upload.png | Bin 0 -> 70318 bytes redirect/.env.example | 9 +- redirect/db/models.go | 67 ++- redirect/db/query.sql.go | 40 +- redirect/go.mod | 18 + redirect/go.sum | 36 ++ redirect/main.go | 109 +++- 136 files changed, 2102 insertions(+), 5754 deletions(-) create mode 100644 frontend/drizzle/0028_hard_red_shift.sql create mode 100644 frontend/drizzle/meta/0028_snapshot.json delete mode 100644 frontend/src/routes/(app)/dashboard-new/+layout.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard-new/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard-new/account/settings/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/+layout.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/+page.svelte rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/(components)/sidebar-nav.svelte (81%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/+layout.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/+layout.svelte (96%) rename frontend/src/routes/(app)/dashboard/{ => account}/settings/+page.server.ts (73%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/account/+page.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/account/+page.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/account/schema.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/qr/(components)/DemoQR.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/qr/+page.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/qr/+page.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/qr/schema.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/security/+page.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/security/+page.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/account/settings/security/schema.ts (100%) delete mode 100644 frontend/src/routes/(app)/dashboard/links/(components)/form.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/links/+layout.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/links/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/links/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/links/[id]/edit/(components)/form.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/links/[id]/edit/schema.ts delete mode 100644 frontend/src/routes/(app)/dashboard/links/[id]/qr/(components)/qr.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/links/schema.ts rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/+page.server.ts (94%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/(components)/app-sidebar.svelte (53%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/(components)/project-switcher.svelte (96%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/(components)/schema.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/+layout.server.ts (93%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/+layout.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/+page.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/+page.svelte (90%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/[...catchall]/+page.svelte (88%) create mode 100644 frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/(components)/upload-file-card.svelte create mode 100644 frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.server.ts create mode 100644 frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.svelte rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/(components)/ShortenerCard.svelte (88%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/(components)/form.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/+layout.server.ts (86%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/+page.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/+page.svelte (100%) rename frontend/src/routes/(app)/dashboard/{links/[id] => project/[project_id]/links/[linkid]}/+page.server.ts (99%) rename frontend/src/routes/(app)/dashboard/{links/[id] => project/[project_id]/links/[linkid]}/+page.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/[linkid]/edit/(components)/form.svelte (98%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/[linkid]/edit/+page.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/[linkid]/edit/+page.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/[linkid]/edit/schema.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/[linkid]/qr/(components)/qr.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/[linkid]/qr/+page.server.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/[linkid]/qr/+page.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/links/schema.ts (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/settings/(components)/DemoQR.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/settings/(components)/dns-info.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/settings/(components)/dns-tooltip.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/settings/(components)/form.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/settings/+page.server.ts (98%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/settings/+page.svelte (100%) rename frontend/src/routes/(app)/{dashboard-new => dashboard}/project/[project_id]/settings/schema.ts (100%) delete mode 100644 frontend/src/routes/(app)/dashboard/projects/+layout.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/(components)/form.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/+layout.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/+layout.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/[...catchall]/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/(components)/form.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/schema.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/(components)/qr.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/schema.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/DemoQR.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-info.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-tooltip.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/form.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/projects/[id]/settings/schema.ts delete mode 100644 frontend/src/routes/(app)/dashboard/projects/schema.ts delete mode 100644 frontend/src/routes/(app)/dashboard/settings/(components)/sidebar-nav.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/settings/+layout.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/settings/+layout.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/settings/account/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/settings/account/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/settings/account/schema.ts delete mode 100644 frontend/src/routes/(app)/dashboard/settings/qr/(components)/DemoQR.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/settings/qr/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/settings/qr/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/settings/qr/schema.ts delete mode 100644 frontend/src/routes/(app)/dashboard/settings/security/+page.server.ts delete mode 100644 frontend/src/routes/(app)/dashboard/settings/security/+page.svelte delete mode 100644 frontend/src/routes/(app)/dashboard/settings/security/schema.ts create mode 100644 frontend/src/routes/(public)/(landing)/features/+page.svelte create mode 100644 frontend/src/routes/(public)/(landing)/features/file-upload/+page.svelte create mode 100644 frontend/src/routes/api/project/[project_id]/file/+server.ts create mode 100644 frontend/src/routes/api/project/[project_id]/file/[file_id]/+server.ts create mode 100644 frontend/src/routes/webhook/s3/+server.ts create mode 100644 frontend/src/routes/webhook/tigris/+server.ts create mode 100644 frontend/static/features/file-upload.png diff --git a/frontend/.env.example b/frontend/.env.example index 7cdbf76..77de6e2 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -32,4 +32,13 @@ PRIVATE_GOOGLE_CLIENT_SECRET= # Stripe PRIVATE_STRIPE_SECRET_KEY= PRIVATE_STRIPE_WEBHOOK_SECRET= -PRIVATE_PRO_PLAN_PRICE_ID= \ No newline at end of file +PRIVATE_PRO_PLAN_PRICE_ID= + +# Tigris/S3 +PRIVATE_AWS_WEBHOOK_TOKEN= +PRIVATE_AWS_BUCKET_NAME= +PRIVATE_AWS_ACCESS_KEY_ID= +PRIVATE_AWS_SECRET_ACCESS_KEY= +PRIVATE_AWS_ENDPOINT_URL_S3= +PRIVATE_AWS_ENDPOINT_URL_IAM= +PRIVATE_AWS_REGION=auto \ No newline at end of file diff --git a/frontend/bun.lockb b/frontend/bun.lockb index e232ceb3de492e349ec5ad87ca2f101ccf913926..b928e424272e8758c64e61fbef30deed3ac3e8bb 100755 GIT binary patch delta 74919 zcmeFacU%+Q_CA_Kq6`9x6;$j9C`}ZDlmrB&C@KmHs3=W9=}oW&5k*i`7=>|L>A@4eh-WhR>Qp7);L?|1LH-}}!yAChOSXYalC+H2RDK$!np?@pn?Oe^!_ z^QY~5>UV!(iKoaxK1I)6-=tSs($+;2H$Kesx?XGCzO$ZMBD&TW3~ZqsTP#s2{98kj zD06gtxJns>gxb)P(;{M0B1EE{HF$kJP#3%luqki>ur6?>zz+iIL61mI3Q2&aKYFZX zL|mFE3+ehuPY3Gsf)E9P41|QmC&i&a2k7L;kXj;<5%4ImF|db#3EF)66F680yaB^w zD(fmXRYvIWx$WRPxtWq26CDbx{@|&Wr{L=W-Ju%*oneLW4)Um)n;MoBk{lsw10NfJ zPl!$mNr^}n#g0Y>ToP1C`BI`H;-ZIVh@K)J=^ub-wlcFG1Tq*I6HoPuicl+693K~k zD2ha*5SD}tk*E+PJkKmZW@^RTsyT#P`QYcr3&xDIz&OCM`nr4$WwR0@XmO;0BN?hzd!L9~CDOSvKN3Y!=+7IA4HH z>0!yq5y@d9(PZ$XC&VYGfD_$DChFjDwo=kUX%CUy84D!qi_vA28izM3<%F=K(~o6O+?_7%`UKocNM90MSQ z_=hQv)dF5pp;DAtCJ-A0oG0KU0h53fp`ily60oa)ra+oQ4F#+r;G1?le@DQi_?Vc~ z1d(W1N0A6!l<5v^ri9QQ0>(k6o{;d~hM($}f#`+It-!XxLSRc^iop8<+d#JlBI=oS zfi$$=So3%RNaag`7$=!?1%9lchXC2_(q`hhY^AiZQIfq#1XnZVCj5AzDVr3LE-GTh zHCrhC9feVaR0HHn4hdIvS8NguJTw42MJYTYEhHv2LKGH{ae^Fr;Hi5jp#>DB3Y1aSg6bKho&oCJ=OLABzYLmak%dZZrLPJvk*Q8a*UBV!|7~4W7y+Qh(qI z@ZO<7y&vZV z^Nx)M(%|$M%9nEza8y)uSQN@Dqmv`j5mC_@B+&F&I*bolLP$!K<;aMPco?7xCxq}7 z#74(OCxj#?|8-*@j(il!BcXhSr9cY(X0(_1i9qVwL5Ln1>7 z+G1ynw4{Y6`A96kP^71*#+axSIytrmNMoQ#K;(~(OR*Qtg+YoK^o02M7*RHK8l>^b zG4WKPUjpxt8Cp#XQU#Fg#YV&qrwP*!Z%S&xHYCs>DF#vt<^m~X+Q62;>O_9yIRhj^ zn}N-N`M@^7SRm!|5zq?Q0=fZ^cEtB^gzz$u%IyZyU|y1d`A<7!cMw$Zj?sJtbAV)U z6p#!J7I-<33^WB&#osdcd^ZH#3#9yufmC6ZfZ;$Y=Lw{Wt$y4ksBVL6q_6ulCYTd)oZLAna%gh5CQuEsXs!J!cw9!ymL^B z22kJ0eCg!0NGsYo+yq_C+d!|6d~F22k$^RUb-}-w#Gf0+!ya{4EYJW)gG^rt)ExEA z%avTd!jnL9cRkDKn>JD3iiAfbriNgMsBTG++Mx#Pt>0YvFUJp&TI+gt0 zX@mxi`ut~)Fq6~5v4nlB-%iTnVrgQAuI5Lm50DP?jo0u~;4u(GJ97sR zQ!_JfEx$ROfo=qSxsWeU(BG`%7mTp=d_H@@-Zk)4J^}ffC?Whp0ujbQ8jWQe`2w4O zG!kMq@hd_BbXqA=fiy}BHuDt^fllS$p**b!leh3Ik5sVxX%)W~TmrU6di`zuTKr`z z)>v9&Tm;#+n9MM2p z>L%~u9ns#&H^>7#IS?8@Dw(#s_PemgQpoEefhtZ-jm8cu61_ozR?zPQ^?~I;a&!yO z5V!2u6daNJ-21wRi@Q@_hwTIj=+f>h*XH;{SJj>H-7i$k+l% zj=V4BLv{s74u=7$f+fJlz*1mK-~=GGyeH5Q*cNCC)B#ch?(g9BBS6ZxNC}}GgmowQ zP>z94yGMY4G9Wo{;Uw=+Tj=$n>jJ5QPbK^yy#=HS_MYY=un#WC^afHzR)T&M1`Sy~gOSS1m-sgNT;_+@JRmLoU4V^&hmcNN`&JcX-F3)9Bj@gk5E(pI}gZ7RkH8j|%mf ziY1YQ3&AFHiEa(F3_2E%|myu&PXeUZB;GZO*@ zO}%%0yT$8j{sDA~g!%!T`k<>mhmNe;w!5BOyROvY$>pGt<7|V@{W$bZ@792ApPK|| zD`Q_xO*`51@Y8EaVLePf`JVeea95}5H7mQ1ur%^Kb6v3}Pj{`J&z)e|`}ec9c;)6z zx<5K|S%&q%q}mx{-nyUN=`eiU&b#M2XI*`+RT}uVMgxO)H51vH+97AA%r*Xgaf-1Wx} zdpEK81m{yRC9;s9k*1@f|PqI`_=c=V4vjsrMp^Cl2T{Z+d3csh>TM=QjTK zarzbWr)L&?7?ylw#b$$yOv#cegL(s^&fUtW_w49;!}DJ(SKN=D8C!kt;=z}-jz=#D zkG#KO)rxiR)?4Mg_3T%3<(zSn<&IZeeYQ`?+&9f_i%!?`PTOajTsLW;)531B(qg7V z)0e%x&Y3hn^}hbm`**YNa3=jf*G*jWEA>dG^PLkeCHErR+{h|0tuhGqjh{R5dg~rd z_wVs_zv(*1Hfh67pKsGIew}XeG$Pt)OPgbZ-9l;14pYtGw6NtcXzA7P#id~7`MXy_Ne z^16D%>RUb?EKRVT{V~$K?&u!l%$zNv+B}j?nDWMP{y8V9`Gr0!S1VhET(>>2zGH9Y z@vD!=+aEA~X%@!qmH+5k$8AB;xi!5n{p#hir?2CK=0|E|-#Obk?YzbG#o#?_GjBx#ts&_CqxQ=%4)kbGBn=@B>jC&@T^Qfp|%b~4v3s#yfy0@)wo6lnL zaMsI2A)d|V;{6j_j`zbX(@r4~Yq8$#FNlDI|)TY@V51JccbdQ-~{A##|v0)ndKP z<>E9p*IXgK$(Dm^r_JiLmy0K{UhNg)Q*3T~g~Xa+PlB4lG947mWvm=Fs9eHUn#&|L zb=bTPa&azO-a#SW%Q6-U@mto*LLqUj&E{Fi#beoWP#amsQX&4%dRZzYj=F4~rJPB} zis%h9BG$LPj5!TWu4<7%dztt=>t&^o$n@DfD>*X-`(YO(X|a{2GVy+vu~ta*>agC{ za`8Ym*IL2k<7S1*idkQC8FLC+H$FQwJ*>}EiI8pHI2Mh^1s&Va_- z_cfI<7okz~By54TOsbDVT4z=d@$Cf(wj-RhAy8qd%7OdPD0?l6lEloI^>&hr1KC_B z1v3xF7AKV@^aFDV8hKEY0>IR7&KIk}7MRJzv23ohf>{cVa!6T&b~5G;GzByaDodH9 zV++<>E@#4Vz@VWhW(#a(;`wa39FYa*qT&o3WDOIT{t0ccjLER`lQ z=?iF1Y`&=@V~*p8J$QIwA!FhN4K42^V^;l6^R<$_Xf#wrY(a^=wj;ka@DqC|G+|(3so~13o@ap>s8J54K`}qj)o$8>kRJXUhW> z5_@-6Cx~`-uONl^B%2$gkZNIF?!x8=IWi8AsNtB+!|k=9P)oH{J^F&p9i(7d_26p& zhmkV~8d`}K6rg{LpwY^s+Bldy(E33`pwS7g9wHHM!oQvfQpNN#HVhB#9R3SiMQO%M_pSEB%V75i7#Q?TU{a?QcpI2 zsFM~1KHFzVe75d=)ZBDP-P!zMPTCM?wqc}olu4w0S?_SU7zcrHg=BeORtLwpDwc^* zFowSB*dm(Z!E7$LdEhAe*w8R(t_d0+eItaP1|Rk?2btKP%^j}L-36{M)g=;%*qJdF z?7{hrc7MK)`N7CQiNMd5wb1^|oCm)(v_aY&3y=e=XXhv#2dTGem-_@MKvf#^PTFT6 zZ5cQxNsA!auoX6r(o>N7vU(PdOjBf}MWO~pjtPZEV-;)55E*kF8vLZOXD?%%1F_sg z!#qQrCPJgAVJ=~Ae};yIi^>IJ?+#+UW8{*QAT}>X&TJ0imnc3qccD?sv{h?DmqGkQ z!Mcc%A17!vRD149XcT)Xt*6p^(A?>K!?YW$9^*83MnFSjgqoWTVNb@%nVcbfm93b;rAva*Qi>7t9uj}h$01qTZz#M~Erv5mLa|z^)%_DR zVZ5RB=EKyQYBoa?;spOTLZgVP7HH{fXgIx^J4(z#SnnjcBrk-`OOi83L-;tYT>32v-ke>bwX+<4<%; zp}C-#7VE2!F*PFiO^x4C0-;f}`GtxTG~~o$bPpO;t{MP&%s7>I{t^pOtVt4H`BEnq5{hNm(>|GDFV10!e=I6R^VwzIZ*= zhCK!vP8OLeO}Y`93+;1Et&x0{ssn(;btLOOMlKmUlFb8L8Ofd;BWGH~@I8wYXOc`Z zFoyLWD`$3rq{yjGd(v;vIFo;* z_bFHaSV-=wL41WIRcQ=JY14SxmoV)aUq}>FG(23!%pe{bdfGxJIU3LADdkMV1b*ru z45%s$8tpeI5F=yUl5ieVYm)0pY+k0EX_?G>qdHPc2SIb7wlK3HQIIh-`^hAili9op za%sa9^aGne!I9|=36n^$ya5{KrJ(7gYShCFg@(y5r0s@=aVKak(lo7%{}=5Rv_Cc- zM&bPSyBzy3+H+`sYVb(sR}{X@i=g%YUG5DuJ{|_RSE?LM=To04PTFJCZh&ScLE|eX z?H)8fXr#%;s;eTc5E|bY(mp}sLqghsahh5Qv_CfMjaN5@a*q5L?Fcl!F_hLqscsBu zY0&t_K$Da!S)E)t(=3xefAT%<35}+K>Quz!L!&u`DUQA7I5ezYSS5dv1YUJQV%#V2 z3#@9TX40X#APpVwER&p_!0JquODwZkZ$L~In+Lcvkv%z8E;Y`^KFQ|$J2HD9xuXpB zfqpXPm!N5@&R=emXw0Bi$=FG(PQF~Sc@pcLFK32K<|mQrq$gQCnLP>efe19Bg7EIu5N9>!WaFzCxl| z%&&${xgt>zH2xSm2O8};{F+(?je^Q=Y{q$N$6-%0G>`QzkTXj_QbbWDy5?#gdlIC< zRGvi5wlay=RMxvt&Wr^~v4Sxyg$JQgMJR{6j`z@n-orX)Gflm7Ag3g48k@(;nT;TA zPylN`ZcJ`Nvx5ddF~RHS^J9nC;-Jx#;kDzMY0}oyF_KuHSx(vzLVjnu42^~Cv zet7fRTxc}BdF|7`XaibSp5-H=I)q3Tv#j?#x#S|t=FO8!b!Vwg znDZQ^zL0PmP~;?@#p*0jX#GxTH=B;5`N$Fv$wMXWfP{l7xNpBx-RIB&mbd~)e2MFj z`lvW_j#4Y;JBc|~r%0g%mCsUhuF6!AlNJP?ngt0*;RPy+uh49s>c9-+$&mPLdm-@^ zGV_V^S?Ht%!I`b-;K(e2MB@gH#wKzRnhl$8>&R#o@sotVr|k!=3!e*5o2EePr`l?+ zL$alFj8u04m83JJ6C`I=FW*s`0SWi*`HsweNM0z<-?>^WRBu1D3y*+CBc9&`mqEjw zHcE>ZVJxVmU`Q%E(ru7%|BqDt#j1OFNXd}+66b$Q9ha!fPKShhc;tKhTXI>dwo(KM z_wY!43klB?@*O4Jm$4^T$fa4!sM3nnPFfI#vK1>FrCpYjRgzdpNQCqR(jb-OR!kld zw;B>(qSgwvG#nCN_AsPCRla5`)lw=X-pV;h168S}tH>kR(;p3K5F%7VBodWLSg}ca z*2P4diC@hx%2@dFWy~6AbT)t%FO#-kgDKDETRSqJA<^mt9xIx|TK=Ym*Oo$~m5|q7 zLF@Nhb6v+T8mcsD4zzw`Pgi$6Kk>*Jc4m-;%9YU|e;d*}0gYVILKf^M)*UF1ZoLgc z)l{(VAiiKINI%L-8;kA^Xt?iEHI!@@ZfCSN{+`kuWV%X<0&>aFb)dnO{&;n%-Jyv?o%#u+9eYPX7R_=@C^Z?Y#vfmi6i8$gG4kj5oMV zreAaGpUgXd_mloW(9VKo4SE*nh<~FQ({22ogM~^V(~pOyZpNRojC4DHXQ%2M{UOlQ zPL}+scOz)Wf3Kis3E#1p@y;@7FKDVmr+y|Rbr}DQGQGpQgmJ*fC5Dd+v-?kG^(fFy z0vRn-s3w^kAbWwt#$+QiFx`U*A!LC;Q8SEYQ&mt`B4rrsv)<95=H5S@OnG$Gc?|rK zq7K==lxB7##g4BfP{zE17DTnsm^bj+_a{7mN@W%>+DNcLHiqsE46Z;6({Sx~wYm=b z|8$_bUAm({`U~0Bq~2+eeFYMONC;N51Ndbp&8(T57+r9x$p(*p=VV@j4X2!{yOn+i z`LVBBgAGy_VXC`1KdJTR7VXLUTfCl_s4H4UeL6 ze|-fKwS~X+Fg?uAaFwQ;3N2ZvQQdgv3CJ)aS%WSz{hmkoCBFt-{WEg)XMt9)Wg7G= z(CWr%(7lfS844P75$F)1dJXzJX!YRIpa&iM)BikO*on~t8UODlnjZhts~RQ}K>tnd zJD~rjLiq`iC|0n&@ArIU_JMW*tqQdM4`}L66KXo>!swO%8|yMf#i}BUSnm04Q2+bM zKfR#gZ2T!4KZNQva_<1GUL|;%meam>-oq=<=!iu<_-9#RtSb1)#b=rV zP2Ck5l{Gl?*UD&;>qmfAyY{CmK&qDwjb=1F`?r?UK&xXRm?&oSH1hs2!8n}b*B-ug zh0yqcUXU)6o`Hr>RqPxk)|ITzS~)YmlJ6cYym%nAzmm-Z`4S`@PA~=WFrnS~-yUCw5_z%mXpt-Blr0b#C z(C4Vqo5a&ci%iQ~e2>5rgnaNV`e>P%50Xr(&gP8yZGI5qb54a!a{e}Za+{pdy`!#` zZha!4^-$I0i;u7gmp-}<;uJy8@@prn-s`wShh&8a2 zNvA?{QGGCX3X%=VsP09XZ_pf|;ZD~E4-fCLCwDlQcg8%S7hQxjZ)AAs;e|M;E?{&=O(cE^USw}MUUaEZsTK&e@LENwxHORrtWmK%Qoap%kpmkA{y!m= z-z?-Kq;gw$>>=7J5IAM3E={D2+XbGGbQ}>yQoMHKMHiu%wcFi7N!1)A4X-l1==x8n zjr4L=E*_~Iwlh_EY*ea?knC3Q>fa)by!$ki>p*ymmo8pE@v4EBn5_N<8$&k~`2QXX zL;1gFQ04dEI@$n97D5Alqw=rB|FG^%o=~5522$O2f&-dJR2P9Kq@nBpl(6}G8!M^9 z6hi8MLdxPIM>0ivJrGQ_Cj`4f#(<`6dbZlxjf}DQz;|DB~0$>A8YFRnVsqiHnfL ze7sSJ3k05!#?Bl;|2sswQdEQtwD&CqQr=|(E*G#ENPV?R&{qS=@CHHO2&9XU_-z8e z9Y_t>A*5>}Il5cLqyI^u3ibhs+pkLCk@zy`Wau!EDmVe8Eamj}7o>)q6?j4_S1IU( zWbY!7s7v%lK?jb2$`jOrFG7MQQU%|I zf38n&?2^s#sM|3xN+65Jorw&5J{}V`6IjOcA5};96xB@9u z-2_Me4k_JD$mb5E0pS5`3Je8O2S*Eh43MtBL$afc6$+#PiAWU+5Ry1b;L`=n5OB1B zV+0%vr0egHlE&jr4>%P_4o?T7KBZ^|FNkIW@gpjvHvuUkoS+kugL4I)ko0*#qKfcF z2f($!+Q1S)KSGJP{vOf)f1mLGjHi|zM}-XV9FWG;MIbqPNpOUa^vl57wA@q)#62LL zWj_Py`kz46a`)e;h+6aoj#D4kL>MR%488pYDW48_!rC>^|I}DL5Ht;&3JJ}C_z|@b z^p*m)A`;hs4^{2|dxPYVwNSB*Q1O2qRYzV_q-mHAc=kfYU4b;8U4X=U0x4qN0`>w@ zD18L%4aASg7jHCs1_*i}kn#lqsr+C8mBA1wW0*ih0I8x_L5~OGN0cP!DFUVeX>^Yk z^sxet2U7XTK&mhYNcpA;>G?p~i&-G@;kW~VLcIn^1=b3<4oLHOhoF}N=^}Ihz5x>T zUcgU4D*su~zX2(764Xu&tRKBnf1?vH6-ZoVw@CX~C=FkaS0V#BC1ic*) zKce;mS^+8D21xcg0;ybQ0cAk^h#Uo7PDta=6#^A>6R?|0Yib59tEUwBLzNQ(31pA1uF3pW#EnG?PMV#7f5_Q-l&3EK)R=10wlvL zfo8xn0)7NiMPGoFpW;mo5DO>)(wNWzQu(^TcEB!D#GeQ!5afUtPzTr_h#ye^-Y7ko z6amTbQ0TN$rUI$Lu|VR-2{;}|7a{R<%AqlqFYtsE@flLYpM(Msa9%W9NZ^1}a4wK8 zLdrK^&)GT=w&B&Cvx2WLc>nf@-4i{-e38kv ztqadKE?gYit8){b3%QTHKXA@n#D=}P4ahh#bNfJpH?FnoP8w4dWp&AY;l_~-?tGC| z@0qOZdbF=eP@v=8WBt7!EcM@&_@XfJbX?X~*LD-F`<)p#CF|-a+#ZWXKkLwM2{7Cn zJMlp7a~H85=if!#Q(T){+6BaF83?HigdXRMFKyxi>>-s9QHK-TgV62@BElYo0k?&S zokTS33Zgz2(iKFM1BjzUG~^5%Ko~lLNOb_wm@6aV1Q8~VAdI*~M-b^wATANnlxyV# z!ps@OBqtEYTqO}#iLi49(Sn=c3}T8L#6u!laUJn>RGhs6#B4c;He3}EPl)KQ0Aa!v zC_v100r7zdQ_j@|gr_Tr6)qsmxi>_7Cc@tpL?E^yU z1Hy;%^#Kvk8$<~aeK>J%5ZZk}MDzyX%WWZIClL+%fauSK^Z^mo7sOE_{5gZZAPjv$ zr1k|dkSinN1Q8~_AOg8WUl8g2KwKhX5Z9_72($hmCiMd`gsUXtDiL=5K@8<4^anA; z55z+vLb#58Ang4?%=QBj##IsVgoy6`AR@Q|e-LvAfcQW}Bnci zmU10OfUqA4V)h6S%eg8do)FP}B#0GU!AKBuV?cZ$Vio5a1Hv;F#EKXYYq&Q=d?vy_ z7Q{MkX)K7B0U+zB_j54t&%~QrGS`}3}PQw2|~P| zGfx2=;3g0paEz-aIL^6_0-WIH5tMUp2u^Zd z>3~z*QUJF)164{hP-O+@n*kzVG>8%+&T`_>AhgGTh!_o`lG{SWP9hqP0dau~83R)n zxm^U8ID@f(%Um?U6|RinDrYnfaE(hOxXzUm+~8V`2i)W`2ySte1h+YJCEyM>f#5E8 zji8F_mpBxN3sOoNE@~2{(_RntMa=l=GShc*ZRSaI3Sa zt=Vw=1?QVhZJk7ICE^t)ov`e<60wA{ohlvx z$eoJR8eA2D7T0weK*|*m)a0rOYH_al0Bvp_0mHo^(BZtM18Q?i33RzH1bUqB41hkj zhM*27o(ZVS1riu=TL|iLOaY)i7Xsj-3dMTL6s^|5zb>rp*g$7O^C@d*8qTO~n(h?* zi)*{T(R&MvS}Q%8Ic#_99XDs@`9i~P%7t6KMtw?^Ymp_{NbG@E?(d=877W8vi=d${I{IlfHmRs-S8yNjKv(qv- z>`T`hSEGjSPHbHH)avtrmDlU}4CC}zv7wUfX&amU<4pf=9}nq_@;_FT{iuUWy~_z* z;%$x(ZT{$d;<8%}oJKY4+wY7-?!MC1HgoyCNgtN!^-tPX9BMwW#H^FXnW?d+dkuf? z^_b_C6LbHwwEn)$4Sr4A@~yazhrQ$1LdQ-er3M@KbmyL8QM_lIujMr+QzV{2QteRpuc@o%?u4HJ@6f7VuR zJpbB-i=74cJ`b|0*3YVHkUQ1dF6?;b%8uek(%4Q+~q;e$9#ZQ_bogl&}7tIPy-D`aiEt zx>nY(V4%&D2_6duB`$Aeb@Q>ShI_`E?u|b%OLV_xQ1hb2lj|8gFViWE{NQE&F+07^ zxb-?Vt6%i78~w)aQqBycuKHt_>AoG$EN}VpN67t!<5Hh)@_d{9zPT+|LGCS7baNSU zq-Bw(@6&Sqzp6C%u=^OtelxH)jI!H=nb$ zc5g=u=M(KEeUF~5E(l)ns7d1!ofb}hzWd4XuY*_A`s)5udLyv&=ND-eYiE{nhjbrL z*s;<4SUhb32)wKiacb(0VPo>x-sqecp$dw26J1u)^w` zk<7!^+jx8;*A7*&S7bNZPy|5?hi+-EB|OAhYEj6XN1Pj2eCrcR9(2H&Z9@MMdh zPVZa9YrmV~cgbVJ`|LFp-rSl7vavn9KG*2u(Coay>8pEd_;6z;x^xk3Xyry9@Tl*0 z=r@qnmsZy1oRd>B-ECYAzi+^719~eTTZ=xmF{%`w#g#qv1AL zI~%q!tu>epeBJL}$Iu(~?ke5%=bU}a-6Hq;w7WMb^x*9t4M&aIKRe3PZcxecxIB~1 z2@W%BEc{XXev40!-d@T*w&z8v@1v%dK207 z!u8FjXOH;N@WF?Pf&2DA?-FvbptUI69Lf5MOoyUis z7@6BkF19!^ZRIjatE|LX{li+09R9%8yWbM)VMq6WKGN{i!&N868t$2DMz8CJ#(F2C zKA10`-16|pM?3nrk?r!ai*TH>{z~8hrv`n@8(n*)o* zPdTx@UEMc#W47$#oaezkvB$_(1xHIf+l_0J-egl`^A{&(CO5g1c~kbR^Rxa#i(amA z^U6JSCcwoerDSHe`GYzSc=GXj;cMM5Q$NM$?E7vrN5egHP4|48lumH2EVeJs`K7Bh zTl_^luj2ilnsKbF!|I&Owe=6X?rHNNaAUBMZ~FVe1Eu30`Df`}wl$8sa&>;NQo8@c zn@H{jxwp7K6CVC#L&uN$-`N3*(z1+OcP;LlHsjKtnD!3P^BWu#b)3EXEV$mHx|=$V zU3F#A4Kw@eW8V60Ecmg$HnXNphK73`G~KgJu$tUrVVz*<{dlX`)^R;D3bwpkbJXHh zk4BUI9dG>%x~S{(;J8he(Ojj;Lcdo_E}9M;Hpn!`?xC*HEswr6D}BPbh4bOwlz_C* z=#QH%?n&#N-FbhD`xmR)Ek0&89=P3LBjeiB_hnJwl#`1L-nRTYIsIDg$eGQS|Ll?f zsIr$qXh7zt&0U^#&~VRE)4jx<<=ckY&E3&%ZOm-0!fMI(^6XkIP9NSX*M2RYx;|jJ zvE}{PGhv;KK1}oBl=t0tcl5G+UAU&jc_+QJOP5Pq;V#Li2<~~T&%3z0Q(xyEDK&lN z^(&M1J|-Qy=KgLXG=-QyuWy6J$4GLcd?t8!gNlk~6;up&UpIOK5)H`d` zX7hp%7rqB{KRK{<$^K`>Z?{iUM6C_Z7`BP~LGI;sdaz{Ym7U*)9jR%eKiOp4(I!nl zc;{*LUVNn}?%k=Mc}K#ER%Tool{@QZ&x=2I*81FU-?6$|-*nW@?mmC0?#gYqHQei{ z>0a^snQkLL?tc}Zd}7hzJr<`+QylwvKI}aBv3E4Q+5AId8 zIJDvL)Yp2}PY>;7bvn&?C$8oG(E6+UexKkPT1Wb}uJ){JWb)qm5i80+fN zr@u~H)~T6U^3d0ILXQbGo~&Ovb6J5-j}tC0E2n&$py8gKrhB)}dbZ3DzmRZuU!wsN zPgM zy559SoZce1cg*ul#_`8y(e*Yzw~S=pz38dvvu0h)`wr{8s=h3l{%~j{%k~T!zFstD zWv|uuoY>2vEBj~G9DA2@T9y@FvDm4nhI=wi_e_u7khRVW4jvJ5WK^}S{+rlGei0rU zqql6i-A6RFM`6sigGF!SEV>xA3fi@3Rm#+?^LFOqpQp$9Wkkfj8M5h`U0ZG+xz{LG z5__i8re2fJb*(5r{p}Rf^U+0V%J!mHb*DIge|BWfrx0y+NQ}qBXS*-8U9b2!+P%K- zvcLlQH_M-Gh7I+&yjR1$uA1)sSl-yFMu<&yFS+b&N&CZNKh0K@XH>V2ID6~O=+=+2 zOLranGO)(1Pe#!@((-&Qblq-;UHo+@S-H1z)u);zUV9b?aIuTw-sRx-BPyfsexCJI zesRl=^?j2*`ulXbdC~P}t?MKEcV6!>dqe&7i5Xdvt(7Ono!Q;frC8r~%=6`e;`^iQ zH!jv&6rkarqo#YONA12Jnzh<#pw*L@XGS>#v=UDB{@(rT7DbGKexdD@p<45gaw{*` z7%e#6)PG|NS7(lW*vQcM2OWSkh<#UtBfLvUz+FU!< zvwlCTpujE4hTSro$apJsH?;USIu~>gBPvojRCE z9(EO5FRmA}cXJ*0Zdz81xLeWuiz{#Fd-r91oubSQ`&-u!@m_Qv&xroGr_gk7n555# zZrvjqjg>v@uuHb~)#(RmcD0He4WEUkuU;{)%A~AHTK?9>+ZklN#*3 zqcto`$8h@XoN??HMX#M5bPly1cdluzYfobbysf|2pudKD^mj_?OG`4aNn67ME7#^~ zWjpo1&}_xo(APR!7N4zG_;bj&x&trto)A{FSC%o^$Rcf*l}_}?t-p-Fw>-Gh=*eY^ z&)N>VymH++%Vlt{x4b%X^2Iq5{S?dJ9jZU3gVJF~_{zHOKe}CPS!r+Fx%$ndqC;ED z?oWMc3t^+_pN#)5D8BW1PS@}$3Dd4Dz47Y&_BI11&h9dM+N#DM%Gl`A zY5Q`9n5DECxi-!G;@#vPJqOQhT)J5!dfheMyFBbj@cPp?dTlLRUSrqKsE?2LI&>TL zDx;tCj=~wOtP3JX)ZN*=QyU+n%%yXzzaH}XsbkYe{5iv;{e!W4t{KcIduPDqFNb?Y zX|Jl!nU5bdYMkZVi{Ixs_w-fl+*otay_s&4551_hxn0WM-tQ77{Y+`n>ej2$D_?J= zuC{rb{^PoH+je^FxS@q@HQe*ibkAzx)MrIUr#^0T{!OnvBbpX}*!|v4{^aGbJAEAH zgxU`&uhfpObL{H?W$lu;(Sw=VO^q5mE^2P~czxBc`rZ*2?2E^081~dOd~4M4nw@9d z&b)kN*NS6f#y74S5nT8w$p875)a$iYUivV?NvrAJ7jH{aTUneKf32#ZvZ!YI#B1F< zUU~mEK4j^*PPzd6-Q<8)Cq4&yJq%4azVGzm=!lROO~(5th2Qp?oHzAo zX`kU%vrZH*WF8(2Sm2z$VMkH%?mkW*%oi7ni#H!I4Lz@6*jv-#gU|N7&7I%x;V#`i zGyEg&xvwY)FI^yRHsem?_7BaK#Y1{Ie|L3T>CXetnb2)wTCO$R<`X^cgHK~(5&xqC)}P|^?oYv zylnn0vmOr~P4YM#;CJ-lwyS-HZGY8r*+kVJ{wjsfb$V$U&Tn6^u;@XboF#>uzQu1k z=iX-E{NW7~#+tSpIKnik)qq)A*Vgrp?U#CI?{K4MlN%Wx?RTK1c6S&1_4UFI^xi+` zu%^R4nsy7@B^5P%6*)C>*VHEM?}BYkj1kR0tjOu|bvk3O5sBWKhA&&U9$&9~{iE^4o{M|?rWkxowkfIE zP213I#mdN>jEVc-)m`;%>B6@Ajg!nn&Q8yH<*2ic)p8r~JZ_lM+Q$F(?yDMx`)C?& zyy)}7ajhoz?>)`v?AOt?cN{*tCgXBW==-l}-nDC4J^xw$womcbQcwM}1-7G4AKF{h zw|BdPCv0UNh6}vjH$A;>p@!YQnsyiLI22T@H^FnW@=#^W=y5KiY%KTRtK5P$XNvBg zy6aL_{h0a8VnoQ)o!@PJW+aqPe%R%}_)E!72i8B)>vYyLC$o!&VP8$dRYnco_tdVQ z@4D~h&@B&43@Rvo_Q*Q#4Ey0%T#;-cEf42no2uXH=4e|rpIH?vZ|Puhu8-icKd7CRcf{521vKx?5IDnhNp2C zLuDHka8f&Dq<}qyP8hcc;@lM8$_r z9Vwe~erTru#mW+sj4cLBE>_GsSgK(dPkeu0T502k53Opxw!;(4+^a7auC>~^?n2EO z3(V^M>S64zY}v4XOzPc$7QUG|PjX8Sk8XYKf=y22lVS4${F?i;{}}i3p7U%C!vUIx z+t*v0_?>HMTu-~h__e2Yrq6vozRfzB?XDD)G49eG!}6lPj_hdEz45s#zV)v^IAg!* zTcU2^Rih{315Tdj)qo|D&bee?GQm%>)JD!=7sedg?2{AYyz*V{*( z+OhY%?xpID!?S|!EzvMMNYilS{7ao>!<`=QuDe_Jfp+(wU3Qh2-59gV((9C)^FAg; z_Bqwr`s(r5*@fGJ9d~%xeXn=++w2O> zGbh@fU*+nrVRwk8-NTm&KrDNvuT|2 z`pX+l`aQ@P>NT**79$_uq(c3;lFL^28ywwic>ZG7L>K#=%7M!+TzL?C?Mc1Y*5l3e z?)IW@kx&~Rs%f~rQLWNfqG=X`J$?qRQtlsymDjJ+vH3rzhvnxg$`8k^U1)wcvWLaW zxP&IziPsyHG@J7_{KovQAvJf-?`V0~Tk||I?2lcgNUM?f!_~Vf>+Zhjz3tinEZN=Nx|*sCu%hd{TQ^a-h)|DXI+%v3~wg%(y$w* z{vrV~uJzTV=-W0^OGcY#w?j9ApeD=c#SMfpnt9HLuoSHRzSlqUHleI@R zz4OrcUHSbF&-Ck97=2nlE-`t(!3Yh*;hGNbpB-1YQr3UOpfBBS5AE4%+p_o3t9(W; zD(+zaNIB-6?e6~fOI*r}oez$7eLkegA??sd4#jzE4C^jlP&M#+-|R6>`R~)<*NF&C z!vlNY&pY95?7nN#10TbnZUJrC{CY_)dsp@Ra?`iUDPn4mXT=-rA2b?&@wMKY-rkcQ zwqAQ>!hW+pmp5#(xUz0Xx}_NBKlNZ6u4&jdq+MqRyO^f^Cm1cX`_#Aa=*xQ#Z$Det zc~wB@rHRiL2V{KE-~RCFfnz2`hWiiPTopc`)z|b0D^cUwSMNR?b!MyPW5!5LyMq@W z8sMLmUC~e{r_19Tm$Ryre$!??{-IlA_1LDz@9j&{d-bgS(;;tnjj&$xYfa+$6!S&B znbRMFL#7SrF^AneY#)6&i`wBRO~c=+V-`%GIm3^8cVGC(nooJxHAC6xa7*tdWt+Nb7^W|QP+wYmj+HdY zyZXR6;@gD2;-8JBE+u`2C`xzT-lO&L@;=-7dwMl&y<_CX_I^pzmh7?~@!IBOy-0C` z37zg-bg}yJ{7vu18g@r$+HK%h_vFjlY08&Yi@a=tLe{jtpCcdP zdH3bYVIL>XOy3p$YRl-7cI~IxNOgjax9bzz@kg!H!P7OLXpGb}EFM+y(|2`ATbn&g zD-wEYi`zREL|l82)@a|mm7eD(ZuZW)`)K_nhp{949*tB?dZD!0VckdOIp|CGbK}?e zT-p|Bv`)ig_19leG_-bHzdmtwP}`Esn)ROU(DPHiteu%U#pYqZVFzZ1%*$Lg^0Tw+ z^s?wVF5`^YBfVZsotaR2{JXcotN5$O<(YT;E!d-Bm%crNzp(#nO{slx*|qIA0(I|< z%1XKNy5*f;jrad*DLU+ZX6)4aHKLwQ+7NJU`Q?KnhUj(J@^VAdITpLSUv7QnWzEYQ zJKi5XV{Ri2!*Rb2!)`tAw8Vm)D~GzhdzKdz*WPjP;G(mqN4IuTK8~C{Z*R2jy^6d0 zmCU~M6b<0M>aJ;7B!YA2-&RyPCTCwcx`A*Mw zy-t`h=}@Plz$4*1gKzx&_3Xhban?(N2!qgLC-0x=w5{R8sD;HpJI=kGWKm;#bjddT zgBo^~37Ur2&AL!<%gMyDhJ)*!z6ZMAzLB}%XT|zYYnNL2Ieg8XJfn5&P#5;&fo3n; zcKN=qi}TIg122}O)!HJ_DtX+%*`xvgRYv?ek*H~R?mpjyzPCRfe^lPv`|_AcGha2B zGW*JrlFw~lD2MqkIpOf`X<3KpzPhb%>bWj`6cRYcr`w6VHWev7(rPYu?Htges;`E} zNt%XJGqTp&Mz#+reNktXRi{gPVpn$YK4v_lRp`VGH|INh#5{X&tl`VE70nOkYx;d?Fw1W6c1w$`ra({d`{LhY!sXw_wL@crzKya41eYqG|)TyJaOfs zZk@EREj@IucB7za8?&wkEOm4?%{jbrR?figt`FV4$0@!V&g#;%WUg`Zxf%}B-z2Cn ztv*+$HA?FFBKx61>uNv0Pvez(Jqz|l)Op%EDs!*d$>0_H>@w}dt>^yuGGk4RPc6Gg zY%gy%e5PH+NE@x|#r6Gsb5Cm+PSrGQKYqo&yOYKpygxK&kmU6wuSso8<{sYkemvfy9$m8Y_Oy%AXOyj)f0rI(}1k<@M1T#3_`GA?+ z8UolT0u*wA1T43OU>3(L0L2KcOH_PESe#0m6n0R2R&0evExU}vnaRZ5T9{x_YHdnAhJOf|EtHteIBi5Fh z;LF~0i0iN7TCN2bZHT`DDg}pDBctAk=#*$0cgDsjUs|SI6 zCXJ%B=#&V`o62q2AnqexSo&jw_>Nd&Y+ZVLllZHYIqxA-{cq!kTvdtK)M&FOZ-RZLiH2MeNIpFlD>~d69blCqje=+5X2Un5e$PL=~B&_?Vc~1Vqug^ye}0 zbg^aX-zM_^3;J*4@lHzK74w}kJUt;K3IEpo^LC-LYVo}Kf11zV2cN04uGHY8xV}Q{ zE{(%?8LOffscPM@GwAL?7l}*UgWqRea7MgZs*UdruFGxNAa-l5{-4==g!nIlQ2+mI z&8Vs3Ra*DFcy2r&!2i>CGLWEiu$bD7PFcfkb(P4KKY#HTaXLuC%A)#nJx!fTXcK{} zwvdN@W4x}4fh};P(=Tz;--**zN8qH8o2wXn`-@PPzHY>f=ai!QAn~KB)Kj4Lx0*43K^}v?cOVqK&USjVp)>spb zy~GkV#uR&Oi7A%<@6GPsF2}7QCi(t;`F*=P@6DT;H#2YEyxkj+nOZ;}sNg^G13i;0 zkiH6`G&=YyR_3EmF({2R5GV5~C$JSn8l8|z+9(Sq`mtXj;HjJzf;2k*(F1sf@&zNJ ztD*v=KIIf13Nunm3IjW+cKAmjLQ12PS(L?Wl zz&U{a*(eNz|L8Bxi}OS1r<(60filx!XJ^nl>IMpuP?`@wC$o^sDZW8zuvy|%7hGH@ zG(l<9=Qw*xbOVJINDHNa_vAEk$dP7q@kQcbfEfBCks9g;43*Oc$pTB`xr?l7N+tTf-z zh#e|Uf~a(2r1?zqcRR4dozHB{%GvHC{Q>Y0pkN91nl1rAFhD<3Sq-cK=m?-sfcd}z zU?H#wpreIo{(lTi2Npxl5@0D%6{rS80(7QT4WK4a3#bFsr9oH^Hw}O^WbO|P0D1wv z0V5Cv!~kRyXug|)K0pGH2qXa(AQ|WfSbcef3490K0=@@+0Dc610&W9$fV;pw;6CsGcnJIqgaEWtqfHgh z+X^yLsVlnEB~Vj zt6u3Fuw4VL1K$F4XwnJbBybAY1?&O#y7N9wS;?+6K=%RrfzN>hz(Jr6WYz>~1J!{V zKsZ35D*8#(I$%Ap0oVv^0yYCnfn~sQcYd)cYi}D4ejAVu%s~1~pc(4b0-!UqHsi{H zS-@;y2rvv74qyPdqywJ=2Y^GsR$v>j16U8N09FC>fKPxDD5o^w50sbV}J^U^vhlpkvW`AwQkJRv9P{cml-% zI{uDMu4@DgVWq@hk)S^=LVx{2oV8gC+O16*2I}(5%~+YjLAa&Qd1ws`1;T)EUa&bU zW21GO)@k~LmDXljic13T0CfEBN`P!Qg&fuZ69774gUod-&>NsnBj^QxSD+iv9q0-4 z0w}usK0t5Sn*q&%7C=j&73r@rZkhn~0E+N502%^x`ql;DBJeeE3HTh?0?=7wvjGmY zLbK-p+%~w$L#(Bdd<#()d35Bh^#z{b`6=)WppYm7$SIJ5UVi#Q|@?2Pg@Y0(=2KpbStJC=XNsDgl+bUu)K(bz$66Ad2FB zRK#n5iXm5qTo-bKx*;hyuH=)G&rLpdGEA3tpb*{wgHDMn7 z`&P_1yfB`N07ZdffEQ34n1;L`1JeQWV8~-3=YeooR4<>ldhxD&QfpS<_5?*e1)c%F z0KWpyfr$`7S?OFw!c$c58E_o%L2)GjO8*hhw}Bsk?|@st_wqB*D82U<2*kfHj!V}( zHPb>vYWs%yrkHv8HmsoS6QmCS1DV2)aX$_i3yc70Tr@>`OMslJ3}85b@`cMsZsb5< z08k2Pq~n3O(ggDaXl&8jMw(1CiIxD1f%(8E0KCKymwC9MF-ilR2KsD(qDRw!M1brJ z*&DJuUO?{c*V?c+cUvypQ$?uY_i&#Fco)bEcmRcf{6IeWz5)0O;=TY-ANMp^h=Xg{v8e2jYNOfF`sF zps_O&NCHLx8Qk~*3kazcX84T7=t$zyg3q@)}?@unJfSd{EUYyfrvHBrSHz;@h! zhTmQ?k4ChcdVss+;75_(&?VEY{fGx1ALF?uW;SV&vapxa~{X-F}RdnSU@{+)tUoT@+qX# zj8Meh!}A&7B%p{Q9i92gnwk;~8C9`J8w0c*m75@5l^kO?#@lye0i~be`4`|h@GFo4D5Jsye1#Za+>M3Uh==^IKp+61 z)z}v(0#Lw3$x{XQL?aJ?=tL_D5WO_6o? zg2}s63KmyQrcCJ&sO6`ENvOg@6|M|a1n4w-N-qca11g^lR!b(TiK>uFpedQ1(WwAM z8wT8yaMFs>a>_%R&C)2OleZg4ixLSS_gASosYww^Y7{lbRS_DFdlDK3P%Ej0iVUUO z$c=VZ8Kpu>J{z^rF*9Y10EoFNu4zDDpbuaMC`v<>pk0~?hykJiBhVY@1@r`{g*|{S zKzBeHXI*jM4d_fC4}O3f+6z#P-Um7XtpFNS&4Fe>Q=kdZ7-$60Kx_bz`e{It2510M zLlg~AU(=YNZmAE{1!@5`0cv1%peTJjL~4x$sI@c*3(NO4H20K7g(x(taBVq_G^i9x zGKi1Th?gp%q|vj|EtDqmQU4R6p3JB)Q%k4q9(r5}QJS}nG zLe6g^fy&gOdkPyWLfUBV+sgMeAc?OXt~3bgp04fX`wsH8qkJX0B2Oz@PP2)@K?1bu zrByK;X_QWbg~o=q#gtA0s4tZY(6gdNtvs#Dm3&GbU+RCkCZaICdu`#QiD-Z{5Dt(~ znzGaYg^#*RYX=;)rSQ>Th8F^8E^7r)t*Mr&xTXMBpdUbi6pD*bp)>$#5NQpRQXypk zk_?)z+UZL?G<9jZ+LXDf%uy1onV=>JUyLYXq8 zLTw-R!E>S}Edlq+3?P*gl95J@u>ho1$F34-H2<~zXU4Nu6Vyllr50@2F_VfGw1Z1q zX-ZcHYIX)GX;YTf!tOkfdT3fG7LTS6sZm>KVL8pY>iP9l|Pfz=4^-3}7~}0$2vj2lj%OeCQ>( zUks2Nx)9d|KyBQg18ouR$uVAvYiBuaIqoU1nhvc}0ts3T5c5jlQ-ECMb9`eD=23JN z9_bwdZE=1(jtBhniE-P0wFhg#S`)SK8Utu__qD&O4;qwOwo*=kaxqY!~-(evrDg<@GtAKor_l{!4B508# zj#(=TEsuR|3=|w8L8uK)TD+4=UuWX^UQRsgCn(_oVdPuzb;#?{4IJ-+qiEG%zW8Hw z@te%GHNPCiLUUh{v)<;dX0cG8${2Bl!RHdY_t1kW{k{>BM1$+P@p&<<7|z={vw#)h zA4jtiuJL?hG+H>2AE(O*{zEit4uDXk+F79w)_0gxo$AmN}_*V@YhcGX-(O$V^DTu{_{dsBnLLw zJ@^6>#2)7Da3N`r5?C?sWb9F>B{yH4SXUt|?<`a!I3OeduXiMVVua8l{IrSru!6by zLldi%+ZO|px+jhM#X@F7UIQ2JPI<)Z({y9ItMk6QGQ5tE6+z>~#4WL`70wOW6N`=? z%J0Oo&^$xIP7MivmzRrUA>Iw~OrvOJn-XW{O*^|w&W=%ZmrsmCG17$vGX5#}S)RPS z^(Iz~KZ|2wtax5tDISSs^YWmLtQa%o+{nE6bTccC zuV>a%(sZ7&2}$3XS(vPJcl2fMd_1HNs#Pc-kL|-+dY8fYqrrK%UG$K)<0}kD0YRt< zhVNZ|q7U=(`2`H5l=fpUt}EHOc^)u?p$6y=mY=h}(0vOio(7JT0aL5qZm4z||D-SK zH@wFB(Tmb_OiAsS@U;Nh1lZFH_ zprg!Oh55(HtOP4rif>A0 zEpf`%Yw-BofVrl=`tY5K^II+1v_iB`ba!Q69@Y;7!XS^h`D1o9`1EDI<;-<>IC=`j z82Q|O=+PGZR6q2uXqTt2n3-itOA{q#!nXI#-^_m~R}FQShOx}dZB|yw^)tQzBRl+1 z8L1{`lmB>n$#n)ioY^Ovv9%6SKjK@wv_Zb@O4bGHQpjf<{)NebFz z=8usbpR~Q3%Hr@bT1qO61Lq4;nOB}IGHYIcUUm^{#NSC{eyq4Zuaw3@U>f%=Vy&3L zpXXZ)vGNqH?a#kWW0l;S_``qay@tVP9be8$@&^6c1Mfpfq?tW6uF@SkRJjNFXq#5(h znxBT{O#Cs2D>ND$0AuS`UZVUoGxf=$I=}8xDTT{(pMk8HPkEUmqUBfH7ERkSSmlT; z&)b5-yCpcNf{RxU@AEL1|5=rz7Y`oAJl>ItvU1}pydu~zkmaPa$wt@-v-z2UXxLhQ zk1n5c<48N{0-w5)dDN5y+(9{HlcI{e$eq8G=dWrxFJ;QdHSU+`GjYyGDy3iro-~LR z3-_-eEg$o}gFZWVC%UW3Q5_V_?)3JxPgnLSQE-JyY0GyF!mx^!8CEwo_psI}KTl;C z%6}$?X<#S{31=1+C~>I5oi-{%H(q8i`pOt6jkjH%MGEzpJbOeA*RX&P{D(I?yx(B7 zc@`g!i}!Z$VY;VZ4B7R1;uDMF)=n-^#w1>)203A=1YLAAr z$yeaRGb$x8i02!EUTOypOpx^7dd?mmHt*PLl_Qnc8-hwqGWwCK3#_Q z|D}S;cacv9L&P&Mc!8nLJMChvAAK34GUSEzC(o?J;5M#5484%9QYr=W>r{z`;Gh=f z|L#n1l^4ffs2sg9M$=!DoX zO`xGuKkxd^e&0_REc3Nt%)(06=BcJ`xF5O8ByvP zPzs0hAsuD;LDS7ERS!D3n2$1Pw8*JrRc zlA1ZVX^tC@^PTL1ijajy&x)eFyhnq9+VXwmPmli$FAL3LMM7(^<=~IgVNd7tIvFTp zMFXi0)_Hy<>)#&LoCJji1VdPG6G=kx4V%A@%XQH#YDbE6>B=V}E50CJk--|(YSvWJ zX9rwqg+J`|vG;th>h*KD)~%-nNo4uHxD&P}ZNv5ZIb4n4r3DoSuZ@6vdqXyj z*RSvUWOc}%NSeICkpaOWF2Y86vFa80nh{vHg_ZKiCDHsV@NbbA9tp!krZ1@eoL<{= z=4Wc|wlE50R`M2YcxV2){zp~HT5cGL9&l!pCE>yXdbzg5QAgl&W_~2TX?#5xaMB)Z zq^GZ_^H90egx-r^U!1zLo?7Z1{*t6Qv-grTVXM7xEM1FHa0m*cVWhXuf0+8IN6{wF zRY9<*>7$?zVb0}Zovjg))b{)Y#Ci8l@?&A)J9m;wDdF?d zHg(lcU&^ImG8jAYq%p8Bo!{sCKSXJAsXqF$-=GT!oJfMrTd+_>VYVK>JH>^fp+G9hv2SK5A z#B&n@KraqPUNEJ56)Kd_u}pp`NI>d# z83zg`NBXMwF1~u*;6j)vUo12=d+`M1WlehVF%wXalfC$76Ie^Os5j3wkyXkQXGF*w zIWvsB=0q5rsoXdP=1$reMr@CghTZ&C)9w{1e0DVCRzsao+e4sGT|OQ3$`ZQJ8#Yks z`?k@1FC_V-L`xjM{V^o|Kr9L#kmQfM}M#zM-;UdIl4 zA`k7Ap`EtipfUb3!F5A&>4pd&iDlm%BvfV6B&fC%6OtpD!TgBM3%={H84S zeoWU+*J|!>D5@gF-jCtmAg|9zh^M?IO5ZHAKX+-`-@@op-U}vP01b^O8!K^~>AdQ6 zvu6u_fSIJ>fv$B@=?G9nrFl=}WwNq-oYYV*Md5H#(@0vK@y0HWe+yaRgX1OT4XLzk zxA9h3s#x)LUre{pq;;7cb_7G9My^MvKhBj`rm(ry~2P1RgsT;$|iAIa66n zpCt)W1y^l*(Pn+^$}h#{5~@OQH-WQh7`Jm1dE04F>B>aeGMx6w-F?VKgifGR(Jk9S zp%L;!>xV1P&cEp*Q=sfaiF`iEXq3$FLxxYsWXZCn9@&-=SNb;MJJ^hq?p+opbKj3q z%4gsp3pdO1!|CUvI~~kn=eP(8Vgl(KpFG{#t7nsFmGUf^Cm}B@*pK_Jh08$l0{TfC zhadZ_I61=*ut()?)sOE6w@)HCd?9Z}sj$AUs`kjGa!l;UA5mR4fP+e$UA@qeqTR<2 zRXI+ALJ`@{PlrBRer(|~m2#&auRR@g$z|n_l)6-~O5DHJ&A0mf9#(_OJicgEabKzCEM<+&AMob3JI~_op*o);@*jo55mPz#zVV9rMgdZEZS; zPnm)G#tf3`8$EW|{sx2F|4HQ_lmLzqpwJGcQP2+qsyA75PNggu#IGZ7_-=5}Ml0m< z+1@2)EE=P7Tm^;nH0}eBeW&jonW$3!l>wkoLCM!r%TK)4Jcmk|Ihe0QUY~7p-buJ6p-rBEn+>Z3 zv5`D^4k}g&9At+ZHEKFxsArCNiGwC@ZBWRnr}yeU`JE-F{Y909@D3ySUdkIUb1eQX zDJaJm#S_(v4FiQ-B=@-HeQ&&5`6lIs*MomEM)LROqJ!3g!wVea_U$Zi^_yFxRgNPg zd0lY)&49E$65W0jFE9@U^cW>+t=R2X0WBt{Q5aZWjQWn^4d=0thzFykD(s7i>=qNy zWCU4H7$ej(=NL(CD>{8My>IKA(yIYslZ%YuYe;$-a8R%Pad>O)gU0;NkeA#mSnZ%O z{9EE^Aam3Yi@v|ML5`C`I^L*YVH?9eKY>BRHR=;~l2vx&8&+ZB)|Es5B4}TP51G$O zcn@@w#$eNBN$W0^%J~kYgiGPL5j=P`;%CRk^7Pfv))el&03Lw$y@Qwc*JGvJk;Pm0 zIa9C##Xrey!E6?JHcWrFpCE+MY!S|LPlhpbEck+NjUiwJ5>1A zg27oZNum@8;=`9BS`!Gxlt$k1lV8r;UZ&t(HE$~42;m4B9mmDTSwu*Zls9x$BsTk=exfGMf~OKULcjr-wB7{~RiIXU@oRdip-Ve`thPb~0lZlG z@_gtjxa^bXNHMDaY_=QpXS+^3uLf_u25;6HB})!f!`=_dPyS=X1ahdVk|}fPn9S#| zfn|~$E4;ZtnseuhC>PgCx}h1OoUwBV4Ot6~w(9V_B!B|B8D z>l&{5+L9-!c@z8bq_wDvRh0Lnq%7No6_Qb+@Dr5>BR>-L+`9yPW=>$ z7%R3CQJ~POec2k_*#CLE(}GepfY%wp3g^TE9uW}2>yBWaHt8N8^&$hgZNrxo-w-qK zkt;HU1c{Z~u9N~=eGFbr zPhP3BQiuzc>TIuPGMI{^%#JZdp8QfJOwk6ij{}ALtou2(cX{IGSy0XnibI*2BA3WT z#tz%@?dh-)m*f^;#oGo7we8un=^LAk9am$tN;)YQ^zNV`Pa2-uQC+3{1WG|<8u#9+ zysd8Zq!+{p2JwaKV78(0EK+5m8isMJq!7fd!RdT$5uco?h zs8ki72m0v?$ARwK1LoIu1|CJ`*=z&Bl&#vUK3rfqiiwq?6uWl zLSIF%kreRJl(y@p9+;5JnF2~Fjn)w(%E}f~f+9(0lD$n!(Fo_mN_4Z2Q2CoGZXfFU zQ?kc8smD4DxKv|(r3VI8`)Qz1K;WHmtN72>`-=qydz@gGsi2SxcsAquz-tu;QcRsZ zWKbL`Q(H4X)+19PWUAJ{)%Dk3OCy@4bo1w+6a^)3=#z@I>%H7&r{HhyFpq30KT=VC z!g^^=A2ob-b8p6xg?3qvRa0t~{SMAnMLH-InYuwTAq8i*DpuMuPqL@bQBd}^_a9Z! zJ}C6NUVjKXbg)uI4Ae0jrSReFCXYaaH`p?_Ej~2*(Zzdp!-~k z^2HFg*T%Wor|qkLnGTW_!RQA41?mmG>fG>B72eRRHet;Uq4nf!+^f;*hXNtvF^Uwg zMqig_kcZaSaEtfw;0rBLd&)zvPISy3JfW<{&8FBeeKqNK&ENK(pxQgAHrdw&!&jad zRd#5nDBT(JuT2>3a&I3n`ilKK>Ql6*WA_lbaL}u5gc#Z4+q{2Qu?{0sqrE#@q|JzF z>B#rrYcqRNIz#Y+J|xKOz^0^HlhP9LKIHhT7DvYx{Sy_X7#H3Y9^NVqwF8@GOk?Q?dYYSuvt(h+huwtKznULR-N}PjA)VQa=Y?BP*5r8Wh^F-oKQ8$CPO+ zUaFL5+wp}x#YF0w4xj2~yr`*nt{qZAYXUxe(zJcf<+7c?7bW@=v=s8tSorl&+v)wg z@9L@K33aIpN<~o4HtV(^uVLIhg%adqGlN8`D|4iK1Ebs0z6vSCMSrXGA@a~x$$Rgv zb`8I&H&f&hA9^kXr64Fh+gWCfyi&TcA}z>eBPhi{i7Rt;#MJUPrR`O4fDK0COOR+9 z9ZIit*$DmMKpLra<_3fw|T%m-x=fJsiPo(r&!!TWrR$oap=4&bJU` zwKDr`Hm%cE2@0Z-8hN(nmKpw6{AHygQxi}qh&yu_TT{oy?>Ya$V58^w(w@bLhr{*UWwQk>TK_! z1?LV1wRHoekwxvDa%kCxf`efCMHfZlM?fhF<|vqO-h2ehW8X>WZ)J77CEf_G}xF2)xk`|sx1n2>HzAiI*W<2TPXeE?a=Z1mDUCa zbXBEq1Vnr6^F=wv()(e{W~cp29!In3iy|mQzYRKlD&|Wz!b{FgO*SJl|4e?HH${HX z=C_LAwG~1)?vtW%e@ybfYchLngd*ini@pMd?0$%K?T%J|PC73SbQ*JSh!B@lCbaWa zNT|!cp|@L4_s=DVwV-iE%vT)>E>+43d1G1j?30n*3ssHSw)qR|htoQW?7}H|Q&rn5 zRs93m3nF_V}E#W1+Fr0$!s|vec{_ zFfzO$*jLbpsfEKuc6}x%<`le2&XEQIJ#sMjYXlts==;yC8g2Eq-5?^<~%wS7|&HUeuTj*~f9Q^v4$}1E& zu5V|~Ix0%jSB;JWFtqiYH?e8%^iL$ed9lhygyL`aCz_M(1v`%#IS`ADeRThAC5lmG zU#s-9zg_44!+QP0?EhJ_?A@<#j#qyn?GV~5|J2WTC1js!sIyvI#e`>jrrId-*#>c#M&qY zqyvsgyP{$D*vud2o`653tR<~Mp|C^GODEGy=KdP-4TWNn^Sn&UyMJuY{CM$56bi&m z2Bj?W$Q!m$ob zvmq{}bhdufRX&CQa;9sYmwz}U!SJK;#$(x%`nQ>#Dw;IrS zn<95Z8hr(~?8KeMU-4Oy%(Wz38L`E9Lx9H~o^cx3y>mi(<#B&ooH1 zKptA$Vy4w%-@NScgOrDSSAAc?!HKIW#e7A)&B3SqyDnw6%lyYh+BYQ*FXg+)E+sVZ z@2(~e-l0wN7XANY&9G(uy7+&-X4o=+Ec`!TGhnc@elPt$Tr+Gs9~}R$)(o5T_&kL; z{wHfj9Nr)Q&(@5$|JKaG)V-zIaffM_zo56<9etDhHX(>?2O4aMH2i-C8vL~%5Wo48 zZ@a4W7khmr>aQgFzNy2e&8GPsfc`l6cPFg&ONVzIf64Iw8F~4yy&oOt=bfr>vy|6c zUc&XefloK+z>ns?eVO3?^OBAQTz$^)&KuZ8;)<#W=9!7qxiPM<~{N>YC~Tn7~T?q<)T z10gFru6m|YdYgIHN02T zgOfA&RQU>kn+`HDB##u{}#^tE)5gYqXVEwfJLAO2BG`j`x#U(<*CDVHQs zq0^!VfBwnBVazoaDfH6MhAWN0Ci{ExA%!c`FKy@)SOi(yzubq+i!vleQk?{gdW)+z>-w8On!!?!*td z)35XS;Pn3Zy7XON=r+F@8`v^@Y>OEvDke_W@nVav^ZQ_Ao38VmpRtBzb|C|G`umvp z`EfmXxlM4vd{%rS3!6b-eapYo_C7XN(&+(P>zkGv;u}a<4r;PEQ?hcNz;C5_gQ{Ho z3R*4}U82vLIc-9l)Tvuf;JFCsmdN=D%d4c! zxP@mL>$TeX$95h2&8A0qre65v-rZHr!$%ZzICjaS^gBHy!}68a(+~H(a%qeM1oG@< zB|`?`hylvD`0K3&HlFhtJ_XNI(WzH=&heQy)^jeNJ@EWsb*@7F5`Xw}-6)Q~cUPN@ zInC!FzuZjk1859cZvHB?e!_+pg*wMd>Nwgo$M&atnZY%HUe>3?^bN!xo;4+<8d5^* zDk&+U2CJ!GnkgmKkZd)jnB$U6R$gj9JDxYzoM1AY!v(iv1}q)tq36wREv28dD8v)`Yq#iRRS!L4k?pn3x3903!sMl46oA=A=}G zlx(%6TB0ophWOOfWF=9`W{~)oL7M zkhnydmE+> zhG329cd!ITnh zOy;iL+)60w%uF&V4Yo6z3!V%f|X(8*jVInOhR>o1FG?< z&E5PeOJ!soSBNY-3l)O1q{(%napY|D z(O^I}O-G3Fq0xn+MOrYxNf#{VIJH1Kz9g$@i5X-_FeSyM#`DKPZe?m^8W>S3)evPB zA7ewDV@8D}&Em3lUq$f6T9lPm$*HEG#u1Uc-wRgS+rB=Fg%YgFf+;iD>3O zNtY%{qGR{8u7g&AF@4FWQihPG2wL+~st58NM_A#qPUw!MhVg<;+{)VBn=EC3KTDd^ z+KI(p@sflpjn*V*v)92Chzj#cB+B1f^J`$(<$e4dvWQ^7XE6?!GD=g*oJa?GNMaP?U%CqegH@ zHwOAiK;1j6v|aCZKN&psdsJ5ZjTHMRh_((&PPLF_2v!EMXoQ*`5+3meN*~p)lnSvK zQc^4ii#aA*Rfg=br)qd%qv6GyjZlSbzzt+w?0GaJBq}YsuPIgaNtK?_u$TrW!!;Mn zz0I7I=2T2R3j2ZRcvE!Wl(a;(HX2Fs7W7cONh}#o3QDj<8&lz0Q&CX7k$mLL zY&PLQCs^X*uqI_D35wMeon}oT5izDzR9fq&YwD4bVp0{TNl$~JGZ^EbJG*en*{2%O zdT7HA$db@sQ@9xA$|9>aS|qFDGW9X-vmEJ}x`)Q7>^>LgR+jVbYls05oO%CP`J$-*jENtTgUi$Dn7Dp)WY+gOtW4 z#)KO%)a9Zy{eh~c(soo@K)_&g+)G(asaCrjp;YafqAgw!V@$C|?BP-jiL}#@mmH;F zCmfCf0__vUUR{wZYc&QK&8f2YTTX76;FQu8Lnl4KZs7f|a35P}+;<^V;S>*Gq;Gk& zt1LRbzd0J4N~_gk#m>%{l46cEi+RKs9cDhwl-9k0F^LSFzMLefSmAe8(6+@s?3GH| z6AU=zXnYB zCkzI-s^6{>SMToZ9SsBxk zP;YCBk+*xqO2o)Y)n`j^TACsx;V+6r9LN+c_DM~glq9GQ@iY z2c1M*4Avw>WHKn39D+^A=S%$BVQ(b&_&Xp5*Ka=NP%1C9BzM*t2V_o9f~rzteP8fWa<6 z6l7m4m6GiVYL+^gv#6yhzKfDKD@7Gj%Pu6dGpkwSoD)exz&`SW@_au zlM@d;vkWH;s>o1hBBkv>W+_sdn$c-poa{ForDWNVs3KLhYILI6;ba!2A*do^oHZxw zRw}cktaJzzXU#Yo9AS$jyH{LebEISlmFZ-68ab15@PTrqFpQE{gx9a(v;NS-uleKY$Et6y_Zo8O=b>>!JK5)ks#5r z3y(IXsJlaFGO)4FBBPjg2S30RWk4(z`})*DV)<$NrYdQ?}3JVh~41mcnz?>9g86b2TuIMx?9Tm))D4Hd$$>Eurm9=PUVAfR8 z)HjcFj11DJlaWioAX?tCjB>fNG(#%Y($|FWo*WKJk;9IGWcX83dD~dGylQ7U@S<>s z6isLP+cm0;5UIBI8VFtygl(d-I%$hMM|4^$Q&nMg-zW?dQb!Odn%dWUqZCq-ju;}T nl~I>zXT1dbq9X+qRfyyssN>xpGDK!=1W|G>x6d%SjV|6 delta 37203 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 statement-breakpoint +ALTER TABLE "shortener" ADD COLUMN "is_file_upload" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "shortener" ADD COLUMN "file_path" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "file_storage_usage_in_byte" bigint DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/frontend/drizzle/meta/0028_snapshot.json b/frontend/drizzle/meta/0028_snapshot.json new file mode 100644 index 0000000..41d70a7 --- /dev/null +++ b/frontend/drizzle/meta/0028_snapshot.json @@ -0,0 +1,542 @@ +{ + "id": "ce042384-c48f-4a7b-b067-8808e73834d6", + "prevId": "c91acbf2-ca0a-4726-b2b2-70fd1f8bd9ff", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.email_verification_token": { + "name": "email_verification_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.file": { + "name": "file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at_epoch": { + "name": "created_at_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at_epoch": { + "name": "updated_at_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qr_background": { + "name": "qr_background", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#ffffff'" + }, + "qr_foreground": { + "name": "qr_foreground", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#000000'" + }, + "domain_status": { + "name": "domain_status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'verified'" + }, + "enable_custom_domain": { + "name": "enable_custom_domain", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "custom_ip": { + "name": "custom_ip", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain_id": { + "name": "custom_domain_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "qr_corner_square_style": { + "name": "qr_corner_square_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_dot_style": { + "name": "qr_dot_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_image_base64": { + "name": "qr_image_base64", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.shortener": { + "name": "shortener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "link": { + "name": "link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "ios": { + "name": "ios", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ios_link": { + "name": "ios_link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "android": { + "name": "android", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "android_link": { + "name": "android_link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_file_upload": { + "name": "is_file_upload", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()" + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "plan": { + "name": "plan", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "qr_background": { + "name": "qr_background", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#fff'" + }, + "qr_foreground": { + "name": "qr_foreground", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#000'" + }, + "qr_corner_square_style": { + "name": "qr_corner_square_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_dot_style": { + "name": "qr_dot_style", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "qr_image_base64": { + "name": "qr_image_base64", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_storage_usage_in_byte": { + "name": "file_storage_usage_in_byte", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.visitor": { + "name": "visitor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "shortener_id": { + "name": "shortener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "country_code": { + "name": "country_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "device_vendor": { + "name": "device_vendor", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "os": { + "name": "os", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "browser": { + "name": "browser", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "referer": { + "name": "referer", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/frontend/drizzle/meta/_journal.json b/frontend/drizzle/meta/_journal.json index 6f3235e..c8d597b 100644 --- a/frontend/drizzle/meta/_journal.json +++ b/frontend/drizzle/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1726769139213, "tag": "0027_funny_wasp", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1734847628427, + "tag": "0028_hard_red_shift", + "breakpoints": true } ] } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index f4ae8fc..3b1ab8e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,8 @@ }, "type": "module", "dependencies": { + "@aws-sdk/client-s3": "^3.705.0", + "@aws-sdk/s3-request-presigner": "^3.705.0", "@lucia-auth/adapter-drizzle": "^1.0.7", "@prgm/sveltekit-progress-bar": "^2.0.0", "@stripe/stripe-js": "^4.3.0", @@ -62,6 +64,7 @@ "qr-code-styling": "^1.6.0-rc.1", "resend": "^3.4.0", "stripe": "^16.8.0", + "svelte-file-dropzone": "^2.0.9", "svelte-sonner": "^0.3.28", "sveltekit-superforms": "^2.20.0", "tailwind-merge": "^2.5.4", diff --git a/frontend/src/lib/components/app-sidebar.svelte b/frontend/src/lib/components/app-sidebar.svelte index ea915d2..5cf3916 100644 --- a/frontend/src/lib/components/app-sidebar.svelte +++ b/frontend/src/lib/components/app-sidebar.svelte @@ -99,7 +99,7 @@ {#if sidebar.open} - diff --git a/frontend/src/lib/components/nav-user.svelte b/frontend/src/lib/components/nav-user.svelte index 591b360..3e52f51 100644 --- a/frontend/src/lib/components/nav-user.svelte +++ b/frontend/src/lib/components/nav-user.svelte @@ -97,7 +97,7 @@ {/if} - + Account diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte index 598829f..7616556 100644 --- a/frontend/src/lib/components/ui/input/input.svelte +++ b/frontend/src/lib/components/ui/input/input.svelte @@ -1,22 +1,35 @@ - +{#if type === 'file'} + +{:else} + +{/if} diff --git a/frontend/src/lib/db/schema.ts b/frontend/src/lib/db/schema.ts index 365cd32..23bbff9 100644 --- a/frontend/src/lib/db/schema.ts +++ b/frontend/src/lib/db/schema.ts @@ -6,6 +6,9 @@ import { uuid, boolean, text, + integer, + numeric, + bigint, } from 'drizzle-orm/pg-core' import { relations } from 'drizzle-orm' @@ -40,6 +43,11 @@ export const user = pgTable('user', { .notNull() .default('square'), qrImageBase64: text('qr_image_base64'), + fileStorageUsageInByte: bigint('file_storage_usage_in_byte', { + mode: 'number', + }) + .notNull() + .default(0), }) export const shortener = pgTable('shortener', { @@ -56,6 +64,24 @@ export const shortener = pgTable('shortener', { userId: text('user_id').notNull(), active: boolean('active').notNull().default(true), projectId: text('project_id'), + is_file_upload: boolean('is_file_upload').notNull().default(false), + file_path: text('file_path'), +}) + +export const file = pgTable('file', { + id: text('id').primaryKey(), + userId: text('user_id').notNull(), + projectId: text('project_id'), + key: text('key').notNull(), + name: text('name').notNull(), + size: bigint('size', { + mode: 'number', + }) + .notNull() + .default(0), + eTag: text('etag').notNull(), + createdAt: bigint('created_at_epoch', { mode: 'number' }).notNull(), + updatedAt: bigint('updated_at_epoch', { mode: 'number' }).notNull(), }) export const project = pgTable('project', { @@ -180,3 +206,10 @@ export const sessionRelations = relations(session, ({ one }) => ({ references: [user.id], }), })) + +export const fileRelations = relations(file, ({ one }) => ({ + shortener: one(shortener, { + fields: [file.key], + references: [shortener.file_path], + }), +})) diff --git a/frontend/src/lib/server/types.ts b/frontend/src/lib/server/types.ts index 07ae941..2561bd1 100644 --- a/frontend/src/lib/server/types.ts +++ b/frontend/src/lib/server/types.ts @@ -10,3 +10,19 @@ export const userCreateSchema = z.object({ password: z.string(), password_confirm: z.string(), }) + +export type TigrisNotificationPayload = { + events: { + eventVersion: string + eventSource: string + eventName: 'OBJECT_CREATED_PUT' | 'OBJECT_DELETED' + eventTime: string + bucket: string + object: { + key: string + size: number + eTag: string + } + }[] + sendTime: string +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 6e17922..0456422 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -91,3 +91,32 @@ Number.prototype.toDecimalPoint = function (decimal: number) { export const isAlphanumeric = (str: string) => { return str.match('^[A-Za-z0-9]+$') } + +// https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string +export function byteToHumanReadable( + bytes: number, + si = true, + dp = 1, +) { + const thresh = si ? 1000 : 1024 + + if (Math.abs(bytes) < thresh) { + return bytes + ' B' + } + + const units = si + ? ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] + let u = -1 + const r = 10 ** dp + + do { + bytes /= thresh + ++u + } while ( + Math.round(Math.abs(bytes) * r) / r >= thresh && + u < units.length - 1 + ) + + return bytes.toFixed(dp) + ' ' + units[u] +} diff --git a/frontend/src/routes/(app)/dashboard-new/+layout.server.ts b/frontend/src/routes/(app)/dashboard-new/+layout.server.ts deleted file mode 100644 index c274829..0000000 --- a/frontend/src/routes/(app)/dashboard-new/+layout.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const user = event.locals.user - return { user } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard-new/+page.server.ts b/frontend/src/routes/(app)/dashboard-new/+page.server.ts deleted file mode 100644 index c97e0c9..0000000 --- a/frontend/src/routes/(app)/dashboard-new/+page.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad, Actions } from './$types' - -export const load = (async (event) => { - redirect(302, '/dashboard-new/project/personal') -}) satisfies PageServerLoad - -export const actions = { - signout: async (event) => { - console.log('signout') - event.cookies.delete('token', { path: '/' }) - redirect(303, '/login') - }, -} satisfies Actions diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/+page.server.ts b/frontend/src/routes/(app)/dashboard-new/account/settings/+page.server.ts deleted file mode 100644 index 3cddff4..0000000 --- a/frontend/src/routes/(app)/dashboard-new/account/settings/+page.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad } from './$types' - -export const load = (async () => { - redirect(300, '/dashboard-new/account/settings/account') -}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/+layout.server.ts b/frontend/src/routes/(app)/dashboard/+layout.server.ts index e258a58..c274829 100644 --- a/frontend/src/routes/(app)/dashboard/+layout.server.ts +++ b/frontend/src/routes/(app)/dashboard/+layout.server.ts @@ -1,23 +1,6 @@ -import { env } from '$env/dynamic/public' -import { db } from '$lib/db' import type { LayoutServerLoad } from './$types' export const load = (async (event) => { const user = event.locals.user - - const projects = await db.query.project.findMany({ - where: (project, { eq }) => eq(project.userId, user.id), - }) - - const breadcrumbs = [{ name: 'Home', path: '/dashboard' }] - - const page_title = 'Home' - - return { - shortener_url: env.PUBLIC_SHORTENER_URL, - user: user, - breadcrumbs, - page_title, - projects, - } + return { user } }) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/+layout.svelte b/frontend/src/routes/(app)/dashboard/+layout.svelte deleted file mode 100644 index dd12220..0000000 --- a/frontend/src/routes/(app)/dashboard/+layout.svelte +++ /dev/null @@ -1,127 +0,0 @@ - - - - - -
- - - - - {#if $page.data.breadcrumbs} - {#each $page.data.breadcrumbs as breadcrumb, index} - {#if index == $page.data.breadcrumbs.length - 1} - - - {breadcrumb.name} - - - {:else} - - - -
- -
-
- {@render children()} -
-
-
-
- - - - -
-
- -
-
-
- Log Out? - - You are about to log out of this account. - -
-
- - -
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/+page.server.ts b/frontend/src/routes/(app)/dashboard/+page.server.ts index 74f9661..69cd2f4 100644 --- a/frontend/src/routes/(app)/dashboard/+page.server.ts +++ b/frontend/src/routes/(app)/dashboard/+page.server.ts @@ -1,30 +1,8 @@ import { redirect } from '@sveltejs/kit' import type { PageServerLoad, Actions } from './$types' -import { db } from '$lib/db' -import { sql } from 'drizzle-orm' export const load = (async (event) => { - const user = event.locals.user - - const projects = await db.query.project.findMany({ - with: { - shortener: { - with: { - visitor: true, - }, - }, - }, - where: (project, { eq }) => eq(project.userId, user.id), - }) - - const shorteners = await db.query.shortener.findMany({ - with: { - visitor: true, - }, - where: (shortener, { eq }) => eq(shortener.userId, user.id), - }) - - return { projects, shorteners } + redirect(302, '/dashboard/project/personal') }) satisfies PageServerLoad export const actions = { diff --git a/frontend/src/routes/(app)/dashboard/+page.svelte b/frontend/src/routes/(app)/dashboard/+page.svelte deleted file mode 100644 index 332698f..0000000 --- a/frontend/src/routes/(app)/dashboard/+page.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - -
-

Projects

- -
- -
diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/(components)/sidebar-nav.svelte b/frontend/src/routes/(app)/dashboard/account/settings/(components)/sidebar-nav.svelte similarity index 81% rename from frontend/src/routes/(app)/dashboard-new/account/settings/(components)/sidebar-nav.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/(components)/sidebar-nav.svelte index d2c76a0..3f86799 100644 --- a/frontend/src/routes/(app)/dashboard-new/account/settings/(components)/sidebar-nav.svelte +++ b/frontend/src/routes/(app)/dashboard/account/settings/(components)/sidebar-nav.svelte @@ -6,15 +6,15 @@ const items = [ { title: 'Account', - href: '/dashboard-new/account/settings/account', + href: '/dashboard/account/settings/account', }, { title: 'QR', - href: '/dashboard-new/account/settings/qr', + href: '/dashboard/account/settings/qr', }, { title: 'Security', - href: '/dashboard-new/account/settings/security', + href: '/dashboard/account/settings/security', }, ] as const diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/+layout.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/+layout.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/+layout.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/+layout.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/+layout.svelte b/frontend/src/routes/(app)/dashboard/account/settings/+layout.svelte similarity index 96% rename from frontend/src/routes/(app)/dashboard-new/account/settings/+layout.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/+layout.svelte index b75de50..7f80437 100644 --- a/frontend/src/routes/(app)/dashboard-new/account/settings/+layout.svelte +++ b/frontend/src/routes/(app)/dashboard/account/settings/+layout.svelte @@ -11,7 +11,7 @@
diff --git a/frontend/src/routes/(app)/dashboard/settings/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/+page.server.ts similarity index 73% rename from frontend/src/routes/(app)/dashboard/settings/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/+page.server.ts index 76567ad..d4f8048 100644 --- a/frontend/src/routes/(app)/dashboard/settings/+page.server.ts +++ b/frontend/src/routes/(app)/dashboard/account/settings/+page.server.ts @@ -2,5 +2,5 @@ import { redirect } from '@sveltejs/kit' import type { PageServerLoad } from './$types' export const load = (async () => { - redirect(300, '/dashboard/settings/account') + redirect(300, '/dashboard/account/settings/account') }) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/account/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/account/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.svelte b/frontend/src/routes/(app)/dashboard/account/settings/account/+page.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/account/+page.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/account/+page.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/account/schema.ts b/frontend/src/routes/(app)/dashboard/account/settings/account/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/account/schema.ts rename to frontend/src/routes/(app)/dashboard/account/settings/account/schema.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/(components)/DemoQR.svelte b/frontend/src/routes/(app)/dashboard/account/settings/qr/(components)/DemoQR.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/(components)/DemoQR.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/qr/(components)/DemoQR.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/qr/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/qr/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/account/settings/qr/+page.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/+page.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/qr/+page.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/qr/schema.ts b/frontend/src/routes/(app)/dashboard/account/settings/qr/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/qr/schema.ts rename to frontend/src/routes/(app)/dashboard/account/settings/qr/schema.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.server.ts b/frontend/src/routes/(app)/dashboard/account/settings/security/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.server.ts rename to frontend/src/routes/(app)/dashboard/account/settings/security/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.svelte b/frontend/src/routes/(app)/dashboard/account/settings/security/+page.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/security/+page.svelte rename to frontend/src/routes/(app)/dashboard/account/settings/security/+page.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/account/settings/security/schema.ts b/frontend/src/routes/(app)/dashboard/account/settings/security/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/account/settings/security/schema.ts rename to frontend/src/routes/(app)/dashboard/account/settings/security/schema.ts diff --git a/frontend/src/routes/(app)/dashboard/links/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/links/(components)/form.svelte deleted file mode 100644 index d5eac37..0000000 --- a/frontend/src/routes/(app)/dashboard/links/(components)/form.svelte +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - Add Shortner - - - - Add Shortener - - Create A New Shortener Here. Click Add To Save. - - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
-
- - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - Project - - - {selectedProjectName} - - - - None - - {#each projects as project} - - {project.name} - - {/each} - - - - {/snippet} - - Shortener Project - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for iOS - - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for Android - - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Add - -
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/links/+layout.server.ts b/frontend/src/routes/(app)/dashboard/links/+layout.server.ts deleted file mode 100644 index 631f315..0000000 --- a/frontend/src/routes/(app)/dashboard/links/+layout.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const { breadcrumbs: parentBreadcrumbs } = await event.parent() - - const breadcrumbs = [ - ...parentBreadcrumbs, - { name: 'Links', path: '/dashboard/links' }, - ] - - const page_title = 'Links' - - return { breadcrumbs, page_title } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/links/+page.server.ts b/frontend/src/routes/(app)/dashboard/links/+page.server.ts deleted file mode 100644 index 51d8729..0000000 --- a/frontend/src/routes/(app)/dashboard/links/+page.server.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad } from './$types' -import { shortener } from '$lib/db/schema' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { nanoid } from 'nanoid' -import { isAlphanumeric } from '$lib/utils' -import { generateId } from 'lucia' -import type { Project } from '$lib/db/types' - -export const load = (async (event) => { - const user = event.locals.user - - const projects = db.query.project.findMany({ - where: (project, { eq }) => eq(project.userId, user.id), - }) - - return { - projects, - form: await superValidate({ active: true }, zod(formSchema), { - errors: false, - }), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - create: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const user = event.locals.user - let project: Project | undefined = undefined - const selected_project = form.data.project - if (selected_project) { - project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and( - eq(project.userId, user.id), - eq(project.uuid, selected_project), - ), - }) - } - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and(eq(shortener.code, form.data.custom_code)), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project) { - if ( - !project || - (project && !project.enable_custom_domain) - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } else { - if (project) { - if ( - !shortener.project.enable_custom_domain && - !project.enable_custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - - if ( - shortener.project.custom_domain === - project.custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } else { - if (!shortener.project.enable_custom_domain) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - } - } - - const code = form.data.custom_code_enable - ? form.data.custom_code - : nanoid(8) - await db.insert(shortener).values({ - id: generateId(8), - link: form.data.link, - projectId: project?.id, - userId: user.id, - code: code, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/links/+page.svelte b/frontend/src/routes/(app)/dashboard/links/+page.svelte deleted file mode 100644 index c9ddbed..0000000 --- a/frontend/src/routes/(app)/dashboard/links/+page.svelte +++ /dev/null @@ -1,375 +0,0 @@ - - -
- - - - {#await data.projects then projects} -
- {/await} -
- -{#await fetchShorteners(pageNumber, perPage, sortBy, selectedProject, search)} -
-
- {#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as _} - - {/each} -
-
-{:then result} - {#if result.shorteners.length > 0} - -
- {#each result.shorteners as shortener} - - {/each} -
-
-
- { - perPage = parseInt(value) - pageNumber = 1 - }}> - - {perPage} - - - - Page Size - {#each [12, 24, 48, 96] as pageSize} - - {pageSize} - - {/each} - - - - - {#snippet children({ pages, currentPage })} - - - - - - - - {#each pages as page (page.key)} - {#if page.type === 'ellipsis'} - - - - {:else} - - - {page.value} - - - {/if} - {/each} - - - - - - - - {/snippet} - -
- {:else} -
-
-
-
-
-
No Shortener Found
-

Add a new shortener

-
- -
-
-
-
- {/if} -{/await} - - { - if (!open) { - history.back() - } - }}> - - - Shortener QR - - Use this QR code to share the shortener. - - - - - - - - - { - if (!open) { - history.back() - } - }}> - - - Shortener QR - - Use this QR code to share the shortener. - - - - - - - diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/edit/(components)/form.svelte deleted file mode 100644 index 628a84b..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/(components)/form.svelte +++ /dev/null @@ -1,204 +0,0 @@ - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
- - - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for iOS - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for Android - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Save - - diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.server.ts b/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.server.ts deleted file mode 100644 index dd59952..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.server.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { PageServerLoad } from './$types' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import { shortener } from '$lib/db/schema' -import { eq } from 'drizzle-orm' -import { isAlphanumeric } from '$lib/utils' - -export const load = (async (event) => { - const user = event.locals.user - const { id } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - id: true, - code: true, - projectId: true, - ios: true, - ios_link: true, - android: true, - android_link: true, - link: true, - active: true, - }, - where: (shortener, { eq, and }) => - and(eq(shortener.code, id), eq(shortener.userId, user.id)), - }) - - if (!shortener) { - redirect(300, `/dashboard/links`) - } - - return { - shortener, - form: await superValidate( - { - ...shortener, - custom_code_enable: true, - custom_code: shortener.code, - }, - zod(formSchema), - { errors: false }, - ), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const user = event.locals.user - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and( - eq(shortener.code, form.data.custom_code), - ne(shortener.id, event.params.id), - ), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - - await db - .update(shortener) - .set({ - link: form.data.link, - userId: user.id, - code: form.data.custom_code_enable - ? form.data.custom_code - : undefined, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - .where(eq(shortener.id, event.params.id)) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.svelte deleted file mode 100644 index cc5fb37..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/+page.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -{#if !shallowRouting} - -
-
-
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/edit/schema.ts b/frontend/src/routes/(app)/dashboard/links/[id]/edit/schema.ts deleted file mode 100644 index 3f8eb36..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/edit/schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - android: z.boolean(), - android_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/qr/(components)/qr.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/qr/(components)/qr.svelte deleted file mode 100644 index 7aaceed..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/qr/(components)/qr.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -
- - {value} - - {value} -
- - - - QR Link - - - - - Standard - - - With Style - - - - -
-
diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.server.ts deleted file mode 100644 index 898c3e8..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad } from './$types' - -export const load = (async (event) => { - const user = event.locals.user - const { id } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - code: true, - }, - where: (shortener, { eq, and, isNull }) => - and(eq(shortener.code, id), isNull(shortener.projectId)), - }) - - if (!shortener) { - redirect(300, `/dashboard/links`) - } - - return { shortener } -}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.svelte deleted file mode 100644 index 49ba36c..0000000 --- a/frontend/src/routes/(app)/dashboard/links/[id]/qr/+page.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - -{#if !shallowRouting} - -
- -
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/links/schema.ts b/frontend/src/routes/(app)/dashboard/links/schema.ts deleted file mode 100644 index f7890dc..0000000 --- a/frontend/src/routes/(app)/dashboard/links/schema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - project: z.string().optional(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z.string().url().optional(), - android: z.boolean(), - android_link: z.string().url().optional(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard-new/project/+page.server.ts b/frontend/src/routes/(app)/dashboard/project/+page.server.ts similarity index 94% rename from frontend/src/routes/(app)/dashboard-new/project/+page.server.ts rename to frontend/src/routes/(app)/dashboard/project/+page.server.ts index 4e1faa2..86eaff4 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/+page.server.ts +++ b/frontend/src/routes/(app)/dashboard/project/+page.server.ts @@ -8,7 +8,7 @@ import { zod } from 'sveltekit-superforms/adapters' import { formSchema } from './[project_id]/(components)/schema' export const load = (async (event) => { - redirect(302, '/dashboard-new/project/personal') + redirect(302, '/dashboard/project/personal') }) satisfies PageServerLoad export const actions: Actions = { diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/app-sidebar.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/app-sidebar.svelte similarity index 53% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/app-sidebar.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/app-sidebar.svelte index e6e98b1..70fdb80 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/app-sidebar.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/app-sidebar.svelte @@ -7,7 +7,14 @@ import ProjectSwitcher from './project-switcher.svelte' import * as Sidebar from '$lib/components/ui/sidebar/index.js' import type { ComponentProps } from 'svelte' - import { HomeIcon, LinkIcon, SettingsIcon } from 'lucide-svelte' + import { + CloudDownloadIcon, + CloudIcon, + FileIcon, + HomeIcon, + LinkIcon, + SettingsIcon, + } from 'lucide-svelte' import { page } from '$app/stores' import type { User } from 'lucia' import type { Project } from '$lib/db/types' @@ -16,6 +23,9 @@ type SuperValidated, type Infer, } from 'sveltekit-superforms' + import { Progress } from '$lib/components/ui/progress' + import { byteToHumanReadable, cn } from '$lib/utils' + import { Button } from '$lib/components/ui/button' let { ref = $bindable(null), @@ -32,6 +42,8 @@ createProjectForm: SuperValidated> } = $props() + const sidebar = Sidebar.useSidebar() + let projectId = $derived(activeProject?.id || 'personal') let data = $derived({ @@ -49,30 +61,41 @@ navMain: [ { title: 'Overview', - url: `/dashboard-new/project/${projectId}`, + url: `/dashboard/project/${projectId}`, icon: HomeIcon, isActive: - $page.url.pathname === - `/dashboard-new/project/${projectId}`, + $page.url.pathname === `/dashboard/project/${projectId}`, + isUnlocked: true, }, { title: 'Links', - url: `/dashboard-new/project/${projectId}/links`, + url: `/dashboard/project/${projectId}/links`, icon: LinkIcon, isActive: $page.url.pathname.startsWith( - `/dashboard-new/project/${projectId}/links`, + `/dashboard/project/${projectId}/links`, + ), + isUnlocked: true, + }, + { + title: 'Files', + url: `/dashboard/project/${projectId}/file_uploads`, + icon: FileIcon, + isActive: $page.url.pathname.startsWith( + `/dashboard/project/${projectId}/file_uploads`, ), + isUnlocked: user.plan !== 'free', }, { title: 'Settings', url: projectId === 'personal' - ? `/dashboard-new/account/settings` - : `/dashboard-new/project/${projectId}/settings`, + ? `/dashboard/account/settings` + : `/dashboard/project/${projectId}/settings`, icon: SettingsIcon, isActive: $page.url.pathname.startsWith( - `/dashboard-new/project/${projectId}/settings`, + `/dashboard/project/${projectId}/settings`, ), + isUnlocked: true, }, ], }) @@ -95,12 +118,27 @@ {#snippet child({ props })} - - {#if item.icon} - - {/if} - {item.title} - + {#if item.isUnlocked} + + {#if item.icon} + + {/if} + {item.title} + + {:else} + + {#if item.icon} + + {/if} + {item.title} + (Pro) + + {/if} {/snippet} @@ -109,6 +147,23 @@ + {#if sidebar.open && user.plan !== 'free'} +
+
+ + Storage Usage +
+
+ + {byteToHumanReadable(user.fileStorageUsageInByte)} + / 100 GB + + +
+
+ {/if}
diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/project-switcher.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/project-switcher.svelte similarity index 96% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/project-switcher.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/project-switcher.svelte index 78aa330..5e5ee15 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/project-switcher.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/project-switcher.svelte @@ -80,7 +80,7 @@ Default - +
@@ -97,7 +97,7 @@ Projects {#each projects as project, index (project.name)} - +
@@ -135,7 +135,7 @@ Create A New Project Here. Click Add To Create. - + {#snippet children({ props })} diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/schema.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/schema.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/(components)/schema.ts rename to frontend/src/routes/(app)/dashboard/project/[project_id]/(components)/schema.ts diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.server.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.server.ts similarity index 93% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.server.ts rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.server.ts index bd59672..48ac8a4 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.server.ts +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.server.ts @@ -29,8 +29,8 @@ export const load = (async (event) => { { name: activeProject?.name || 'Personal', path: activeProject - ? `/dashboard-new/project/${activeProject.id}` - : '/dashboard-new/project/personal', + ? `/dashboard/project/${activeProject.id}` + : '/dashboard/project/personal', }, ] diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+layout.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+layout.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.server.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/+page.server.ts similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.server.ts rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+page.server.ts diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/+page.svelte similarity index 90% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/+page.svelte index a2c14f4..6148678 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/+page.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/+page.svelte @@ -13,7 +13,7 @@
diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/[...catchall]/+page.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/[...catchall]/+page.svelte similarity index 88% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/[...catchall]/+page.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/[...catchall]/+page.svelte index 0d2f635..29f957a 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/[...catchall]/+page.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/[...catchall]/+page.svelte @@ -11,7 +11,7 @@
Page Not Found
diff --git a/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/(components)/upload-file-card.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/(components)/upload-file-card.svelte new file mode 100644 index 0000000..2354462 --- /dev/null +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/(components)/upload-file-card.svelte @@ -0,0 +1,127 @@ + + + + +
+
+ + {file.name} + + + {byteToHumanReadable(file.size)} + + {#if isCancelled} + (Cancelled) + {:else} + + ({((uploadProgress / uploadMax) * 100).toFixed(1)} %) + + {/if} +
+ +
+ {#if !isCancelled || isCompleted} + + {:else} + + {/if} +
+
diff --git a/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.server.ts b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.server.ts new file mode 100644 index 0000000..75ad33b --- /dev/null +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.server.ts @@ -0,0 +1,35 @@ +import type { PageServerLoad } from './$types' +import { db } from '$lib/db' + +export const load = (async (event) => { + const user = event.locals.user + + const { activeProjectId, breadcrumbs: parentBreadcrumbs } = + await event.parent() + + const breadcrumbs = [ + ...parentBreadcrumbs, + { + name: 'Files', + path: `/dashboard/project/${activeProjectId}/file_uploads`, + }, + ] + + const files = db.query.file.findMany({ + where: (file, { and, eq, isNull }) => + and( + eq(file.userId, user.id), + activeProjectId !== 'personal' + ? eq(file.projectId, activeProjectId) + : isNull(file.projectId), + ), + with: { + shortener: true, + }, + }) + + return { + files, + breadcrumbs, + } +}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.svelte new file mode 100644 index 0000000..e0d1dc3 --- /dev/null +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/file_uploads/+page.svelte @@ -0,0 +1,323 @@ + + + +
+ + + + + +
+
+ +
+
+
+ + Delete File {deleteKey}? + + + Files And Their Shortener Will Be Permanently Deleted + +
+
+ + +
+
+
+
+ + { + if (!open) { + history.back() + } + }}> + + + Shortener QR + + Use this QR code to share the shortener. + + + + + + + diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte similarity index 100% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/DeleteShortenerDialog.svelte diff --git a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/ShortenerCard.svelte b/frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/ShortenerCard.svelte similarity index 88% rename from frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/ShortenerCard.svelte rename to frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/ShortenerCard.svelte index 39db77e..3bbcf59 100644 --- a/frontend/src/routes/(app)/dashboard-new/project/[project_id]/links/(components)/ShortenerCard.svelte +++ b/frontend/src/routes/(app)/dashboard/project/[project_id]/links/(components)/ShortenerCard.svelte @@ -18,6 +18,8 @@ QrCode, TrashIcon, Loader2Icon, + FileIcon, + Split, } from 'lucide-svelte' import DeleteShortenerDialog from './DeleteShortenerDialog.svelte' import ProjectEditLinkPage from '../[linkid]/edit/+page.svelte' @@ -47,7 +49,7 @@ } const getUrl = () => { - return `/dashboard-new/project/${activeProjectId}` + return `/dashboard/project/${activeProjectId}` } let editProjectLinkOpen = $state(false) @@ -57,7 +59,7 @@ const showEditModal = async () => { isLoadingEditProjectData = true editProjectLinkOpen = true - const href = `/dashboard-new/project/${activeProjectId}/links/${shortener.id}/edit` + const href = `/dashboard/project/${activeProjectId}/links/${shortener.id}/edit` const result = await preloadData(href) if (result.type === 'loaded' && result.status === 200) { @@ -94,16 +96,20 @@ - - - - favicon - - + {#if shortener.is_file_upload} + + {:else} + + + + favicon + + + {/if}
@@ -111,11 +117,17 @@
- {shortener.link} + {shortener.is_file_upload + ? shortener.file_path?.split('/').pop() + : shortener.link}
-

{shortener.link}

+

+ {shortener.is_file_upload + ? shortener.file_path?.split('/').pop() + : shortener.link} +

@@ -163,7 +175,7 @@
-
- - - - {/each} -
-{:else} -
-
-
-
-
-
No Project Found
-

Add a new project

-
- -
-
-
-
-{/if} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/(components)/form.svelte deleted file mode 100644 index 3c23497..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/(components)/form.svelte +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - Add Shortner - - - - Add Shortener - - Create A New Shortener Here. Click Add To Save. - - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
-
- - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for iOS - - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - - Shortener link for Android - - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Add - -
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.server.ts deleted file mode 100644 index e57912f..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const { id } = event.params - try { - const user = event.locals.user - const project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and(eq(project.userId, user.id), eq(project.uuid, id)), - }) - - if (!project) { - redirect(300, '/dashboard/projects') - } - - const { breadcrumbs: parentBreadcrumbs } = await event.parent() - const breadcrumbs = [ - ...parentBreadcrumbs, - { - name: project.name, - path: `/dashboard/projects/${project.uuid}`, - }, - ] - return { breadcrumbs, project } - } catch (e) { - redirect(300, '/dashboard/projects') - } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.svelte deleted file mode 100644 index a6bae07..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+layout.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -
-
-

- {data.project.name} -

- - {#key $page.url.pathname} - -
- - -
-
- {/key} -
-
- -
- {@render children()} -
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/+page.server.ts deleted file mode 100644 index dd1cd9e..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.server.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { db } from '$lib/db' -import { - and, - asc, - desc, - eq, - getTableColumns, - ilike, - sql, -} from 'drizzle-orm' -import type { PageServerLoad } from './$types' -import { shortener, visitor } from '$lib/db/schema' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { nanoid } from 'nanoid' -import { isAlphanumeric } from '$lib/utils' -import { generateId } from 'lucia' - -export const load = (async (event) => { - const { project: selectedProject } = await event.parent() - - const user = event.locals.user - - const search = event.url.searchParams.get('search') - let sortBy = event.url.searchParams.get('sortBy') - let page = parseInt(event.url.searchParams.get('page') ?? '1') - let perPage = parseInt( - event.url.searchParams.get('perPage') ?? '12', - ) - - if (isNaN(page)) { - page = 1 - } - - if (isNaN(perPage)) { - perPage = 10 - } - - if ( - sortBy !== 'latest' && - sortBy !== 'oldest' && - sortBy !== 'most_visited' - ) { - sortBy = 'latest' - } - - const shortenerColumns = getTableColumns(shortener) - const shorteners = db - .select({ - ...shortenerColumns, - visitorCount: sql`count(${visitor.id})`, - }) - .from(shortener) - .where( - and( - eq(shortener.userId, user.id), - eq(shortener.projectId, selectedProject.id), - search - ? ilike(shortener.link, `%${decodeURI(search)}%`) - : undefined, - ), - ) - .leftJoin(visitor, eq(shortener.id, visitor.shortenerId)) - .groupBy(shortener.id) - .offset(perPage * (page - 1)) - .limit(perPage) - - if (sortBy === 'latest') { - shorteners.orderBy(desc(shortener.createdAt)) - } else if (sortBy === 'oldest') { - shorteners.orderBy(asc(shortener.createdAt)) - } else if (sortBy === 'most_visited') { - shorteners.orderBy(sql`count(${visitor.id}) desc`) - } - - const pagination = db - .select({ - total: sql`count(*)`.as('total'), - }) - .from(shortener) - .where( - and( - eq(shortener.userId, user.id), - eq(shortener.projectId, selectedProject.id), - search - ? ilike(shortener.link, `%${decodeURI(search)}%`) - : undefined, - ), - ) - - return { - selectedProject, - shorteners, - page, - perPage, - search, - sortBy, - pagination, - form: await superValidate({ active: true }, zod(formSchema), { - errors: false, - }), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - create: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const { id } = event.params - const user = event.locals.user - const project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and(eq(project.userId, user.id), eq(project.uuid, id)), - }) - - if (!project) { - return fail(400, { - form, - }) - } - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and(eq(shortener.code, form.data.custom_code)), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project && !project.enable_custom_domain) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - - if (!shortener.project) continue - - if ( - shortener.project.custom_domain === project.custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - - const code = form.data.custom_code_enable - ? form.data.custom_code - : nanoid(8) - await db.insert(shortener).values({ - id: generateId(8), - link: form.data.link, - projectId: project.id, - userId: user.id, - code: code, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/+page.svelte deleted file mode 100644 index 0ec2332..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/+page.svelte +++ /dev/null @@ -1,266 +0,0 @@ - - -
- - -
-
- -{#await fetchShorteners(pageNumber, perPage, sortBy, selectedProject, search)} -
- {#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as _} - - {/each} -
-{:then result} - {#if result.shorteners.length > 0} - -
- {#each result.shorteners as shortener} - - {/each} -
-
-
- { - perPage = parseInt(value) - pageNumber = 1 - }}> - - {perPage} - - - - Page Size - {#each [12, 24, 48, 96] as pageSize} - - {pageSize} - - {/each} - - - - - {#snippet children({ pages, currentPage })} - - - - - - - - {#each pages as page (page.key)} - {#if page.type === 'ellipsis'} - - - - {:else} - - - {page.value} - - - {/if} - {/each} - - - - - - - - {/snippet} - -
- {:else} -
-
-
-
-
-
No Shortener Found
-

Add a new shortener

-
- -
-
-
-
- {/if} -{/await} - - { - if (!open) { - history.back() - } - }}> - - - Edit Shortener - - Edit Shortener Here. Click Save To Save. - - - - - - - - - { - if (!open) { - history.back() - } - }}> - - - Shortener QR - - Use this QR code to share the shortener. - - - - - - - diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/[...catchall]/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/[...catchall]/+page.svelte deleted file mode 100644 index 101da74..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/[...catchall]/+page.svelte +++ /dev/null @@ -1,8 +0,0 @@ -
-
-
-
404
-
Page Not Found
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/(components)/form.svelte deleted file mode 100644 index 6aaa314..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/(components)/form.svelte +++ /dev/null @@ -1,208 +0,0 @@ - - -
-
Preview
-
-
- {#if isPreviewLoading} -
- -
- {:else if previewData} - -
- {previewData.title} -
- {/if} -
-
-
- - - - {#snippet children({ props })} - Link - - {/snippet} - - Shortener link - - - - - {#snippet children({ props })} - - Custom Code - {/snippet} - - - {#if $formData.custom_code_enable} - - - {#snippet children({ props })} - - {/snippet} - - - Custom Code For The Shortener - - - - {/if} - - - {#snippet children({ props })} - - iOS Link - {/snippet} - - - {#if $formData.ios} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for iOS - - - {/if} - - - {#snippet children({ props })} - - Android Link - {/snippet} - - - {#if $formData.android} - - - {#snippet children({ props })} - - {/snippet} - - Shortener link for Android - - - {/if} - - - {#snippet children({ props })} - - Active - {/snippet} - - - - - {#if $submitting} - - {/if} - Save - - diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.server.ts deleted file mode 100644 index 8d29e26..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.server.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { PageServerLoad } from './$types' -import { fail, setError, superValidate } from 'sveltekit-superforms' -import { zod } from 'sveltekit-superforms/adapters' -import { formSchema } from './schema' -import type { Actions } from './$types' -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import { shortener } from '$lib/db/schema' -import { eq } from 'drizzle-orm' -import { isAlphanumeric } from '$lib/utils' - -export const load = (async (event) => { - const { project: selectedProject } = await event.parent() - const { linkid } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - id: true, - code: true, - ios: true, - ios_link: true, - android: true, - android_link: true, - link: true, - active: true, - }, - where: (shortener, { eq, and }) => - and( - eq(shortener.id, linkid), - eq(shortener.projectId, selectedProject.id), - ), - }) - - if (!shortener) { - redirect(300, `/dashboard/projects/${selectedProject.id}`) - } - - return { - shortener, - form: await superValidate( - { - ...shortener, - custom_code_enable: true, - custom_code: shortener.code, - }, - zod(formSchema), - ), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const { id } = event.params - const user = event.locals.user - const project = await db.query.project.findFirst({ - where: (project, { eq, and }) => - and(eq(project.userId, user.id), eq(project.uuid, id)), - }) - - if (!project) { - return fail(400, { - form, - }) - } - - if (form.data.custom_code_enable) { - if (!form.data.custom_code) { - return setError( - form, - 'custom_code', - 'Please Enter Custom Code', - ) - } - if (!isAlphanumeric(form.data.custom_code)) { - return setError( - form, - 'custom_code', - 'Code cannot contain special characters', - ) - } - - const customCodeExist = await db.query.shortener.findMany({ - where: (shortener, { eq, and, ne }) => - and( - eq(shortener.code, form.data.custom_code), - ne(shortener.id, event.params.linkid), - ), - with: { - project: true, - }, - }) - - for (const shortener of customCodeExist) { - if (!shortener.project && !project.enable_custom_domain) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - - if (!shortener.project) continue - - if ( - shortener.project.custom_domain === project.custom_domain - ) { - return setError( - form, - 'custom_code', - 'Duplicated Custom Code', - ) - } - } - } - - await db - .update(shortener) - .set({ - link: form.data.link, - projectId: project.id, - userId: user.id, - code: form.data.custom_code_enable - ? form.data.custom_code - : undefined, - ios: form.data.ios, - ios_link: form.data.ios_link, - android: form.data.android, - android_link: form.data.android_link, - }) - .where(eq(shortener.id, event.params.linkid)) - - return { form } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte deleted file mode 100644 index dbafcfc..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -{#if !shallowRouting} - -
-
-
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/schema.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/schema.ts deleted file mode 100644 index 88634ef..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - project: z.string().optional(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - android: z.boolean(), - android_link: z - .union([z.literal(''), z.string().url()]) - .optional() - .nullable(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/(components)/qr.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/(components)/qr.svelte deleted file mode 100644 index 431761a..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/(components)/qr.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -
- - {value} - - {value} -
- - - - QR Link - - - - - Standard - - - With Style - - - - -
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.server.ts deleted file mode 100644 index 350b855..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from '$lib/db' -import { redirect } from '@sveltejs/kit' -import type { PageServerLoad } from './$types' - -export const load = (async (event) => { - const { project: selectedProject } = await event.parent() - const { linkid } = event.params - - const shortener = await db.query.shortener.findFirst({ - columns: { - code: true, - }, - where: (shortener, { eq, and }) => - and( - eq(shortener.code, linkid), - eq(shortener.projectId, selectedProject.id), - ), - }) - - if (!shortener) { - redirect(300, `/dashboard/projects/${selectedProject.id}`) - } - - return { shortener, project: selectedProject } -}) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.svelte deleted file mode 100644 index 10f0d94..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/links/[linkid]/qr/+page.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - -{#if !shallowRouting} - -
- -
-
-{:else} - -{/if} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/schema.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/schema.ts deleted file mode 100644 index 1b6547a..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - link: z.string().url(), - active: z.boolean(), - ios: z.boolean(), - ios_link: z.string().url().optional(), - android: z.boolean(), - android_link: z.string().url().optional(), - custom_code_enable: z.boolean(), - custom_code: z.string(), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/DemoQR.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/DemoQR.svelte deleted file mode 100644 index a92721f..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/DemoQR.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -{value} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-info.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-info.svelte deleted file mode 100644 index 7ceff19..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-info.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - - - - {#if cname_record} - CNAME - {/if} - {#if a_record || aaaa_record} - A and AAAA - {/if} - - {#if cname_record} - - - - Type - Host - Value - - - CNAME - {host} - {cname_record} - - - - {/if} - {#if a_record || aaaa_record} - -
- {#if a_record} - - - Type - Host - Value - - - A - {host} - {a_record} - - - {/if} - {#if aaaa_record} - - - Type - Host - Value - - - AAAA - {host} - {aaaa_record} - - - {/if} -
-
- {/if} -
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-tooltip.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-tooltip.svelte deleted file mode 100644 index b5d669b..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/dns-tooltip.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - {#if custom_ip} - {custom_ip} - {:else if cname_record} - {cname_record} - {:else if a_record} - {a_record} - {:else if aaaa_record} - {aaaa_record} - {:else} - {'Public IP not found'} - {/if} - - - {#if custom_ip} -
- {'Create a CNAME/ALIAS record for ' + - domain + - ' to ' + - custom_ip} -
- {/if} - {#if cname_record} -
- {'Create a CNAME/ALIAS record for ' + - domain + - ' to ' + - cname_record} -
- {/if} - {#if a_record} -
- {'Create a A record for ' + domain + ' to ' + a_record} -
- {/if} - - {#if aaaa_record} -
- {'Create a AAAA record for ' + - domain + - ' to ' + - aaaa_record} -
- {/if} - {#if !(custom_ip || cname_record || a_record || aaaa_record)} -
- {'Public IP not found'} -
- {/if} -
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/form.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/form.svelte deleted file mode 100644 index 0ccad24..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/(components)/form.svelte +++ /dev/null @@ -1,250 +0,0 @@ - - - - - - {#snippet children({ props })} - Name - - {/snippet} - - Update Project Name - - -
- -
- - - {#snippet children({ props })} - Background Color - - {/snippet} - - QR Code background color - - - - - {#snippet children({ props })} - Foreground Color - - {/snippet} - - QR Code foreground color - - - - - {#snippet children({ props })} - - Image (Pro) - -
- {#if !$formData.qrImage && !qrImageBase64} - - {:else} - - {/if} -
- Click to edit - { - const file = e.currentTarget.files?.item(0) - if (!file) return - - if (file.size > 2097152) { - toast.error('Too Big! Max file size is 2MB') - return - } - $formData.qrImage = file - }} /> - {/snippet} -
- -
- - - {#snippet children({ props })} - - Corner Square Style (Pro) - - - {/snippet} - -
- - - -
-
- - - {#snippet children({ props })} - - Dot Style (Pro) - - - {/snippet} - -
- - -
-
- - {#if $submitting} - - {/if} - Save - - diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.server.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.server.ts deleted file mode 100644 index 7eca1a3..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.server.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { - message, - setError, - setMessage, - superValidate, - fail, - withFiles, -} from 'sveltekit-superforms' -import { - formSchema, - deleteSchema, - customDomainFormSchema, - enableCustomDomainFormSchema, -} from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { - project as projectTable, - shortener, - visitor, -} from '$lib/db/schema' -import { and, eq } from 'drizzle-orm' -import { - checkDomainAvailable, - createCustomDomain, - deleteCustomDomain, -} from '$lib/server/domain' -import { env } from '$env/dynamic/private' - -export const load = (async (event) => { - const { project } = await event.parent() - - let cnameRecord = '' - let aRecord = '' - let aaaaRecord = '' - - const provider = env.PRIVATE_HOSTING_PROVIDER - - if (provider === 'fly.io') { - cnameRecord = env.PRIVATE_FLYIO_CNAME - aRecord = env.PRIVATE_FLYIO_IPV4 - aaaaRecord = env.PRIVATE_FLYIO_IPV6 - } else if (provider === 'railway') { - } else { - aRecord = env.PUBLIC_SHORTENER_IP || '' - } - - return { - form: await superValidate( - { - name: project.name, - qr_background: project.qr_background, - qr_foreground: project.qr_foreground, - qrCornerSquareStyle: project.qrCornerSquareStyle, - qrDotStyle: project.qrDotStyle, - }, - zod(formSchema), - ), - qrImageBase64: project.qrImageBase64, - enableCustomDomainForm: await superValidate( - { enableDomain: project.custom_domain || '' }, - zod(enableCustomDomainFormSchema), - ), - customDomainForm: await superValidate( - { domain: project.custom_domain || '' }, - zod(customDomainFormSchema), - ), - deleteForm: await superValidate( - { deleteShorteners: true }, - zod(deleteSchema), - { - id: 'deleteProject', - }, - ), - cnameRecord, - aRecord, - aaaaRecord, - } -}) satisfies PageServerLoad - -export const actions: Actions = { - update: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail( - 400, - withFiles({ - form, - }), - ) - } - const user = event.locals.user - - const qrImage = form.data.qrImage - - const qrImageType = qrImage ? qrImage.type : undefined - const qrImageBlob = qrImage - ? await qrImage.arrayBuffer() - : undefined - const qrImageBase64 = qrImageBlob - ? Buffer.from(qrImageBlob).toString('base64') - : undefined - - await db - .update(projectTable) - .set({ - name: form.data.name, - qr_background: form.data.qr_background, - qr_foreground: form.data.qr_foreground, - qrCornerSquareStyle: - user.plan !== 'free' - ? form.data.qrCornerSquareStyle - : undefined, - qrDotStyle: - user.plan !== 'free' ? form.data.qrDotStyle : undefined, - qrImageBase64: - user.plan !== 'free' - ? qrImage - ? `data:${qrImageType};base64,${qrImageBase64}` - : undefined - : undefined, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, user.id), - ), - ) - - return withFiles({ - form, - }) - }, - enable_custom_domain: async (event) => { - const form = await superValidate( - event, - zod(enableCustomDomainFormSchema), - ) - if (!form.valid) { - return fail(400, { - form, - }) - } - - if (form.data.enableDomain === '') { - return setError(form, 'enableDomain', 'domain cannot be empty') - } - - const userId = event.locals.user.id - - if ( - !event.locals.user.email_verified && - env.PRIVATE_MAIL_PROVIDER && - env.PRIVATE_MAIL_PROVIDER !== '' - ) { - return setError( - form, - 'enableDomain', - 'Please verify your email account', - ) - } - - if (event.locals.user.plan === 'free') { - return setError( - form, - 'enableDomain', - 'Please upgrade your account to pro plan to use this feature', - ) - } - - if (event.locals.user.plan === 'pro') { - const projectsWithEnabledDomains = - await db.query.project.findMany({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.userId, userId), - eq(projectTable.enable_custom_domain, true), - ), - }) - - if (projectsWithEnabledDomains.length >= 5) { - return setError( - form, - 'enableDomain', - 'You are only allowed to use maximum 5 custom domains in pro plan', - ) - } - } - - const existingProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - }) - - if (!existingProject) { - return fail(400, { message: 'Project not found' }) - } - - const sameDomainDifferentProject = - await db.query.project.findFirst({ - where: (projectTable, { eq, and, ne }) => - and( - ne(projectTable.uuid, event.params.id), - eq(projectTable.custom_domain, form.data.enableDomain), - ), - }) - - if (sameDomainDifferentProject) { - return setError(form, 'enableDomain', 'Domain already taken') - } - - const domainAvailable = await checkDomainAvailable( - form.data.enableDomain, - ) - - if (!domainAvailable) { - return setError(form, 'enableDomain', 'Domain is not available') - } - - const customDomain = await createCustomDomain( - form.data.enableDomain, - ) - - if (!customDomain.success) { - return setError( - form, - 'enableDomain', - 'Cannot create custom domain', - ) - } - - await db - .update(projectTable) - .set({ - custom_domain: form.data.enableDomain, - custom_domain_id: customDomain.id, - custom_ip: customDomain.ip, - enable_custom_domain: true, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - - return setMessage(form, 'Custom Domain Enabled') - }, - disable_custom_domain: async (event) => { - return { message: 'Disabling custom domain is unavailable' } - const userId = event.locals.user.id - - const existingProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - }) - - if (!existingProject) { - return fail(400, { message: 'Project not found' }) - } - - const deleteOldCustomDomain = await deleteCustomDomain( - existingProject.custom_domain_id, - ) - - if (!deleteOldCustomDomain.success) { - return { message: 'Cannot delete old custom domain' } - } - - await db - .update(projectTable) - .set({ - enable_custom_domain: false, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - - return { message: 'Custom domain disabled' } - }, - update_custom_domain: async (event) => { - const form = await superValidate( - event, - zod(customDomainFormSchema), - ) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - const existingProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - }) - - if (!existingProject || !existingProject.enable_custom_domain) { - return fail(400, { - form, - }) - } - - const sameDomainDifferentProject = - await db.query.project.findFirst({ - where: (projectTable, { eq, and, ne }) => - and( - ne(projectTable.uuid, event.params.id), - eq(projectTable.custom_domain, form.data.domain), - ), - }) - - if (sameDomainDifferentProject) { - return setError(form, 'domain', 'Domain already taken') - } - - const sameDomainSameProject = await db.query.project.findFirst({ - where: (projectTable, { eq, and }) => - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.custom_domain, form.data.domain), - ), - }) - - if (sameDomainSameProject) { - return { form } - } - - const domainAvailable = await checkDomainAvailable( - form.data.domain, - ) - - if (!domainAvailable) { - return setError(form, 'domain', 'Domain is not available') - } - - const customDomain = await createCustomDomain(form.data.domain) - - if (!customDomain.success) { - return setError(form, 'domain', 'Cannot create custom domain') - } - - const deleteOldCustomDomain = await deleteCustomDomain( - existingProject.custom_domain_id, - ) - - if (!deleteOldCustomDomain.success) { - return setError( - form, - 'domain', - 'Cannot delete old custom domain', - ) - } - - await db - .update(projectTable) - .set({ - custom_domain: form.data.domain, - custom_domain_id: customDomain.id, - custom_ip: customDomain.ip, - }) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - - return { - form, - } - }, - delete: async (event) => { - const userId = event.locals.user.id - - const form = await superValidate(event, zod(deleteSchema)) - - if (!form.valid) { - return fail(400, { - form, - }) - } - - try { - const deletedProject = await db - .delete(projectTable) - .where( - and( - eq(projectTable.uuid, event.params.id), - eq(projectTable.userId, userId), - ), - ) - .returning() - - await deleteCustomDomain(deletedProject[0].custom_domain_id) - - if (form.data.deleteShorteners) { - const deletedShorteners = await db - .delete(shortener) - .where( - and( - eq(shortener.projectId, deletedProject[0].id), - eq(shortener.userId, userId), - ), - ) - .returning() - deletedShorteners.map(async (shortener) => { - await db - .delete(visitor) - .where(eq(visitor.shortenerId, shortener.id)) - }) - } else { - await db - .update(shortener) - .set({ projectId: null }) - .where( - and( - eq(shortener.projectId, deletedProject[0].id), - eq(shortener.userId, userId), - ), - ) - } - - return { - form, - } - } catch (error) { - return fail(400, { - form, - }) - } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.svelte b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.svelte deleted file mode 100644 index 503af33..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/+page.svelte +++ /dev/null @@ -1,397 +0,0 @@ - - - -
-
-

Custom Domain

-

- Update project domain. -

-
- - - - -
-
- {#if data.project.domain_status === 'pending'} - - {:else if data.project.domain_status === 'verified'} - - {:else if data.project.domain_status === 'disabled'} - - {/if} -
-
- {#if data.project.enable_custom_domain && data.project.custom_domain} - - {data.project.custom_domain} - - - custom domain - - - - - {:else} - - {env.PUBLIC_SHORTENER_URL} - - - default domain - {/if} -
-
- {#if !data.project.enable_custom_domain} - - - Enable Custom Domain - - - - - - Are you absolutely sure? - - - Enabling a custom domain will allow you to use - your project with a custom domain. - - -
- - - {#snippet children({ props })} - Add Custom Domain -
- -
- {/snippet} -
- - - - - - Update Project Domain (leave blank to use - default) - - -

- Only include the domain name, not the - protocol. -

-

- Make sure the domain is pointing to - our server. -

-

- Please contact us if you need a custom - domain. -

-
-
-
-
- -
-
- - {#if data.cnameRecord || data.aRecord || data.aaaaRecord} - - {/if} - - - - Cancel - - - -
-
- {:else} - - {/if} -
-
-
-
- {#if data.project.enable_custom_domain} -
- - - {#snippet children({ props })} - Add Custom Domain -
- - - {#if $customDomainSubmitting} - - {/if} - Update - -
- {/snippet} -
- - - - - - Update Project Domain (leave blank to use default) - - -

- Only include the domain name, not the protocol. -

-

- Make sure the domain is pointing to our server. -

-

- Please contact us if you need a custom domain. -

-
-
-
-
- -
-
- {/if} - -
-

Settings

-

- Update project settings. -

-
- - - - - - -
-

Danger Zone

-
-
-
-
- Delete Project - - Permanently delete your project - -
- (deleteDialogOpen = open)}> - - Delete Project - - - - - Are you sure absolutely sure? - - - This action cannot be undone. This will permanently - delete your project and all its associated data. - - -
- - - {#snippet children({ props })} - -
- - - Delete Shorteners? - -
- {/snippet} -
- -
-
- - - {#if $submitting} - - {/if} - Delete - -
-
-
-
-
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/schema.ts b/frontend/src/routes/(app)/dashboard/projects/[id]/settings/schema.ts deleted file mode 100644 index c4d9e86..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/[id]/settings/schema.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - name: z.string().nonempty(), - qr_background: z - .string() - .min(1, { message: 'Background color is required' }) - .max(7), - qr_foreground: z - .string() - .min(1, { message: 'Foreground color is required' }) - .max(7), - qrCornerSquareStyle: z.custom<'dot' | 'square' | 'extra-rounded'>(), - qrDotStyle: z.custom<'square' | 'rounded'>(), - qrImage: z - .instanceof(File, { message: 'Please upload a file' }) - .refine((file) => file.size <= 2097152, `Max image size is 2MB.`) - .refine( - (file) => - file.type === 'image/jpeg' || file.type === 'image/png', - { - message: 'Only JPEG or PNG files are allowed', - }, - ) - .optional() - .nullable(), -}) - -export const enableCustomDomainFormSchema = z.object({ - enableDomain: z.string(), -}) - -export const customDomainFormSchema = z.object({ - domain: z.string(), -}) - -export type FormSchema = typeof formSchema - -export const deleteSchema = z.object({ - deleteShorteners: z.boolean(), -}) diff --git a/frontend/src/routes/(app)/dashboard/projects/schema.ts b/frontend/src/routes/(app)/dashboard/projects/schema.ts deleted file mode 100644 index 01beeb3..0000000 --- a/frontend/src/routes/(app)/dashboard/projects/schema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - name: z.string().min(1, { message: 'Name is required' }), -}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/settings/(components)/sidebar-nav.svelte b/frontend/src/routes/(app)/dashboard/settings/(components)/sidebar-nav.svelte deleted file mode 100644 index 1b2d9ec..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/(components)/sidebar-nav.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/frontend/src/routes/(app)/dashboard/settings/+layout.server.ts b/frontend/src/routes/(app)/dashboard/settings/+layout.server.ts deleted file mode 100644 index 2e331d8..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/+layout.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { LayoutServerLoad } from './$types' - -export const load = (async (event) => { - const { breadcrumbs: parentBreadcrumbs } = await event.parent() - - const breadcrumbs = [ - ...parentBreadcrumbs, - { name: 'Settings', path: '/dashboard/settings' }, - ] - - const page_title = 'Settings' - - return { breadcrumbs, page_title } -}) satisfies LayoutServerLoad diff --git a/frontend/src/routes/(app)/dashboard/settings/+layout.svelte b/frontend/src/routes/(app)/dashboard/settings/+layout.svelte deleted file mode 100644 index b107d5c..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/+layout.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -
-
-

Settings

-

Manage your account settings.

-
- -
- -
- -
- -
-
-
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/account/+page.server.ts b/frontend/src/routes/(app)/dashboard/settings/account/+page.server.ts deleted file mode 100644 index 64f645b..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/account/+page.server.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { error, fail } from '@sveltejs/kit' -import { - message, - setError, - superValidate, -} from 'sveltekit-superforms' -import { formSchema, verifyEmailSchema } from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { user } from '$lib/db/schema' -import { eq } from 'drizzle-orm' -import { sendEmailVerification } from '$lib/server/email' - -export const load = (async (event) => { - const { username, email } = event.locals.user - - return { - form: await superValidate( - { username: username || '', email }, - zod(formSchema), - ), - verify_email_form: await superValidate(zod(verifyEmailSchema)), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - update: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - if (form.data.username) { - await db - .update(user) - .set({ - username: form.data.username, - }) - .where(eq(user.id, userId)) - } - - return { - form, - } - }, - verify_email: async (event) => { - const form = await superValidate(event, zod(verifyEmailSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - try { - await sendEmailVerification({ - userId: event.locals.user.id, - email: event.locals.user.email, - }) - } catch (e) { - return message(form, 'Error sending email verification', { - status: 500, - }) - } - - return message(form, 'Email verification sent') - }, -} diff --git a/frontend/src/routes/(app)/dashboard/settings/account/+page.svelte b/frontend/src/routes/(app)/dashboard/settings/account/+page.svelte deleted file mode 100644 index 833f533..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/account/+page.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - -
-
-

Account

-

- Update your account settings. -

-
- - -
- - - {#snippet children({ props })} - Username - - {/snippet} - - Change Your Username - - - - - {#snippet children({ props })} - - Email - - {#if data.user.email_verified} - (verified) - {:else} - (unverified) - {/if} - - -
- - -
- {/snippet} -
- Change Your Email - -
- - {#if $submitting} - - {/if} - Save - -
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/account/schema.ts b/frontend/src/routes/(app)/dashboard/settings/account/schema.ts deleted file mode 100644 index 9c3c241..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/account/schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - username: z.string().optional(), - email: z.string().email().optional(), -}) - -export const verifyEmailSchema = z.object({}) - -export type FormSchema = typeof formSchema diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/(components)/DemoQR.svelte b/frontend/src/routes/(app)/dashboard/settings/qr/(components)/DemoQR.svelte deleted file mode 100644 index aaed3e0..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/(components)/DemoQR.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -{value} diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/+page.server.ts b/frontend/src/routes/(app)/dashboard/settings/qr/+page.server.ts deleted file mode 100644 index 9af6a54..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/+page.server.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { fail } from '@sveltejs/kit' -import { superValidate, withFiles } from 'sveltekit-superforms' -import { formSchema } from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { eq } from 'drizzle-orm' -import { user as userTable } from '$lib/db/schema' - -export const load = (async (event) => { - const user = event.locals.user - - const qr_background = user.qrBackground - const qr_foreground = user.qrForeground - - return { - qrImageBase64: user.qrImageBase64, - form: await superValidate( - { - qr_background, - qr_foreground, - qrCornerSquareStyle: user.qrCornerSquareStyle, - qrDotStyle: user.qrDotStyle, - }, - zod(formSchema), - { errors: false }, - ), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - default: async (event) => { - const form = await superValidate(event, zod(formSchema)) - if (!form.valid) { - return fail( - 400, - withFiles({ - form, - }), - ) - } - - const user = event.locals.user - const userId = event.locals.user.id - - const { - qr_background, - qr_foreground, - qrCornerSquareStyle, - qrDotStyle, - qrImage, - } = form.data - - const qrImageType = qrImage ? qrImage.type : undefined - const qrImageBlob = qrImage - ? await qrImage.arrayBuffer() - : undefined - const qrImageBase64 = qrImageBlob - ? Buffer.from(qrImageBlob).toString('base64') - : undefined - - await db - .update(userTable) - .set({ - qrBackground: qr_background, - qrForeground: qr_foreground, - }) - .where(eq(userTable.id, userId)) - - if (user.plan !== 'free') { - await db - .update(userTable) - .set({ - qrCornerSquareStyle, - qrDotStyle, - qrImageBase64: qrImage - ? `data:${qrImageType};base64,${qrImageBase64}` - : undefined, - }) - .where(eq(userTable.id, userId)) - } - - return withFiles({ - form, - }) - }, -} diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/+page.svelte b/frontend/src/routes/(app)/dashboard/settings/qr/+page.svelte deleted file mode 100644 index c8471c7..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/+page.svelte +++ /dev/null @@ -1,237 +0,0 @@ - - -
-
-

QR

-

- Update your QR settings. -

-
- - -
- -
- -
- - - {#snippet children({ props })} - Background Color - - {/snippet} - - QR Code background color - - - - - {#snippet children({ props })} - Foreground Color - - {/snippet} - - QR Code foreground color - - - - - {#snippet children({ props })} - - Image (Pro) - -
- {#if !$formData.qrImage && !data.qrImageBase64} - - {:else} - - {/if} -
- Click to edit - { - const file = e.currentTarget.files?.item(0) - if (!file) return - - if (file.size > 2097152) { - toast.error('Too Big! Max file size is 2MB') - return - } - $formData.qrImage = file - }} /> - {/snippet} -
- -
- - - {#snippet children({ props })} - - Corner Square Style (Pro) - - - {/snippet} - -
- - - -
-
- - - {#snippet children({ props })} - - Dot Style (Pro) - - - {/snippet} - -
- - -
-
- - - {#if $submitting} - - {/if} - Save - -
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/qr/schema.ts b/frontend/src/routes/(app)/dashboard/settings/qr/schema.ts deleted file mode 100644 index ecbfd5a..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/qr/schema.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { z } from 'zod' - -export const formSchema = z.object({ - qr_background: z - .string() - .min(1, { message: 'Background color is required' }) - .max(7), - qr_foreground: z - .string() - .min(1, { message: 'Foreground color is required' }) - .max(7), - qrCornerSquareStyle: z.custom<'dot' | 'square' | 'extra-rounded'>(), - qrDotStyle: z.custom<'square' | 'rounded'>(), - qrImage: z - .instanceof(File, { message: 'Please upload a file' }) - .refine((file) => file.size <= 2097152, `Max image size is 2MB.`) - .refine( - (file) => - file.type === 'image/jpeg' || file.type === 'image/png', - { - message: 'Only JPEG or PNG files are allowed', - }, - ) - .optional() - .nullable(), -}) diff --git a/frontend/src/routes/(app)/dashboard/settings/security/+page.server.ts b/frontend/src/routes/(app)/dashboard/settings/security/+page.server.ts deleted file mode 100644 index f505b05..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/security/+page.server.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { db } from '$lib/db' -import type { PageServerLoad, Actions } from './$types' -import { fail } from '@sveltejs/kit' -import { setError, superValidate } from 'sveltekit-superforms' -import { - changePasswordFormSchema, - deleteAccountSchema, -} from './schema' -import { zod } from 'sveltekit-superforms/adapters' -import { project, shortener, user, visitor } from '$lib/db/schema' -import { eq, inArray } from 'drizzle-orm' -import { lucia } from '$lib/server/auth' -import { env } from '$env/dynamic/private' -import * as argon2 from 'argon2' - -export const load = (async (event) => { - return { - form: await superValidate(zod(changePasswordFormSchema)), - deleteAccountForm: await superValidate(zod(deleteAccountSchema)), - } -}) satisfies PageServerLoad - -export const actions: Actions = { - change_password: async (event) => { - const form = await superValidate( - event, - zod(changePasswordFormSchema), - ) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - if (event.locals.user.googleId) { - return setError( - form, - 'old_password', - 'Unable to set a password if using google login', - ) - } - - const userData = await db.query.user.findFirst({ - where: (user, { eq }) => eq(user.id, userId), - }) - - if (!userData) { - return setError(form, 'old_password', 'User Not Found') - } - - if (!userData.password) { - return setError( - form, - 'old_password', - 'User is using other login method', - ) - } - - const passwordMatch = await argon2.verify( - userData.password, - form.data.old_password, - ) - - if (!passwordMatch) { - return setError(form, 'old_password', 'Old Password Not Match') - } - - const newPassword = await argon2.hash(form.data.new_password) - - await db - .update(user) - .set({ - password: newPassword, - }) - .where(eq(user.id, userId)) - - await lucia.invalidateUserSessions(userId) - - const session = await lucia.createSession(userId, {}) - const sessionCookie = lucia.createSessionCookie(session.id) - - event.cookies.set(sessionCookie.name, sessionCookie.value, { - ...sessionCookie.attributes, - path: '/', - secure: env.APP_ENV === 'prod', - }) - - return { - form, - } - }, - delete_account: async (event) => { - const form = await superValidate(event, zod(deleteAccountSchema)) - if (!form.valid) { - return fail(400, { - form, - }) - } - - const userId = event.locals.user.id - - const userData = await db.query.user.findFirst({ - where: (user, { eq }) => eq(user.id, userId), - }) - - if (!userData) { - return setError(form, 'password', 'User Not Found') - } - - if (userData.googleId) { - await lucia.invalidateUserSessions(userId) - - await db.delete(user).where(eq(user.id, userId)) - - const shorteners = await db - .delete(shortener) - .where(eq(shortener.userId, userId)) - .returning() - - await db.delete(project).where(eq(project.userId, userId)) - - await db.delete(visitor).where( - inArray( - visitor.shortenerId, - shorteners.map((shortener) => shortener.id), - ), - ) - - return { - form, - } - } - - if (!userData.password) { - return setError(form, 'password', 'User Not Found') - } - - const passwordMatch = await argon2.verify( - userData.password, - form.data.password, - ) - - if (!passwordMatch) { - return setError(form, 'password', 'Invalid Password') - } - - await lucia.invalidateUserSessions(userId) - - await db.delete(user).where(eq(user.id, userId)) - - const shorteners = await db - .delete(shortener) - .where(eq(shortener.userId, userId)) - .returning() - - await db.delete(project).where(eq(project.userId, userId)) - - await db.delete(visitor).where( - inArray( - visitor.shortenerId, - shorteners.map((shortener) => shortener.id), - ), - ) - - return { - form, - } - }, -} diff --git a/frontend/src/routes/(app)/dashboard/settings/security/+page.svelte b/frontend/src/routes/(app)/dashboard/settings/security/+page.svelte deleted file mode 100644 index 34c9e6c..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/security/+page.svelte +++ /dev/null @@ -1,190 +0,0 @@ - - -
-
-

Password

-

Update your password

-
- - -
- - - {#snippet children({ props })} - Old Password - - {/snippet} - - Old Password To Confirm - - - - - {#snippet children({ props })} - New Password - - {/snippet} - - Update Password - - - - - {#snippet children({ props })} - Confirm Password - - {/snippet} - - Confirm New Password - - - - {#if $submitting} - - {/if} - Change Password - -
- - - -
-

Danger Zone

-

- Changes here are irreversible -

-
- - -
-
-
- Delete Account - - Permanently delete your account and all data - -
- - - Delete Account - - - -
- -
-
- Delete Account - - This will permanently delete your account and all its - associated data. - -
-
-
- - - {#snippet children({ props })} - Password - - {/snippet} - - - Enter your password to delete account - - - -
- - - {#if $deleteAccountSubmitting} - - {/if} - Delete - -
-
-
-
-
-
-
diff --git a/frontend/src/routes/(app)/dashboard/settings/security/schema.ts b/frontend/src/routes/(app)/dashboard/settings/security/schema.ts deleted file mode 100644 index f47fed5..0000000 --- a/frontend/src/routes/(app)/dashboard/settings/security/schema.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from 'zod' - -export const changePasswordFormSchema = z - .object({ - old_password: z.string(), - new_password: z.string().min(8, { - message: 'Password must be at least 8 characters long', - }), - confirm_password: z.string().min(8, { - message: 'Password must be at least 8 characters long', - }), - }) - .refine( - (data) => data.new_password == data.confirm_password, - "Passwords didn't match.", - ) - -export const verifyEmailSchema = z.object({}) - -export const deleteAccountSchema = z.object({ password: z.string() }) - -export type FormSchema = typeof changePasswordFormSchema diff --git a/frontend/src/routes/(public)/(landing)/+layout.svelte b/frontend/src/routes/(public)/(landing)/+layout.svelte index b0dac76..71eeb59 100644 --- a/frontend/src/routes/(public)/(landing)/+layout.svelte +++ b/frontend/src/routes/(public)/(landing)/+layout.svelte @@ -21,12 +21,7 @@ @@ -46,7 +41,7 @@