From 08e02dd556b22652bcc7b3842f4c2a3844e6b47d Mon Sep 17 00:00:00 2001 From: Ben de Roo Date: Thu, 20 Aug 2026 16:51:58 +0200 Subject: [PATCH] ja --- .gitignore | 51 +- backend/__pycache__/auth.cpython-313.pyc | Bin 0 -> 7532 bytes backend/__pycache__/database.cpython-313.pyc | Bin 0 -> 2826 bytes .../__pycache__/email_service.cpython-313.pyc | Bin 0 -> 10874 bytes .../__pycache__/event_signups.cpython-313.pyc | Bin 0 -> 1563 bytes backend/__pycache__/events.cpython-313.pyc | Bin 0 -> 4951 bytes backend/__pycache__/main.cpython-313.pyc | Bin 0 -> 33184 bytes .../__pycache__/permissions.cpython-313.pyc | Bin 0 -> 1671 bytes backend/__pycache__/rooms.cpython-313.pyc | Bin 0 -> 7046 bytes backend/email_service.py | 354 +- frontend(experimental)/newchat/.gitignore | 27 - frontend(experimental)/newchat/README.md | 33 - .../newchat/app/globals.css | 62 - frontend(experimental)/newchat/app/layout.tsx | 27 - .../newchat/app/loading.tsx | 4 - frontend(experimental)/newchat/app/page.tsx | 239 - .../newchat/components.json | 21 - .../components/account-settings-dialog.tsx | 74 - .../newchat/components/auth-screen.tsx | 161 - .../components/create-event-dialog.tsx | 354 -- .../components/event-detail-dialog.tsx | 294 -- .../newchat/components/glass-modal.tsx | 58 - .../newchat/components/room-background.tsx | 81 - .../newchat/components/room-dialogs.tsx | 224 - .../components/room-settings-dialog.tsx | 302 -- .../newchat/components/sidebar.tsx | 227 - .../newchat/components/theme-provider.tsx | 11 - .../newchat/components/ui/accordion.tsx | 58 - .../newchat/components/ui/alert-dialog.tsx | 141 - .../newchat/components/ui/alert.tsx | 59 - .../newchat/components/ui/aspect-ratio.tsx | 7 - .../newchat/components/ui/avatar.tsx | 50 - .../newchat/components/ui/badge.tsx | 36 - .../newchat/components/ui/breadcrumb.tsx | 115 - .../newchat/components/ui/button-group.tsx | 83 - .../newchat/components/ui/button.tsx | 56 - .../newchat/components/ui/calendar.tsx | 213 - .../newchat/components/ui/card.tsx | 79 - .../newchat/components/ui/carousel.tsx | 262 - .../newchat/components/ui/chart.tsx | 363 -- .../newchat/components/ui/checkbox.tsx | 28 - .../newchat/components/ui/collapsible.tsx | 11 - .../newchat/components/ui/command.tsx | 153 - .../newchat/components/ui/context-menu.tsx | 200 - .../newchat/components/ui/dialog.tsx | 122 - .../newchat/components/ui/drawer.tsx | 118 - .../newchat/components/ui/dropdown-menu.tsx | 200 - .../newchat/components/ui/empty.tsx | 104 - .../newchat/components/ui/field.tsx | 244 - .../newchat/components/ui/form.tsx | 178 - .../newchat/components/ui/hover-card.tsx | 29 - .../newchat/components/ui/input-group.tsx | 170 - .../newchat/components/ui/input-otp.tsx | 71 - .../newchat/components/ui/input.tsx | 22 - .../newchat/components/ui/item.tsx | 193 - .../newchat/components/ui/kbd.tsx | 28 - .../newchat/components/ui/label.tsx | 26 - .../newchat/components/ui/menubar.tsx | 236 - .../newchat/components/ui/navigation-menu.tsx | 128 - .../newchat/components/ui/pagination.tsx | 117 - .../newchat/components/ui/popover.tsx | 31 - .../newchat/components/ui/progress.tsx | 28 - .../newchat/components/ui/radio-group.tsx | 44 - .../newchat/components/ui/resizable.tsx | 45 - .../newchat/components/ui/scroll-area.tsx | 48 - .../newchat/components/ui/select.tsx | 160 - .../newchat/components/ui/separator.tsx | 31 - .../newchat/components/ui/sheet.tsx | 140 - .../newchat/components/ui/sidebar.tsx | 771 --- .../newchat/components/ui/skeleton.tsx | 15 - .../newchat/components/ui/slider.tsx | 28 - .../newchat/components/ui/sonner.tsx | 31 - .../newchat/components/ui/spinner.tsx | 16 - .../newchat/components/ui/switch.tsx | 27 - .../newchat/components/ui/table.tsx | 117 - .../newchat/components/ui/tabs.tsx | 55 - .../newchat/components/ui/textarea.tsx | 22 - .../newchat/components/ui/toast.tsx | 129 - .../newchat/components/ui/toaster.tsx | 35 - .../newchat/components/ui/toggle-group.tsx | 61 - .../newchat/components/ui/toggle.tsx | 45 - .../newchat/components/ui/tooltip.tsx | 30 - .../newchat/components/ui/use-mobile.tsx | 19 - .../newchat/components/ui/use-toast.ts | 191 - .../newchat/components/week-view.tsx | 249 - .../newchat/hooks/use-mobile.ts | 19 - .../newchat/hooks/use-toast.ts | 191 - frontend(experimental)/newchat/lib/api.ts | 274 -- .../newchat/lib/auth-context.tsx | 81 - .../newchat/lib/date-utils.ts | 76 - frontend(experimental)/newchat/lib/hooks.ts | 70 - .../newchat/lib/room-theme.ts | 115 - frontend(experimental)/newchat/lib/types.ts | 61 - frontend(experimental)/newchat/lib/utils.ts | 6 - frontend(experimental)/newchat/missing.md | 16 - .../newchat/next.config.mjs | 11 - .../newchat/package-lock.json | 4212 ----------------- frontend(experimental)/newchat/package.json | 72 - frontend(experimental)/newchat/pnpm-lock.yaml | 3404 ------------- .../newchat/postcss.config.mjs | 8 - .../newchat/public/apple-icon.png | Bin 2626 -> 0 bytes .../newchat/public/backgrounds/all-rooms.png | Bin 1581189 -> 0 bytes .../newchat/public/backgrounds/room-1.png | Bin 1873163 -> 0 bytes .../newchat/public/backgrounds/room-2.png | Bin 2343271 -> 0 bytes .../newchat/public/backgrounds/room-3.png | Bin 2349802 -> 0 bytes .../newchat/public/backgrounds/room-4.png | Bin 1614906 -> 0 bytes .../newchat/public/backgrounds/room-5.png | Bin 1558777 -> 0 bytes .../newchat/public/icon-dark-32x32.png | Bin 585 -> 0 bytes .../newchat/public/icon-light-32x32.png | Bin 566 -> 0 bytes .../newchat/public/icon.svg | 26 - .../newchat/public/placeholder-logo.png | Bin 568 -> 0 bytes .../newchat/public/placeholder-logo.svg | 1 - .../newchat/public/placeholder-user.jpg | Bin 1635 -> 0 bytes .../newchat/public/placeholder.jpg | Bin 1064 -> 0 bytes .../newchat/public/placeholder.svg | 1 - .../newchat/styles/globals.css | 92 - .../newchat/tailwind.config.js | 84 - frontend(experimental)/newchat/tsconfig.json | 27 - requirements.txt | 4 +- 119 files changed, 254 insertions(+), 17469 deletions(-) create mode 100644 backend/__pycache__/auth.cpython-313.pyc create mode 100644 backend/__pycache__/database.cpython-313.pyc create mode 100644 backend/__pycache__/email_service.cpython-313.pyc create mode 100644 backend/__pycache__/event_signups.cpython-313.pyc create mode 100644 backend/__pycache__/events.cpython-313.pyc create mode 100644 backend/__pycache__/main.cpython-313.pyc create mode 100644 backend/__pycache__/permissions.cpython-313.pyc create mode 100644 backend/__pycache__/rooms.cpython-313.pyc delete mode 100644 frontend(experimental)/newchat/.gitignore delete mode 100644 frontend(experimental)/newchat/README.md delete mode 100644 frontend(experimental)/newchat/app/globals.css delete mode 100644 frontend(experimental)/newchat/app/layout.tsx delete mode 100644 frontend(experimental)/newchat/app/loading.tsx delete mode 100644 frontend(experimental)/newchat/app/page.tsx delete mode 100644 frontend(experimental)/newchat/components.json delete mode 100644 frontend(experimental)/newchat/components/account-settings-dialog.tsx delete mode 100644 frontend(experimental)/newchat/components/auth-screen.tsx delete mode 100644 frontend(experimental)/newchat/components/create-event-dialog.tsx delete mode 100644 frontend(experimental)/newchat/components/event-detail-dialog.tsx delete mode 100644 frontend(experimental)/newchat/components/glass-modal.tsx delete mode 100644 frontend(experimental)/newchat/components/room-background.tsx delete mode 100644 frontend(experimental)/newchat/components/room-dialogs.tsx delete mode 100644 frontend(experimental)/newchat/components/room-settings-dialog.tsx delete mode 100644 frontend(experimental)/newchat/components/sidebar.tsx delete mode 100644 frontend(experimental)/newchat/components/theme-provider.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/accordion.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/alert-dialog.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/alert.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/aspect-ratio.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/avatar.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/badge.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/breadcrumb.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/button-group.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/button.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/calendar.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/card.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/carousel.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/chart.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/checkbox.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/collapsible.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/command.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/context-menu.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/dialog.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/drawer.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/dropdown-menu.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/empty.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/field.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/form.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/hover-card.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/input-group.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/input-otp.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/input.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/item.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/kbd.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/label.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/menubar.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/navigation-menu.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/pagination.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/popover.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/progress.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/radio-group.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/resizable.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/scroll-area.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/select.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/separator.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/sheet.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/sidebar.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/skeleton.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/slider.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/sonner.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/spinner.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/switch.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/table.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/tabs.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/textarea.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/toast.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/toaster.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/toggle-group.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/toggle.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/tooltip.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/use-mobile.tsx delete mode 100644 frontend(experimental)/newchat/components/ui/use-toast.ts delete mode 100644 frontend(experimental)/newchat/components/week-view.tsx delete mode 100644 frontend(experimental)/newchat/hooks/use-mobile.ts delete mode 100644 frontend(experimental)/newchat/hooks/use-toast.ts delete mode 100644 frontend(experimental)/newchat/lib/api.ts delete mode 100644 frontend(experimental)/newchat/lib/auth-context.tsx delete mode 100644 frontend(experimental)/newchat/lib/date-utils.ts delete mode 100644 frontend(experimental)/newchat/lib/hooks.ts delete mode 100644 frontend(experimental)/newchat/lib/room-theme.ts delete mode 100644 frontend(experimental)/newchat/lib/types.ts delete mode 100644 frontend(experimental)/newchat/lib/utils.ts delete mode 100644 frontend(experimental)/newchat/missing.md delete mode 100644 frontend(experimental)/newchat/next.config.mjs delete mode 100644 frontend(experimental)/newchat/package-lock.json delete mode 100644 frontend(experimental)/newchat/package.json delete mode 100644 frontend(experimental)/newchat/pnpm-lock.yaml delete mode 100644 frontend(experimental)/newchat/postcss.config.mjs delete mode 100644 frontend(experimental)/newchat/public/apple-icon.png delete mode 100644 frontend(experimental)/newchat/public/backgrounds/all-rooms.png delete mode 100644 frontend(experimental)/newchat/public/backgrounds/room-1.png delete mode 100644 frontend(experimental)/newchat/public/backgrounds/room-2.png delete mode 100644 frontend(experimental)/newchat/public/backgrounds/room-3.png delete mode 100644 frontend(experimental)/newchat/public/backgrounds/room-4.png delete mode 100644 frontend(experimental)/newchat/public/backgrounds/room-5.png delete mode 100644 frontend(experimental)/newchat/public/icon-dark-32x32.png delete mode 100644 frontend(experimental)/newchat/public/icon-light-32x32.png delete mode 100644 frontend(experimental)/newchat/public/icon.svg delete mode 100644 frontend(experimental)/newchat/public/placeholder-logo.png delete mode 100644 frontend(experimental)/newchat/public/placeholder-logo.svg delete mode 100644 frontend(experimental)/newchat/public/placeholder-user.jpg delete mode 100644 frontend(experimental)/newchat/public/placeholder.jpg delete mode 100644 frontend(experimental)/newchat/public/placeholder.svg delete mode 100644 frontend(experimental)/newchat/styles/globals.css delete mode 100644 frontend(experimental)/newchat/tailwind.config.js delete mode 100644 frontend(experimental)/newchat/tsconfig.json diff --git a/.gitignore b/.gitignore index 537a84e..f650315 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,27 @@ -# --- Backend (Python) --- -.env -__pycache__/ -*.py[cod] -database/calendar.db -database/calendar.db-journal -.vscode -.venv/ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# --- Frontend (Next.js / v0) --- -node_modules/ -.next/ -out/ -.vercel/ -*.tsbuildinfo -next-env.d.ts +# dependencies +/node_modules -# leftover v0/dev artifacts -dump.txt -*.old-mock -*.old-mock.tsx +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* # env files -.env*.local -.env.production -.env.development +.env* -# OS / editor -.DS_Store -Thumbs.db +# vercel +.vercel -# logs -npm-debug.log* -pnpm-debug.log* -yarn-debug.log* -yarn-error.log* \ No newline at end of file +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/backend/__pycache__/auth.cpython-313.pyc b/backend/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e006d8e51c4dd3fa10575ba873c274dc4a0cb2a6 GIT binary patch literal 7532 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 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/database.cpython-313.pyc b/backend/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f00343f3038640704b37780078dff81052491019 GIT binary patch literal 2826 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 F{s$&cs+|A; literal 0 HcmV?d00001 diff --git a/backend/__pycache__/event_signups.cpython-313.pyc b/backend/__pycache__/event_signups.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3301d1433d9fe0e5c39722756559cfcaee7480b GIT binary patch literal 1563 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 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53c32cf770a9903855c10925ae479c3831c4b27d GIT binary patch literal 33184 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 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/rooms.cpython-313.pyc b/backend/__pycache__/rooms.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce2403fc52bf56d99a1d5327bbfcbd37d3b66949 GIT binary patch literal 7046 zcmc&&-ESMm5#Rgd_m?PE~Do%=` zJ&?P5v%7P++ zHGfVxD^l^SM5VJbl@+22G@yzXgJZlZ4Uj7$4XN_Quo}1+WK>xVGHS#b6Jl>sHU?*j zYM4=CNC6%Z_7+#8>@CLL68OgYMK#VSNvA)--cr8)B%|~&N($D1TxGR~QPa+>G<)my z&FW>8K9{2Q!OCPc!>E}hE|#ko}P#8 z2^GC+@)Y;9QmHI!u*DolVHw20wJGKkqnB&T`sgLSI$G3Va7m-1m$c%~pfyTswPj=E z>f2`THLX%EX|R8v7-#|*pb8q#La|D=BGDVd+PP1Y%3mTwH&6XJGPITGwFDCV0o2=( z*oWgEjISqd?Y$d0wi!7Fw0n`n4$LHGV8zhsLb+sy@QVvoZCNj(Ox*`J=##Ne!n`YC zPc9Njm{WOG7~!8J7d$Q?L#{vy{FvZMvMTs`rGii(s_g3(fg-8_MMpRQ(ZG^)yC^##d*%;)v398mYUmmTuwoKD03CoR#2AJGCE9E8A`@D;@4fg=ED)7f zC#@b5jK2Hxtwb8EZcToFd_&CI!tlTcW_yPA0lahwgYyZ4FYt~qcv~1?Ob93JQ3W)n zG2RoMSP)(F)R+i;hKFWGF5qt2<1;`sNlBHM0m}Ktj3*;U(B+ax`Tk<&%caJmH{ap- zlt=Iwx)$a2*vIFXM%&R%>)g#=J?{Ux-O>S&Uru0p!|cTi{edbzq}+bteHvs+iqv|>@eTDR3~2s+Qf-&lp>JE&Q_ zXKn2LE32<;C%{G}(8oB5p^aJ+*!E5$l9X3Ze>FgYNzlpdWY*T%s6FPd(SF;GV-aS@ z`@V-ABLa1(uFsCaYWci22x{mF-9CNq6$QWyZUpeN3zeaF7rKY%UHW_o|1e8e$3_sM zy~&;m8XoEF?3p}Unmfbm!8Om?eFiKW^p9z%;|_o^cvYXh_>|Zk;`oU+&ArW01Uf(? zM0z`3u<<9fq}y6y&>VEVgWU-Kj#@qZyb>~?jSGIy6*5h57T!)wkp7{ ziy;^~p@TonbvSt6F+&(DsWOhp1@hm#>3LkP9X^V zS@hL}M&}U~I3Lh^Zovoee47{b$6V+nTK-kMfV#$>f)~{lgfuxX8T=^7Ax%z0G*Oj& z<0LowdyRh;9D9T)Fs>>UM3^PF&60hyEm~<-#mX~ay|3$%EoXbANjQB=$+}C$8LKlXeGW$a z4M+LhT-(lZol!Tz@W<46Obk91XnY%d!c_nUC*QR4yH1bduEtaE-4$!MXcT-p%ibIp z!yLQc#e^%mcFjrE#$mz#g1>>{1f4+Ffr*6^q_IHm%UIaiv>B}G*B{Q^z3uXI#ZCPaD)`Z@4Ah6asq_3!sGk8Nd!+7f3e>K#>|8*?}Ds30Xy z@7wC#x0N2)$~^WUB%$6)uHM4uyvNH0D?q}FtvP?MGfd?N{Ff|(>sr&~m@~qqRJoQ5 zwl8Da4pM^48BUea1w+JUw&&=mLkMO&zk=ckqQG(r1mXe6dV#I;AwuT-X6H}`eaYn< zZmHvGrJi#b=X5-W;N9K8HS&?9kXOMdBCvuba$H#Ca$G(qnE^wG8;&~M{uD3Q%0(S- zgD50bWgE%gzjc+KG;>tPHto1^ z8yTXB^T$iLC z{X9TSF#hg4cJdlbH|KACelv zJASQ;>UInBcJrb11y~p8HA8hK!}CDskfH4aoFzcUR?+E?VU&h4d>k5%KXqi7@?Bnj znQ-HXT$V;*D4oXQG!$kKe2#VrpoE!?oid_U(kHQ30Ni0Ww5Cw5*4b4nJ%GJKSYW13 zF>7SO9*bxwx_IVkm@%p*x z{ev9YBuDNgva3-`4sgkv#U_Eu>V1Nf*V7h(>Xu!7a@Y!8F# z7W6}9EpkatUNf2mDr+di9av8{2~^gRF`ySM$R6dJFE_EWs=!NLhfhLf4Mw3YKsW~d zK!VEJKZxBHf$EmZDqID;S!cPd%?2uKY(FdK1T->XY=BSTPCx E8?nl_j{pDw literal 0 HcmV?d00001 diff --git a/backend/email_service.py b/backend/email_service.py index 307f85b..50ada61 100644 --- a/backend/email_service.py +++ b/backend/email_service.py @@ -1,17 +1,19 @@ import hashlib -import secrets -import sqlite3 -from datetime import datetime, timedelta, timezone -from pathlib import Path - import os +import secrets import smtplib +import sqlite3 + +from datetime import datetime, timedelta, timezone from email.message import EmailMessage +from pathlib import Path from dotenv import load_dotenv + load_dotenv() + DB_NAME = Path(__file__).parent.parent / "database" / "calendar.db" @@ -28,7 +30,207 @@ def hash_token(token: str) -> str: ).hexdigest() -def update_email(user_id: int, new_email: str): +# ============================================================ +# EMAIL +# ============================================================ + +def send_email( + email: str, + subject: str, + plain_text: str, + html: str, +): + smtp_host = os.getenv("SMTP_HOST") + smtp_port = int(os.getenv("SMTP_PORT", "587")) + smtp_username = os.getenv("SMTP_USERNAME") + smtp_password = os.getenv("SMTP_PASSWORD") + smtp_from = os.getenv("SMTP_FROM") + + if not all([ + smtp_host, + smtp_username, + smtp_password, + smtp_from, + ]): + raise RuntimeError("SMTP configuration is incomplete") + + message = EmailMessage() + + message["From"] = smtp_from + message["To"] = email + message["Subject"] = subject + + # Plain-text fallback + message.set_content(plain_text) + + # HTML version + message.add_alternative( + html, + subtype="html", + ) + + print(f"Sending email to {email}") + + with smtplib.SMTP( + smtp_host, + smtp_port, + ) as smtp: + + print("SMTP connected") + + smtp.starttls() + + print("TLS started") + + smtp.login( + smtp_username, + smtp_password, + ) + + print("SMTP login successful") + + smtp.send_message(message) + + print("Email sent") + + +def send_verification_email( + email: str, + token: str, +): + verification_url = ( + "https://ben.de-roo.org" + f"/api/verify-email?token={token}" + ) + + plain_text = f"""Hello, + +You requested to use this email address for your Calendar account. + +Verify your email address using this link: + +{verification_url} + +This link expires after 24 hours. + +If you did not request this, you can ignore this email. +""" + + html = f"""\ + + + + + + + + +

Hello,

+ +

+ You requested to use this email address + for your Calendar account. +

+ +

+ + Verify your email address + +

+ +

+ This link expires after 24 hours. +

+ +

+ If you did not request this, + you can ignore this email. +

+ + + +""" + + send_email( + email=email, + subject="Verify your Calendar email address", + plain_text=plain_text, + html=html, + ) + + +def send_password_reset_email( + email: str, + token: str, +): + password_reset_url = ( + "https://ben.de-roo.org" + f"/api/update-password_page?token={token}" + ) + + plain_text = f"""Hello, + +You requested to reset your Calendar password. + +Reset your password using this link: + +{password_reset_url} + +This link expires after 1 hour. + +If you did not request this, you can ignore this email. +""" + + html = f"""\ + + + + + + + + +

Hello,

+ +

+ You requested to reset your Calendar password. +

+ +

+ + Reset your password + +

+ +

+ This link expires after 1 hour. +

+ +

+ If you did not request this, + you can ignore this email. +

+ + + +""" + + send_email( + email=email, + subject="Reset your password", + plain_text=plain_text, + html=html, + ) + + +# ============================================================ +# EMAIL ADDRESS VERIFICATION +# ============================================================ + +def update_email( + user_id: int, + new_email: str, +): new_email = new_email.strip().lower() conn = get_connection() @@ -57,7 +259,10 @@ def update_email(user_id: int, new_email: str): WHERE email = ? AND id != ? """, - (new_email, user_id), + ( + new_email, + user_id, + ), ) if cur.fetchone() is not None: @@ -73,7 +278,10 @@ def update_email(user_id: int, new_email: str): email_verified = 0 WHERE id = ? """, - (new_email, user_id), + ( + new_email, + user_id, + ), ) cur.execute( @@ -104,7 +312,9 @@ def update_email(user_id: int, new_email: str): conn.close() -def create_verification(user_id: int): +def create_verification( + user_id: int, +): token = secrets.token_urlsafe(32) token_hash = hash_token(token) @@ -149,7 +359,9 @@ def create_verification(user_id: int): conn.close() -def verify_email(token: str): +def verify_email( + token: str, +): token_hash = hash_token(token) conn = get_connection() @@ -181,6 +393,7 @@ def verify_email(token: str): ) if datetime.now(timezone.utc) >= expires_at: + cur.execute( """ DELETE FROM email_verifications @@ -222,121 +435,14 @@ def verify_email(token: str): finally: conn.close() -def send_verification_email(email: str, token: str): - smtp_host = os.getenv("SMTP_HOST") - smtp_port = int(os.getenv("SMTP_PORT", "587")) - smtp_username = os.getenv("SMTP_USERNAME") - smtp_password = os.getenv("SMTP_PASSWORD") - smtp_from = os.getenv("SMTP_FROM") - if not all([ - smtp_host, - smtp_username, - smtp_password, - smtp_from, - ]): - raise RuntimeError("SMTP configuration is incomplete") +# ============================================================ +# PASSWORD RESET +# ============================================================ - verification_url = ( - f"https://ben.de-roo.org/api/verify-email?token={token}" - ) - - message = EmailMessage() - - message["From"] = smtp_from - message["To"] = email - message["Subject"] = "Verify your Calendar email address" - - message.set_content( - f"""Hello, - -You requested to use this email address for your Calendar account. - -Verify your email address using this link: - -{verification_url} - -This link expires after 24 hours. - -If you did not request this, you can ignore this email. -""" - ) - - print(f"Sending verification email to {email}") - - with smtplib.SMTP(smtp_host, smtp_port) as smtp: - print("SMTP connected") - - smtp.starttls() - print("TLS started") - - smtp.login( - smtp_username, - smtp_password, - ) - print("SMTP login successful") - - smtp.send_message(message) - print("Verification email sent") - -def send_password_reset_email(email: str, token: str): - smtp_host = os.getenv("SMTP_HOST") - smtp_port = int(os.getenv("SMTP_PORT", "587")) - smtp_username = os.getenv("SMTP_USERNAME") - smtp_password = os.getenv("SMTP_PASSWORD") - smtp_from = os.getenv("SMTP_FROM") - - if not all([ - smtp_host, - smtp_username, - smtp_password, - smtp_from, - ]): - raise RuntimeError("SMTP configuration is incomplete") - - password_reset_url = ( - f"https://ben.de-roo.org/api/update-password_page?token={token}" - ) - - message = EmailMessage() - - message["From"] = smtp_from - message["To"] = email - message["Subject"] = "Reset your password" - - message.set_content( - f"""Hello, - -You requested to use this email address for reseting your Calendar password. - -Reset your password using this link: - -{password_reset_url} - -This link expires after 24 hours. - -If you did not request this, you can ignore this email. -""" - ) - - print(f"Sending password reset email to {email}") - - with smtplib.SMTP(smtp_host, smtp_port) as smtp: - print("SMTP connected") - - smtp.starttls() - print("TLS started") - - smtp.login( - smtp_username, - smtp_password, - ) - print("SMTP login successful") - - smtp.send_message(message) - print("Password reset email sent") - -def create_password_reset(user_id: int): +def create_password_reset( + user_id: int, +): token = secrets.token_urlsafe(32) token_hash = hash_token(token) @@ -378,4 +484,4 @@ def create_password_reset(user_id: int): return token finally: - conn.close() \ No newline at end of file + conn.close() diff --git a/frontend(experimental)/newchat/.gitignore b/frontend(experimental)/newchat/.gitignore deleted file mode 100644 index f650315..0000000 --- a/frontend(experimental)/newchat/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules - -# next.js -/.next/ -/out/ - -# production -/build - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file diff --git a/frontend(experimental)/newchat/README.md b/frontend(experimental)/newchat/README.md deleted file mode 100644 index 14a4cd3..0000000 --- a/frontend(experimental)/newchat/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# calendar-app - -This is a [Next.js](https://nextjs.org) project bootstrapped with [v0](https://v0.app). - -## Built with v0 - -This repository is linked to a [v0](https://v0.app) project. You can continue developing by visiting the link below -- start new chats to make changes, and v0 will push commits directly to this repo. Every merge to `main` will automatically deploy. - -[Continue working on v0 →](https://v0.app/chat/projects/prj_JicVuGp27VUV82GWT4csmGqXBtSV) - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -## Learn More - -To learn more, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -- [v0 Documentation](https://v0.app/docs) - learn about v0 and how to use it. diff --git a/frontend(experimental)/newchat/app/globals.css b/frontend(experimental)/newchat/app/globals.css deleted file mode 100644 index 5791b0a..0000000 --- a/frontend(experimental)/newchat/app/globals.css +++ /dev/null @@ -1,62 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --radius: 0.5rem; -} - -.dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; -} - -@layer utilities { - .bg-grid-slate-200\/50 { - background-image: linear-gradient(currentColor 1px, transparent 1px), - linear-gradient(to right, currentColor 1px, transparent 1px); - background-size: 20px 20px; - } -} - -.react-draggable { - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); -} - diff --git a/frontend(experimental)/newchat/app/layout.tsx b/frontend(experimental)/newchat/app/layout.tsx deleted file mode 100644 index 95ec418..0000000 --- a/frontend(experimental)/newchat/app/layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type React from "react" -import type { Metadata } from "next" -import { Inter } from "next/font/google" -import "./globals.css" -import { AuthProvider } from "@/lib/auth-context" - -const inter = Inter({ subsets: ["latin"] }) - -export const metadata: Metadata = { - title: "Calendar | Rooms & Events", - description: "A shared, room-based calendar for planning events together.", - generator: "v0.app", -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - - {children} - - - ) -} diff --git a/frontend(experimental)/newchat/app/loading.tsx b/frontend(experimental)/newchat/app/loading.tsx deleted file mode 100644 index cec067b..0000000 --- a/frontend(experimental)/newchat/app/loading.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export default function Loading() { - return null -} - diff --git a/frontend(experimental)/newchat/app/page.tsx b/frontend(experimental)/newchat/app/page.tsx deleted file mode 100644 index 9549c74..0000000 --- a/frontend(experimental)/newchat/app/page.tsx +++ /dev/null @@ -1,239 +0,0 @@ -"use client" - -import { useMemo, useState } from "react" -import { Menu, Plus, Loader2 } from "lucide-react" - -import { useAuth } from "@/lib/auth-context" -import { useRooms, useEvents, useMyResponses } from "@/lib/hooks" -import { AuthScreen } from "@/components/auth-screen" -import { Sidebar } from "@/components/sidebar" -import { WeekView } from "@/components/week-view" -import { CreateEventDialog } from "@/components/create-event-dialog" -import { EventDetailDialog } from "@/components/event-detail-dialog" -import { CreateRoomDialog, JoinRoomDialog } from "@/components/room-dialogs" -import { RoomSettingsDialog } from "@/components/room-settings-dialog" -import { AccountSettingsDialog } from "@/components/account-settings-dialog" -import { RoomBackground } from "@/components/room-background" -import { getRoomBackground } from "@/lib/room-theme" -import { addDays, startOfWeek, toDateParam, formatMonthYear } from "@/lib/date-utils" -import type { CalendarEvent, User } from "@/lib/types" - -type DialogState = - | { type: "none" } - | { type: "create-event" } - | { type: "edit-event"; event: CalendarEvent } - | { type: "event-detail"; event: CalendarEvent } - | { type: "create-room" } - | { type: "join-room" } - | { type: "room-settings" } - | { type: "account-settings" } - -export default function Home() { - const { user, loading, logout } = useAuth() - - if (loading) { - return ( -
- -
- ) - } - - if (!user) { - return - } - - return -} - -function CalendarApp({ user, onLogout }: { user: User; onLogout: () => void }) { - const [activeRoomId, setActiveRoomId] = useState(null) - const [selectedDate, setSelectedDate] = useState(() => new Date()) - const [sidebarOpen, setSidebarOpen] = useState(false) - const [dialog, setDialog] = useState({ type: "none" }) - - const { rooms, mutate: mutateRooms } = useRooms() - - const weekStart = useMemo(() => startOfWeek(selectedDate), [selectedDate]) - const weekEnd = useMemo(() => addDays(weekStart, 7), [weekStart]) - const from = useMemo(() => toDateParam(weekStart), [weekStart]) - const to = useMemo(() => toDateParam(weekEnd), [weekEnd]) - - const { - events, - isLoading: eventsLoading, - mutate: mutateEvents, - } = useEvents(activeRoomId, from, to) - - const { myResponses } = useMyResponses(events, user.id) - - const activeRoom = rooms.find((r) => r.id === activeRoomId) - const background = getRoomBackground(activeRoomId) - - const refreshAll = () => { - mutateEvents() - mutateRooms() - } - - return ( -
- -
- -
- {/* Mobile sidebar backdrop */} - {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - -
- { - setActiveRoomId(id) - setSidebarOpen(false) - }} - selectedDate={selectedDate} - onSelectDate={setSelectedDate} - onCreateEvent={() => setDialog({ type: "create-event" })} - onCreateRoom={() => setDialog({ type: "create-room" })} - onJoinRoom={() => setDialog({ type: "join-room" })} - user={user} - onLogout={onLogout} - onOpenAccountSettings={() => setDialog({ type: "account-settings" })} - /> -
- -
-
-
- -
-

- {activeRoom ? activeRoom.name : "All Rooms"} -

-

{formatMonthYear(weekStart)}

-
-
- -
- {activeRoom && ( - - )} - -
-
- -
- {eventsLoading ? ( -
- -
- ) : ( - setDialog({ type: "event-detail", event })} - /> - )} -
-
-
- - {(dialog.type === "create-event" || dialog.type === "edit-event") && ( - setDialog({ type: "none" })} - onCreated={() => { - setDialog({ type: "none" }) - refreshAll() - }} - /> - )} - - {dialog.type === "event-detail" && ( - r.id === dialog.event.room_id)} - currentUserId={user.id} - onClose={() => setDialog({ type: "none" })} - onChanged={refreshAll} - onEdit={() => setDialog({ type: "edit-event", event: dialog.event })} - /> - )} - - {dialog.type === "create-room" && ( - setDialog({ type: "none" })} - onCreated={(roomId: number) => { - setDialog({ type: "none" }) - mutateRooms() - setActiveRoomId(roomId) - }} - /> - )} - - {dialog.type === "join-room" && ( - setDialog({ type: "none" })} - onJoined={(roomId: number) => { - setDialog({ type: "none" }) - mutateRooms() - setActiveRoomId(roomId) - }} - /> - )} - - {dialog.type === "room-settings" && activeRoom && ( - setDialog({ type: "none" })} - onChanged={mutateRooms} - onLeftOrDeleted={() => { - setDialog({ type: "none" }) - setActiveRoomId(null) - mutateRooms() - }} - /> - )} - - {dialog.type === "account-settings" && ( - setDialog({ type: "none" })} - onLogout={onLogout} - /> - )} -
- ) -} \ No newline at end of file diff --git a/frontend(experimental)/newchat/components.json b/frontend(experimental)/newchat/components.json deleted file mode 100644 index 4ee62ee..0000000 --- a/frontend(experimental)/newchat/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/frontend(experimental)/newchat/components/account-settings-dialog.tsx b/frontend(experimental)/newchat/components/account-settings-dialog.tsx deleted file mode 100644 index 053309c..0000000 --- a/frontend(experimental)/newchat/components/account-settings-dialog.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client" - -import { LogOut, Mail, MailCheck, MailWarning, UserRound } from "lucide-react" -import { GlassModal, fieldClass, labelClass } from "./glass-modal" -import type { User } from "@/lib/types" - -interface AccountSettingsDialogProps { - user: User - onClose: () => void - onLogout: () => void -} - -export function AccountSettingsDialog({ user, onClose, onLogout }: AccountSettingsDialogProps) { - return ( - -
-
-
- {user.username.charAt(0).toUpperCase()} -
-
-

{user.username}

-

{user.email ?? "No email on file"}

-
-
- -
- -
- - {user.username} -
-
- -
- -
- - {user.email ?? "—"} -
- {user.email && ( -

- {user.email_verified ? ( - <> - Verified - - ) : ( - <> - Not verified - - )} -

- )} -
- -
- - - -
-
-
- ) -} diff --git a/frontend(experimental)/newchat/components/auth-screen.tsx b/frontend(experimental)/newchat/components/auth-screen.tsx deleted file mode 100644 index 3bcc92c..0000000 --- a/frontend(experimental)/newchat/components/auth-screen.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client" - -import type React from "react" -import { useState } from "react" -import Image from "next/image" -import { Calendar, Loader2 } from "lucide-react" -import { useAuth } from "@/lib/auth-context" -import { ApiError } from "@/lib/api" -import { ALL_ROOMS_BACKGROUND } from "@/lib/room-theme" - -type Mode = "login" | "register" - -export function AuthScreen() { - const { login, register } = useAuth() - const [mode, setMode] = useState("login") - const [username, setUsername] = useState("") - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [error, setError] = useState(null) - const [notice, setNotice] = useState(null) - const [submitting, setSubmitting] = useState(false) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError(null) - setNotice(null) - setSubmitting(true) - try { - if (mode === "login") { - await login(username.trim(), password) - } else { - await register(username.trim(), password, email.trim()) - } - } catch (err) { - const message = - err instanceof ApiError ? err.message : "Something went wrong. Please try again." - setError(message) - } finally { - setSubmitting(false) - } - } - - const switchMode = (next: Mode) => { - setMode(next) - setError(null) - setNotice(null) - } - - return ( -
- Aurora over a mountain lake -
- -
-
-
- -
-

- {mode === "login" ? "Welcome back" : "Create your account"} -

-

- {mode === "login" - ? "Sign in to your shared calendar" - : "Join rooms and plan events together"} -

-
- -
-
- - setUsername(e.target.value)} - className="rounded-xl border border-white/20 bg-white/10 px-4 py-2.5 text-white placeholder:text-white/50 focus:outline-none focus:ring-2 focus:ring-white/40" - placeholder="your_username" - /> -
- - {mode === "register" && ( -
- - setEmail(e.target.value)} - className="rounded-xl border border-white/20 bg-white/10 px-4 py-2.5 text-white placeholder:text-white/50 focus:outline-none focus:ring-2 focus:ring-white/40" - placeholder="you@example.com" - /> -
- )} - -
- - setPassword(e.target.value)} - className="rounded-xl border border-white/20 bg-white/10 px-4 py-2.5 text-white placeholder:text-white/50 focus:outline-none focus:ring-2 focus:ring-white/40" - placeholder={mode === "register" ? "At least 8 characters" : "••••••••"} - /> -
- - {error && ( -
- {error} -
- )} - {notice && ( -
- {notice} -
- )} - - -
- -

- {mode === "login" ? "Don't have an account? " : "Already have an account? "} - -

-
-
- ) -} diff --git a/frontend(experimental)/newchat/components/create-event-dialog.tsx b/frontend(experimental)/newchat/components/create-event-dialog.tsx deleted file mode 100644 index 214f9ee..0000000 --- a/frontend(experimental)/newchat/components/create-event-dialog.tsx +++ /dev/null @@ -1,354 +0,0 @@ -"use client" - -import type React from "react" -import { useState } from "react" -import { Loader2 } from "lucide-react" -import { GlassModal, fieldClass, labelClass } from "./glass-modal" -import * as api from "@/lib/api" -import { ApiError } from "@/lib/api" -import type { CalendarEvent, EventType, EventVisibility, Room } from "@/lib/types" -import { parseDateTime, toBackendDateTime, toDateParam, toInputTime } from "@/lib/date-utils" - -interface CreateEventDialogProps { - rooms: Room[] - defaultRoomId: number | null - defaultDate: Date - /** When set, the dialog edits this event instead of creating a new one. */ - editingEvent?: CalendarEvent - onClose: () => void - onCreated: () => void -} - -const TYPE_OPTIONS: { value: EventType; label: string }[] = [ - { value: "proposal", label: "Proposal (members vote to confirm)" }, - { value: "busy", label: "Busy — room sees it as busy, no details" }, - { value: "private", label: "Private — hidden from the room" }, -] - -const VISIBILITY_OPTIONS: { value: EventVisibility; label: string }[] = [ - { value: "room", label: "Room — visible to all members" }, - { value: "busy_only", label: "Busy only — hide details" }, - { value: "private", label: "Private — only me" }, -] - -const optionClass = "bg-slate-800 text-white" - -export function CreateEventDialog({ - rooms, - defaultRoomId, - defaultDate, - editingEvent, - onClose, - onCreated, -}: CreateEventDialogProps) { - const isEditing = !!editingEvent - const initialRoom = editingEvent?.room_id ?? defaultRoomId ?? rooms[0]?.id ?? 0 - - // Start time defaults to "now", rounded up to the next quarter hour. - const now = new Date() - const defaultStartTime = new Date(now) - defaultStartTime.setMinutes(Math.ceil(now.getMinutes() / 15) * 15, 0, 0) - const defaultEndTime = new Date(defaultStartTime) - defaultEndTime.setHours(defaultEndTime.getHours() + 1) - - const editStart = editingEvent ? parseDateTime(editingEvent.start_time) : null - const editEnd = editingEvent ? parseDateTime(editingEvent.end_time) : null - - const [roomId, setRoomId] = useState(initialRoom) - const [title, setTitle] = useState(editingEvent?.title ?? "") - const [description, setDescription] = useState(editingEvent?.description ?? "") - const [type, setType] = useState(editingEvent?.type ?? "proposal") - const [visibility, setVisibility] = useState(editingEvent?.visibility ?? "room") - const [date, setDate] = useState(toDateParam(editStart ?? defaultDate)) - const [multiDay, setMultiDay] = useState( - editStart && editEnd ? toDateParam(editStart) !== toDateParam(editEnd) : false, - ) - const [endDate, setEndDate] = useState(toDateParam(editEnd ?? editStart ?? defaultDate)) - const [startTime, setStartTime] = useState(toInputTime(editStart ?? defaultStartTime)) - const [endTime, setEndTime] = useState(toInputTime(editEnd ?? defaultEndTime)) - const [minPeople, setMinPeople] = useState(editingEvent?.min_people ?? 1) - const [error, setError] = useState(null) - const [submitting, setSubmitting] = useState(false) - - // Busy/private events are always visible to the room as just "busy" (no - // details) or not visible at all — that's implied by the type, so the - // visibility choice only makes sense for proposals. - const handleTypeChange = (value: EventType) => { - setType(value) - if (value === "busy") setVisibility("busy_only") - else if (value === "private") setVisibility("private") - else setVisibility("room") - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError(null) - - if (!roomId) { - setError("Please select a room.") - return - } - const startDate = new Date(`${date}T${startTime}`) - const endDate2 = new Date(`${multiDay ? endDate : date}T${endTime}`) - if (endDate2 <= startDate) { - setError("End time must be after the start time.") - return - } - - const payload = { - room_id: roomId, - type, - title: title.trim(), - description: description.trim(), - start_time: toBackendDateTime(startDate), - end_time: toBackendDateTime(endDate2), - visibility: type === "proposal" ? visibility : type === "busy" ? "busy_only" : "private", - min_people: type === "proposal" ? minPeople : 0, - } as const - - setSubmitting(true) - try { - if (isEditing && editingEvent) { - await api.updateEvent(editingEvent.id, payload) - } else { - await api.createEvent({ ...payload, status: type === "proposal" ? "proposed" : "confirmed" }) - } - onCreated() - onClose() - } catch (err) { - setError( - err instanceof ApiError - ? err.message - : isEditing - ? "Could not save the changes." - : "Could not create the event.", - ) - } finally { - setSubmitting(false) - } - } - - const dialogTitle = isEditing ? "Edit event" : "Create event" - - if (rooms.length === 0) { - return ( - -

- You need to be in a room before creating an event. Create or join a room first. -

-
- ) - } - - return ( - -
-
- - -
- -
- - setTitle(e.target.value)} - className={fieldClass} - placeholder="e.g. Team dinner" - /> -
- -
- -