diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1ad7c38 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# Server +LISTEN_ADDR=:8080 +SESSION_SECRET=change-me-to-a-long-random-secret-at-least-32-chars +DATABASE_PATH=data/migaduadmin.db +COOKIE_SECURE=false + +# Migadu API (https://www.migadu.com/api/) +# Username is your Migadu account email; password is the API key ("app-key"). +# `make dev` creates .env automatically; replace these before loading domains. +MIGADU_USER=you@example.com +MIGADU_API_KEY=your-migadu-api-key +MIGADU_BASE_URL=https://api.migadu.com/v1 + +# Auth: local | pangolin | both +AUTH_MODE=both + +# Comma-separated CIDRs allowed to supply Pangolin SSO headers +# (Remote-Email, Remote-User, Remote-Name, Remote-Role). +TRUSTED_PROXIES=127.0.0.1/32,::1/128 + +# After the first admin exists, registration stays closed unless true. +REGISTRATION_OPEN=false + +# When true, unknown Pangolin SSO emails are auto-created as role=user. +# First-ever user is always admin regardless of this setting. +SSO_AUTO_PROVISION=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d9dd28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Binaries / local data +/bin/ +/data/ +*.db +*.exe +/migaduadmin +/tmp/ + +# Env +.env +.env.local + +# Frontend +web/node_modules/ +web/dist/ + +# Editor / OS +.DS_Store +.idea/ +.vscode/ +*.swp + +# Go +vendor/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..79fb7ff --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +.PHONY: setup-dev dev build frontend backend run tidy clean + +export PATH := $(HOME)/.local/go/bin:$(HOME)/.local/bin:$(PATH) + +setup-dev: + @bash scripts/setup-dev.sh + +dev: setup-dev + @echo "API → http://127.0.0.1:8080" + @echo "UI → http://127.0.0.1:5173 (live reload; proxies /api → Go)" + @echo "Open the UI URL in your browser. Ctrl+C stops both." + @bash -c 'set -euo pipefail; \ + go run ./cmd/server & \ + api_pid=$$!; \ + (cd web && npm run dev -- --host 127.0.0.1 --port 5173) & \ + ui_pid=$$!; \ + trap "kill $$api_pid $$ui_pid 2>/dev/null; wait $$api_pid $$ui_pid 2>/dev/null" EXIT INT TERM; \ + wait' + +frontend: + cd web && npm install && npm run build + rm -rf cmd/server/ui + mkdir -p cmd/server/ui + cp -R web/dist/. cmd/server/ui/ + +backend: + go build -o bin/migaduadmin ./cmd/server + +build: frontend backend + +run: build + ./bin/migaduadmin + +tidy: + go mod tidy + cd web && npm install + +clean: + rm -rf bin web/dist cmd/server/ui + mkdir -p cmd/server/ui + printf '%s\n' '

UI not built. Run make frontend.

' > cmd/server/ui/index.html diff --git a/README.md b/README.md index 3da05cb..fb4f778 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,95 @@ # MigaduAdmin -Administrate settings for several domains in one place. \ No newline at end of file +Administrate domain-level Migadu settings for multiple domains in one place: +sender denylist (blacklist), sender allowlist (whitelist), recipient denylist, +and spam controls. + +The Go backend holds your Migadu credentials and talks to the +[Migadu JSON API](https://www.migadu.com/api/). The React UI only talks to this +application. + +## Features + +- Multi-domain list and detail views +- Shared denylist / allowlist / recipient deny lists with manual apply to all or selected domains +- Edit per-domain denylist / allowlist / recipient deny lists +- Edit domain spam aggressiveness and related toggles +- First registered local account becomes admin +- Optional [Pangolin](https://docs.pangolin.net/manage/access-control/forwarded-headers) + SSO via `Remote-Email` (and related) headers from trusted proxies + +## Requirements + +- Go 1.25+ (or Go toolchain that can download it) +- Node.js 20+ (for building the UI) + +## Quick start + +```bash +make dev +``` + +On first run this will: +1. Create `.env` from `.env.example` with a generated `SESSION_SECRET` +2. Install frontend dependencies +3. Start the Go API and Vite UI together + +Then open **http://127.0.0.1:5173**, create the first admin account, and edit +`.env` with your real `MIGADU_USER` / `MIGADU_API_KEY` before managing domains. + +Production-style single binary: + +```bash +# edit .env first +make build +make run +``` + +Open http://127.0.0.1:8080. + +## Configuration + +| Variable | Description | +| --- | --- | +| `MIGADU_USER` | Migadu account email (Basic Auth username) | +| `MIGADU_API_KEY` | Migadu API key | +| `MIGADU_BASE_URL` | Default `https://api.migadu.com/v1` | +| `LISTEN_ADDR` | Default `:8080` | +| `SESSION_SECRET` | Cookie signing secret (min 32 characters) | +| `DATABASE_PATH` | SQLite path for users/sessions | +| `AUTH_MODE` | `local`, `pangolin`, or `both` | +| `TRUSTED_PROXIES` | CIDRs allowed to set SSO headers | +| `REGISTRATION_OPEN` | Allow new local signups after bootstrap | +| `SSO_AUTO_PROVISION` | Auto-create users from trusted Pangolin emails | +| `COOKIE_SECURE` | Set `true` behind HTTPS | + +Create a Migadu API key under **My Account → API Keys** in the Migadu admin UI. + +## Pangolin SSO + +When `AUTH_MODE` is `pangolin` or `both`, and the request comes from an IP in +`TRUSTED_PROXIES`, the backend trusts: + +- `Remote-Email` (required) +- `Remote-User`, `Remote-Name`, `Remote-Role` (optional) + +Identity headers from untrusted peers are stripped. Put Pangolin / Badger in +front of this app and add the proxy address to `TRUSTED_PROXIES`. + +## API overview + +| Method | Path | Notes | +| --- | --- | --- | +| GET | `/api/auth/status` | Registration open + auth mode | +| POST | `/api/auth/register` | Bootstrap / open registration | +| POST | `/api/auth/login` | Local login | +| POST | `/api/auth/logout` | Clear session | +| GET | `/api/auth/me` | Current user | +| GET | `/api/domains` | List domains (admin) | +| GET | `/api/domains/{domain}` | Domain settings (admin) | +| PATCH | `/api/domains/{domain}` | Update lists/spam fields (admin) | +| GET/POST | `/api/users` | List / create users (admin) | + +## License + +See [LICENSE](LICENSE). diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..ea71d8d --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "embed" + "io/fs" + "log" + "net/http" + "strings" + + "github.com/squid/MigaduAdmin/internal/api" + "github.com/squid/MigaduAdmin/internal/auth" + "github.com/squid/MigaduAdmin/internal/config" + "github.com/squid/MigaduAdmin/internal/migadu" + "github.com/squid/MigaduAdmin/internal/store" +) + +//go:embed all:ui +var uiEmbed embed.FS + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("config: %v", err) + } + + st, err := store.Open(cfg.DatabasePath) + if err != nil { + log.Fatalf("database: %v", err) + } + defer st.Close() + + authService := auth.NewService(st, cfg) + migaduClient := migadu.NewClient(cfg.MigaduBaseURL, cfg.MigaduUser, cfg.MigaduAPIKey) + server := api.New(cfg, authService, st, migaduClient) + + staticHandler, err := spaHandler() + if err != nil { + log.Fatalf("ui assets: %v", err) + } + + handler := server.Router(staticHandler) + log.Printf("MigaduAdmin listening on %s", cfg.ListenAddr) + log.Printf( + "Migadu credentials: user=%q api_key=%s base=%s", + cfg.MigaduUser, + cfg.MaskedMigaduKey(), + cfg.MigaduBaseURL, + ) + if err := http.ListenAndServe(cfg.ListenAddr, handler); err != nil { + log.Fatalf("server: %v", err) + } +} + +func spaHandler() (http.Handler, error) { + sub, err := fs.Sub(uiEmbed, "ui") + if err != nil { + return nil, err + } + + fileServer := http.FileServer(http.FS(sub)) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/") + if path == "" { + path = "index.html" + } + if _, err := fs.Stat(sub, path); err == nil && !strings.HasSuffix(path, "/") { + fileServer.ServeHTTP(w, r) + return + } + // SPA fallback for client-side routes + index, err := fs.ReadFile(sub, "index.html") + if err != nil { + http.Error(w, "ui not available", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write(index) + }), nil +} diff --git a/cmd/server/ui/assets/index-BWmBGRQb.js b/cmd/server/ui/assets/index-BWmBGRQb.js new file mode 100644 index 0000000..634d0d1 --- /dev/null +++ b/cmd/server/ui/assets/index-BWmBGRQb.js @@ -0,0 +1,11 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var ee=Array.isArray;function S(){}var C={H:null,A:null,T:null,S:null},te=Object.prototype.hasOwnProperty;function ne(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function re(e,t){return ne(e.type,t,e.props)}function w(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function T(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ie=/\/+/g;function ae(e,t){return typeof e==`object`&&e&&e.key!=null?T(``+e.key):t.toString(36)}function oe(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(S,S):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function se(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,se(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ae(e,0):a,ee(o)?(i=``,c!=null&&(i=c.replace(ie,`$&/`)+`/`),se(o,r,i,``,function(e){return e})):o!=null&&(w(o)&&(o=re(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ie,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(ee(e))for(var u=0;u{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,ee||(ee=!0,w());else{var t=n(l);t!==null&&ae(x,t.startTime-e)}}var ee=!1,S=-1,C=5,te=-1;function ne(){return g?!0:!(e.unstable_now()-tet&&ne());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&ae(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?w():ee=!1}}}var w;if(typeof y==`function`)w=function(){y(re)};else if(typeof MessageChannel<`u`){var T=new MessageChannel,ie=T.port2;T.port1.onmessage=re,w=function(){ie.postMessage(null)}}else w=function(){_(re,0)};function ae(t,n){S=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(S),S=-1):h=!0,ae(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,ee||(ee=!0,w()))),r},e.unstable_shouldYield=ne,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1fe||(e.current=de[fe],de[fe]=null,fe--)}function k(e,t){fe++,de[fe]=e.current,e.current=t}var me=pe(null),he=pe(null),ge=pe(null),_e=pe(null);function ve(e,t){switch(k(ge,t),k(he,e),k(me,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}O(me),k(me,e)}function ye(){O(me),O(he),O(ge)}function be(e){e.memoizedState!==null&&k(_e,e);var t=me.current,n=Hd(t,e.type);t!==n&&(k(he,e),k(me,n))}function xe(e){he.current===e&&(O(me),O(he)),_e.current===e&&(O(_e),Qf._currentValue=ue)}var Se,Ce;function we(e){if(Se===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);Se=t&&t[1]||``,Ce=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Te=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?we(n):``}function De(e,t){switch(e.tag){case 26:case 27:case 5:return we(e.type);case 16:return we(`Lazy`);case 13:return e.child!==t&&t!==null?we(`Suspense Fallback`):we(`Suspense`);case 19:return we(`SuspenseList`);case 0:case 15:return Ee(e.type,!1);case 11:return Ee(e.type.render,!1);case 1:return Ee(e.type,!0);case 31:return we(`Activity`);default:return``}}function Oe(e){try{var t=``,n=null;do t+=De(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var ke=Object.prototype.hasOwnProperty,Ae=t.unstable_scheduleCallback,je=t.unstable_cancelCallback,Me=t.unstable_shouldYield,Ne=t.unstable_requestPaint,Pe=t.unstable_now,Fe=t.unstable_getCurrentPriorityLevel,Ie=t.unstable_ImmediatePriority,Le=t.unstable_UserBlockingPriority,Re=t.unstable_NormalPriority,ze=t.unstable_LowPriority,Be=t.unstable_IdlePriority,Ve=t.log,He=t.unstable_setDisableYieldValue,Ue=null,We=null;function Ge(e){if(typeof Ve==`function`&&He(e),We&&typeof We.setStrictMode==`function`)try{We.setStrictMode(Ue,e)}catch{}}var Ke=Math.clz32?Math.clz32:Ye,qe=Math.log,Je=Math.LN2;function Ye(e){return e>>>=0,e===0?32:31-(qe(e)/Je|0)|0}var Xe=256,Ze=262144,Qe=4194304;function $e(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function et(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=$e(n))):i=$e(o):i=$e(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=$e(n))):i=$e(o)):i=$e(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function tt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function nt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function rt(){var e=Qe;return Qe<<=1,!(Qe&62914560)&&(Qe=4194304),e}function it(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function at(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ot(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),vn=!1;if(_n)try{var yn={};Object.defineProperty(yn,"passive",{get:function(){vn=!0}}),window.addEventListener(`test`,yn,yn),window.removeEventListener(`test`,yn,yn)}catch{vn=!1}var bn=null,xn=null,Sn=null;function Cn(){if(Sn)return Sn;var e,t=xn,n=t.length,r,i=`value`in bn?bn.value:bn.textContent,a=i.length;for(e=0;e=tr),j=` `,ir=!1;function ar(e,t){switch(e){case`keyup`:return $n.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function or(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var sr=!1;function cr(e,t){switch(e){case`compositionend`:return or(t);case`keypress`:return t.which===32?(ir=!0,j):null;case`textInput`:return e=t.data,e===j&&ir?null:e;default:return null}}function lr(e,t){if(sr)return e===`compositionend`||!er&&ar(e,t)?(e=Cn(),Sn=xn=bn=null,sr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=jr(n)}}function Nr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Nr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Pr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Wt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Wt(e.document)}return t}function Fr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Ir=_n&&`documentMode`in document&&11>=document.documentMode,Lr=null,Rr=null,zr=null,Br=!1;function Vr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Br||Lr==null||Lr!==Wt(r)||(r=Lr,`selectionStart`in r&&Fr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),zr&&Ar(zr,r)||(zr=r,r=Ed(Rr,`onSelect`),0>=o,i-=o,Ni=1<<32-Ke(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),N&&Fi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),N&&Fi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return N&&Fi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),N&&Fi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===w&&Na(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ba(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=bi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=yi(o.type,o.key,o.props,null,e.mode,c),Ba(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=Ci(o,e.mode,c),c.return=e,e=c}return s(e);case w:return o=Na(o),b(e,r,o,c)}if(le(o))return h(e,r,o,c);if(oe(o)){if(l=oe(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,za(o),c);if(o.$$typeof===S)return b(e,r,ca(e,o),c);Va(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=xi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ra=0;var i=b(e,t,n,r);return La=null,i}catch(t){if(t===Da||t===ka)throw t;var a=hi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ua=Ha(!0),Wa=Ha(!1),Ga=!1;function Ka(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function qa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ja(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ya(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=fi(e),di(e,null,n),t}return ci(e,r,t,n),fi(e)}function Xa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ct(e,n)}}function Za(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Qa=!1;function $a(){if(Qa){var e=va;if(e!==null)throw e}}function eo(e,t,n,r){Qa=!1;var i=e.updateQueue;Ga=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(J&f)===f:(r&f)===f){f!==0&&f===_a&&(Qa=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ga=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function to(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function no(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=E.T,s={};E.T=s,Ls(e,!1,t,n);try{var c=i(),l=E.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Is(e,t,xa(c,r),pu(e)):Is(e,t,r,pu(e))}catch(n){Is(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{D.p=a,o!==null&&s.types!==null&&(o.types=s.types),E.T=o}}function Es(){}function Ds(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Os(e).queue;Ts(e,a,t,ue,n===null?Es:function(){return ks(e),n(r)})}function Os(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:ue},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function ks(e){var t=Os(e);t.next===null&&(t=e.alternate.memoizedState),Is(e,t.next.queue,{},pu())}function As(){return sa(Qf)}function js(){return B().memoizedState}function Ms(){return B().memoizedState}function Ns(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ja(n);var r=Ya(t,e,n);r!==null&&(hu(r,t,n),Xa(r,t,n)),t={cache:pa()},e.payload=t;return}t=t.return}}function Ps(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Rs(e)?zs(t,n):(n=li(e,t,n,r),n!==null&&(hu(n,e,r),Bs(n,t,r)))}function Fs(e,t,n){Is(e,t,n,pu())}function Is(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Rs(e))zs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,kr(s,o))return ci(e,t,i,0),K===null&&si(),!1}catch{}if(n=li(e,t,i,r),n!==null)return hu(n,e,r),Bs(n,t,r),!0}return!1}function Ls(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Rs(e)){if(t)throw Error(i(479))}else t=li(e,n,r,2),t!==null&&hu(t,e,2)}function Rs(e){var t=e.alternate;return e===I||t!==null&&t===I}function zs(e,t){yo=vo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Bs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ct(e,n)}}var Vs={readContext:sa,use:Io,useCallback:z,useContext:z,useEffect:z,useImperativeHandle:z,useLayoutEffect:z,useInsertionEffect:z,useMemo:z,useReducer:z,useRef:z,useState:z,useDebugValue:z,useDeferredValue:z,useTransition:z,useSyncExternalStore:z,useId:z,useHostTransitionStatus:z,useFormState:z,useActionState:z,useOptimistic:z,useMemoCache:z,useCacheRefresh:z};Vs.useEffectEvent=z;var Hs={readContext:sa,use:Io,useCallback:function(e,t){return No().memoizedState=[e,t===void 0?null:t],e},useContext:sa,useEffect:fs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),us(4194308,4,vs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return us(4194308,4,e,t)},useInsertionEffect:function(e,t){us(4,2,e,t)},useMemo:function(e,t){var n=No();t=t===void 0?null:t;var r=e();if(bo){Ge(!0);try{e()}finally{Ge(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=No();if(n!==void 0){var i=n(t);if(bo){Ge(!0);try{n(t)}finally{Ge(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ps.bind(null,I,e),[r.memoizedState,e]},useRef:function(e){var t=No();return e={current:e},t.memoizedState=e},useState:function(e){e=Jo(e);var t=e.queue,n=Fs.bind(null,I,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:bs,useDeferredValue:function(e,t){return Cs(No(),e,t)},useTransition:function(){var e=Jo(!1);return e=Ts.bind(null,I,e.queue,!0,!1),No().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=I,a=No();if(N){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),K===null)throw Error(i(349));J&127||Uo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,fs(Go.bind(null,r,o,e),[e]),r.flags|=2048,cs(9,{destroy:void 0},Wo.bind(null,r,o,n,t),null),n},useId:function(){var e=No(),t=K.identifierPrefix;if(N){var n=Pi,r=Ni;n=(r&~(1<<32-Ke(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=xo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[mt]=t,o[ht]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Fc(t)}}return H(t),Ic(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Fc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=ge.current,qi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Bi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[mt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Wi(t,!0)}else e=Bd(e).createTextNode(r),e[mt]=t,t.stateNode=e}return H(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=qi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[mt]=t}else Ji(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;H(t),e=!1}else n=Yi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(ho(t),t):(ho(t),null);if(t.flags&128)throw Error(i(558))}return H(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=qi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[mt]=t}else Ji(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;H(t),a=!1}else a=Yi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(ho(t),t):(ho(t),null)}return ho(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Rc(t,t.updateQueue),H(t),null);case 4:return ye(),e===null&&Sd(t.stateNode.containerInfo),H(t),null;case 10:return ta(t.type),H(t),null;case 19:if(O(F),r=t.memoizedState,r===null)return H(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)zc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=go(e),o!==null){for(t.flags|=128,zc(r,!1),e=o.updateQueue,t.updateQueue=e,Rc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)vi(n,e),n=n.sibling;return k(F,F.current&1|2),N&&Fi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Pe()>tu&&(t.flags|=128,a=!0,zc(r,!1),t.lanes=4194304)}else{if(!a)if(e=go(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Rc(t,e),zc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!N)return H(t),null}else 2*Pe()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,zc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(H(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Pe(),e.sibling=null,n=F.current,k(F,a?n&1|2:n&1),N&&Fi(t,r.treeForkCount),e);case 22:case 23:return ho(t),so(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(H(t),t.subtreeFlags&6&&(t.flags|=8192)):H(t),n=t.updateQueue,n!==null&&Rc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&O(Ca),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),ta(P),H(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Vc(e,t){switch(Ri(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ta(P),ye(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return xe(t),null;case 31:if(t.memoizedState!==null){if(ho(t),t.alternate===null)throw Error(i(340));Ji()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(ho(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ji()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return O(F),null;case 4:return ye(),null;case 10:return ta(t.type),null;case 22:case 23:return ho(t),so(),e!==null&&O(Ca),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return ta(P),null;case 25:return null;default:return null}}function Hc(e,t){switch(Ri(t),t.tag){case 3:ta(P),ye();break;case 26:case 27:case 5:xe(t);break;case 4:ye();break;case 31:t.memoizedState!==null&&ho(t);break;case 13:ho(t);break;case 19:O(F);break;case 10:ta(t.type);break;case 22:case 23:ho(t),so(),e!==null&&O(Ca);break;case 24:ta(P)}}function Uc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Wc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Gc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{no(t,n)}catch(t){Z(e,e.return,t)}}}function Kc(e,t,n){n.props=Ys(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function qc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function Jc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Yc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Xc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[ht]=t}catch(t){Z(e,e.return,t)}}function Zc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Qc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Zc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=cn));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(el(e,t,n),e=e.sibling;e!==null;)el(e,t,n),e=e.sibling}function tl(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[mt]=e,t[ht]=n}catch(t){Z(e,e.return,t)}}var nl=!1,U=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=Pr(e),Fr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[mt]=e,Dt(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Mr(s,h),v=Mr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,E.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(i(331));var c=G;if(G|=4,Fl(o.current),Dl(o,o.current,s,n),G=c,id(0,!1),We&&typeof We.onPostCommitFiberRoot==`function`)try{We.onPostCommitFiberRoot(Ue,o)}catch{}return!0}finally{D.p=a,E.T=r,Vu(e,t)}}function Wu(e,t,n){t=Ti(n,t),t=tc(e.stateNode,t,2),e=Ya(e,t,2),e!==null&&(at(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=Ti(n,e),n=nc(2),r=Ya(t,n,2),r!==null&&(rc(n,r,t,e),at(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>Pe()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=rt()),e=ui(e,t),e!==null&&(at(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Ae(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ke(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=et(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||tt(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Pe(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Kt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),Dt(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Kt(n.imageSizes)+`"]`)):i+=`[href="`+Kt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),Dt(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Kt(r)+`"][href="`+Kt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),Dt(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=Et(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);Dt(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=Et(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Dt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=Et(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Dt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=ge.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=Et(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=Et(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=Et(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Kt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),Dt(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Kt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Kt(n.href)+`"]`);if(r)return t.instance=r,Dt(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),Dt(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,Dt(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),Dt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,Dt(a),a):(r=n,(a=mf.get(o))&&(r=h({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),Dt(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,Dt(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),Dt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=`modulepreload`,v=function(e){return`/`+e},y={},b=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=v(t,n),t=s(t),t in y)return;y[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:_,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},x=c(u(),1),ee=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,S=/^[\\/]{2}/;function C(e,t){return t+e.replace(/\\/g,`/`)}var te=`popstate`;function ne(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function re(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return oe(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:se(t)}return le(t,n,null,e)}function w(e,t){if(e===!1||e==null)throw Error(t)}function T(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function ie(){return Math.random().toString(36).substring(2,10)}function ae(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function oe(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?ce(t):t,state:n,key:t&&t.key||r||ie(),mask:i}}function se({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function ce(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function le(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=ne(e)?e:oe(h.location,e,t);n&&n(r,e),l=u()+1;let d=ae(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=ne(e)?e:oe(h.location,e,t);n&&n(r,e),l=u();let i=ae(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return E(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(te,d),c=e,()=>{i.removeEventListener(te,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function E(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),w(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:se(t);return i=i.replace(/ $/,`%20`),!n&&S.test(i)&&(i=r+i),new URL(i,r)}function D(e,t,n=`/`){return ue(e,t,n,!1)}function ue(e,t,n,r,i){let a=Oe((typeof t==`string`?ce(t):t).pathname||`/`,n);if(a==null)return null;let o=i??fe(e),s=null,c=De(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;w(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Ie([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(w(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),pe(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:xe(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=Ee(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of O(e.path))a(e,t,!0,n)}),t}function O(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=O(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function k(e){e.sort((e,t)=>e.score===t.score?Se(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var me=/^:[\w-]+$/,he=3,ge=2,_e=1,ve=10,ye=-2,be=e=>e===`*`;function xe(e,t){let n=e.split(`/`),r=n.length;return n.some(be)&&(r+=ye),t&&(r+=ge),n.filter(e=>!be(e)).reduce((e,t)=>e+(me.test(t)?he:t===``?_e:ve),r)}function Se(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function Ce(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function Ee(e,t=!1,n=!0){T(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function De(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return T(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Oe(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function ke(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?ce(e):e,a;return n?(n=Fe(n),a=n.startsWith(`/`)?Ae(n.substring(1),`/`):Ae(n,t)):a=t,{pathname:a,search:ze(r),hash:Be(i)}}function Ae(e,t){let n=Le(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function je(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Me(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Ne(e){let t=Me(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function Pe(e,t,n,r=!1){let i;typeof e==`string`?i=ce(e):(i={...e},w(!i.pathname||!i.pathname.includes(`?`),je(`?`,`pathname`,`search`,i)),w(!i.pathname||!i.pathname.includes(`#`),je(`#`,`pathname`,`hash`,i)),w(!i.search||!i.search.includes(`#`),je(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=ke(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Fe=e=>e.replace(/[\\/]{2,}/g,`/`),Ie=e=>Fe(e.join(`/`)),Le=e=>e.replace(/\/+$/,``),Re=e=>Le(e).replace(/^\/*/,`/`),ze=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Be=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Ve=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function He(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Ue(e){return Ie(e.map(e=>e.route.path).filter(Boolean))||`/`}var We=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ge(e,t){let n=e;if(typeof n!=`string`||!ee.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(We)try{let e=new URL(window.location.href),r=S.test(n)?new URL(C(n,e.protocol)):new URL(n),a=Oe(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{T(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ke=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ke);var qe=[`GET`,...Ke];new Set(qe);var Je=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function Ye(e){try{return Je.includes(new URL(e).protocol)}catch{return!1}}var Xe=x.createContext(null);Xe.displayName=`DataRouter`;var Ze=x.createContext(null);Ze.displayName=`DataRouterState`;var Qe=x.createContext(!1);function $e(){return x.useContext(Qe)}var et=x.createContext({isTransitioning:!1});et.displayName=`ViewTransition`;var tt=x.createContext(new Map);tt.displayName=`Fetchers`;var nt=x.createContext(null);nt.displayName=`Await`;var rt=x.createContext(null);rt.displayName=`Navigation`;var it=x.createContext(null);it.displayName=`Location`;var at=x.createContext({outlet:null,matches:[],isDataRoute:!1});at.displayName=`Route`;var ot=x.createContext(null);ot.displayName=`RouteError`;var st=`REACT_ROUTER_ERROR`,ct=`REDIRECT`,lt=`ROUTE_ERROR_RESPONSE`;function ut(e){if(e.startsWith(`${st}:${ct}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function dt(e){if(e.startsWith(`${st}:${lt}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Ve(t.status,t.statusText,t.data)}catch{}}function ft(e,{relative:t}={}){w(pt(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=x.useContext(rt),{hash:i,pathname:a,search:o}=yt(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Ie([n,a])),r.createHref({pathname:s,search:o,hash:i})}function pt(){return x.useContext(it)!=null}function A(){return w(pt(),`useLocation() may be used only in the context of a component.`),x.useContext(it).location}var mt=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function ht(e){x.useContext(rt).static||x.useLayoutEffect(e)}function gt(){let{isDataRoute:e}=x.useContext(at);return e?Rt():_t()}function _t(){w(pt(),`useNavigate() may be used only in the context of a component.`);let e=x.useContext(Xe),{basename:t,navigator:n}=x.useContext(rt),{matches:r}=x.useContext(at),{pathname:i}=A(),a=JSON.stringify(Ne(r)),o=x.useRef(!1);return ht(()=>{o.current=!0}),x.useCallback((r,s={})=>{if(T(o.current,mt),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=Pe(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Ie([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}x.createContext(null);function vt(){let{matches:e}=x.useContext(at);return e[e.length-1]?.params??{}}function yt(e,{relative:t}={}){let{matches:n}=x.useContext(at),{pathname:r}=A(),i=JSON.stringify(Ne(n));return x.useMemo(()=>Pe(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function bt(e,t){return xt(e,t)}function xt(e,t,n){w(pt(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=x.useContext(rt),{matches:i}=x.useContext(at),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Bt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let u=A(),d;if(t){let e=typeof t==`string`?ce(t):t;w(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):D(e,{pathname:p});T(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),T(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=Ot(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Ie([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Ie([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(it.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function St(){let e=Lt(),t=He(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=x.createElement(x.Fragment,null,x.createElement(`p`,null,`💿 Hey developer 👋`),x.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,x.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,x.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),x.createElement(x.Fragment,null,x.createElement(`h2`,null,`Unexpected Application Error!`),x.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?x.createElement(`pre`,{style:i},n):null,o)}var Ct=x.createElement(St,null),wt=class extends x.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=dt(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(at.Provider,{value:this.props.routeContext},x.createElement(ot.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(Et,{error:e},t):t}};wt.contextType=Qe;var Tt=new WeakMap;function Et({children:e,error:t}){let{basename:n}=x.useContext(rt);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=ut(t.digest);if(e){let r=Tt.get(t);if(r)throw r;let i=Ge(e.location,n),a=i.absoluteURL||i.to;if(Ye(a))throw Error(`Invalid redirect location`);if(We&&!Tt.get(t))if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw Tt.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function Dt({routeContext:e,match:t,children:n}){let r=x.useContext(Xe);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(at.Provider,{value:e},n)}function Ot(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);w(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Ue(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||Ct,o&&(s<0&&c===0?(Bt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?x.createElement(n.route.Component,null):n.route.element?n.route.element:e,x.createElement(Dt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?x.createElement(wt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function kt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function At(e){let t=x.useContext(Xe);return w(t,kt(e)),t}function jt(e){let t=x.useContext(Ze);return w(t,kt(e)),t}function Mt(e){let t=x.useContext(at);return w(t,kt(e)),t}function Nt(e){let t=Mt(e),n=t.matches[t.matches.length-1];return w(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Pt(){return Nt(`useRouteId`)}function Ft(){let e=jt(`useNavigation`);return x.useMemo(()=>{let{matches:t,historyAction:n,...r}=e.navigation;return r},[e.navigation])}function It(){let{matches:e,loaderData:t}=jt(`useMatches`);return x.useMemo(()=>e.map(e=>de(e,t)),[e,t])}function Lt(){let e=x.useContext(ot),t=jt(`useRouteError`),n=Nt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function Rt(){let{router:e}=At(`useNavigate`),t=Nt(`useNavigate`),n=x.useRef(!1);return ht(()=>{n.current=!0}),x.useCallback(async(r,i={})=>{T(n.current,mt),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var zt={};function Bt(e,t,n){!t&&!zt[e]&&(zt[e]=!0,T(!1,n))}x.memo(Vt);function Vt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return xt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Ht({to:e,replace:t,state:n,relative:r}){w(pt(),` may be used only in the context of a component.`);let{static:i}=x.useContext(rt);T(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=x.useContext(at),{pathname:o}=A(),s=gt(),c=Pe(e,Ne(a),o,r===`path`),l=JSON.stringify(c);return x.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function Ut(e){w(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Wt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){w(!pt(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=x.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=ce(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=x.useMemo(()=>{let e=Oe(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return T(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:x.createElement(rt.Provider,{value:c},x.createElement(it.Provider,{children:t,value:h}))}function Gt({children:e,location:t}){return bt(Kt(e),t)}x.Component;function Kt(e,t=[]){let n=[];return x.Children.forEach(e,(e,r)=>{if(!x.isValidElement(e))return;let i=[...t,r];if(e.type===x.Fragment){n.push.apply(n,Kt(e.props.children,i));return}w(e.type===Ut,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),w(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Kt(e.props.children,i)),n.push(a)}),n}var qt=`get`,Jt=`application/x-www-form-urlencoded`;function Yt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Xt(e){return Yt(e)&&e.tagName.toLowerCase()===`button`}function Zt(e){return Yt(e)&&e.tagName.toLowerCase()===`form`}function Qt(e){return Yt(e)&&e.tagName.toLowerCase()===`input`}function $t(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function en(e,t){return e.button===0&&(!t||t===`_self`)&&!$t(e)}var tn=null;function nn(){if(tn===null)try{new FormData(document.createElement(`form`),0),tn=!1}catch{tn=!0}return tn}var rn=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function an(e){return e!=null&&!rn.has(e)?(T(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Jt}"`),null):e}function on(e,t){let n,r,i,a,o;if(Zt(e)){let o=e.getAttribute(`action`);r=o?Oe(o,t):null,n=e.getAttribute(`method`)||qt,i=an(e.getAttribute(`enctype`))||Jt,a=new FormData(e)}else if(Xt(e)||Qt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a + + + ) + })} + + + + setDraft(event.target.value)} + /> + + + {addError ?

{addError}

: null} +

+ Verifies access with your Migadu API key before saving. +

+ + +
+ {user ? ( + <> +
+ {user.display_name || user.email} + {user.role} +
+ + + ) : null} +
+ + + +
+

{title}

+ {children} +
+ + ) +} diff --git a/web/src/components/ListEditor.tsx b/web/src/components/ListEditor.tsx new file mode 100644 index 0000000..8362496 --- /dev/null +++ b/web/src/components/ListEditor.tsx @@ -0,0 +1,143 @@ +import { useEffect, useState, type FormEvent } from 'react' +import { + compactListEntries, + validateListEntries, + type ListEntryIssue, +} from '../lib/listEntries' + +interface ListEditorProps { + label: string + description: string + values: string[] + disabled?: boolean + scrollable?: boolean + onChange: (values: string[]) => void +} + +export function ListEditor({ + label, + description, + values, + disabled, + scrollable, + onChange, +}: ListEditorProps) { + const [draft, setDraft] = useState('') + const [issues, setIssues] = useState(null) + const [verifyMessage, setVerifyMessage] = useState(null) + + useEffect(() => { + setIssues(null) + setVerifyMessage(null) + }, [values]) + + function handleAdd(event: FormEvent) { + event.preventDefault() + const entry = draft.trim() + if (!entry) { + return + } + onChange(compactListEntries([...values, entry])) + setDraft('') + } + + function handleRemove(entry: string) { + onChange(values.filter((value) => value !== entry)) + } + + function handleVerify() { + const found = validateListEntries(values) + setIssues(found) + if (values.length === 0) { + setVerifyMessage('No entries to verify.') + return + } + if (found.length === 0) { + setVerifyMessage(`All ${values.length} entries look valid for Migadu.`) + return + } + setVerifyMessage( + `${found.length} invalid entr${found.length === 1 ? 'y' : 'ies'} found.`, + ) + } + + const issueByEntry = new Map( + (issues ?? []).map((issue) => [issue.entry, issue.reason]), + ) + + return ( +
+
+
+

{label}

+

{description}

+
+ {values.length} +
+ +
+ setDraft(event.target.value)} + /> + + +
+ + {verifyMessage ? ( +

0 ? 'form-error' : 'form-success'}> + {verifyMessage} +

+ ) : null} + + {issues && issues.length > 0 ? ( +
    + {issues.map((issue) => ( +
  • + {issue.entry} + {issue.reason} +
  • + ))} +
+ ) : null} + + {values.length === 0 ? ( +

No entries yet.

+ ) : ( +
    + {values.map((entry) => { + const reason = issueByEntry.get(entry) + return ( +
  • +
    + {entry} + {reason ? {reason} : null} +
    + +
  • + ) + })} +
+ )} +
+ ) +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..bfc00f2 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,737 @@ +:root { + --bg: #edf2f4; + --bg-accent: #d9e4ea; + --ink: #13212b; + --muted: #5b6b76; + --panel: #f7fafb; + --line: #c5d0d7; + --accent: #0f6e6a; + --accent-ink: #f4fffe; + --danger: #9b2c2c; + --success: #1f6b3a; + --shadow: 0 18px 40px rgba(19, 33, 43, 0.08); + --radius: 14px; + --font-display: "Fraunces", "Iowan Old Style", "Palatino Linotype", serif; + --font-body: "Source Sans 3", "Segoe UI", sans-serif; + --font-mono: "IBM Plex Mono", "SFMono-Regular", monospace; + color: var(--ink); + background: + radial-gradient(circle at top left, rgba(15, 110, 106, 0.12), transparent 34%), + linear-gradient(160deg, #f5f8fa 0%, var(--bg) 48%, var(--bg-accent) 100%); + font-family: var(--font-body); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + min-height: 100%; +} + +body { + margin: 0; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +button, +input, +select { + font: inherit; +} + +button { + border: 0; + border-radius: 10px; + background: var(--accent); + color: var(--accent-ink); + padding: 0.7rem 1rem; + cursor: pointer; +} + +button:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.ghost-button { + background: transparent; + color: var(--ink); + border: 1px solid var(--line); +} + +input, +select { + width: 100%; + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; + padding: 0.7rem 0.85rem; + color: var(--ink); +} + +.app-shell { + min-height: 100vh; +} + +.sidebar-layout { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1.25rem; + padding: 1.15rem 1rem 1rem; + border-right: 1px solid rgba(197, 208, 215, 0.95); + background: rgba(247, 250, 251, 0.92); + backdrop-filter: blur(10px); + position: sticky; + top: 0; + height: 100vh; + overflow: auto; +} + +.sidebar-top, +.sidebar-bottom { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.side-nav { + display: flex; + flex-direction: column; + gap: 1.1rem; +} + +.nav-group { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.nav-group-label { + margin: 0 0 0.35rem; + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 0.72rem; + font-weight: 700; + color: var(--muted); +} + +.side-nav a { + color: var(--ink); + text-decoration: none; + padding: 0.45rem 0.65rem; + border-radius: 8px; + font-weight: 600; +} + +.side-nav a:hover { + background: rgba(15, 110, 106, 0.08); + text-decoration: none; +} + +.side-nav a.active { + background: var(--ink); + color: #fff; +} + +.nav-disabled { + color: var(--muted); + padding: 0.45rem 0.65rem; + font-size: 0.92rem; +} + +.domain-registry { + border-top: 1px solid var(--line); + padding-top: 0.9rem; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.domain-registry-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; + max-height: 180px; + overflow: auto; +} + +.domain-registry-list li { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.35rem; + align-items: center; + border-radius: 10px; + padding: 0.15rem; +} + +.domain-registry-list li.active { + background: rgba(15, 110, 106, 0.1); +} + +.domain-pick { + text-align: left; + background: transparent; + color: inherit; + padding: 0.4rem 0.5rem; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.domain-pick strong { + font-size: 0.92rem; +} + +.domain-add-form { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.4rem; +} + +.icon-button { + padding: 0.25rem 0.55rem; + min-width: 2rem; + line-height: 1; +} + +.sidebar-session { + border-top: 1px solid var(--line); + padding-top: 0.85rem; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.55rem; +} + +.sidebar-session .session-meta { + align-items: flex-start; +} + +.small { + font-size: 0.82rem; +} + +.content-pane { + width: min(960px, calc(100% - 2rem)); + margin: 0; + padding: 2rem 1.5rem 3rem; +} + +.content-pane:has(.list-page-editor) { + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: column; + padding-bottom: 1.25rem; + box-sizing: border-box; +} + +.content-pane:has(.list-page-editor) > .page-title, +.content-pane:has(.list-page-editor) > .page-lede, +.content-pane:has(.list-page-editor) > .form-error, +.content-pane:has(.list-page-editor) > .form-success, +.content-pane:has(.list-page-editor) > .muted { + flex-shrink: 0; +} + +.list-page-editor { + flex: 1; + min-height: 0; + margin-top: 1rem; +} + +.list-page-editor .tabs, +.list-page-editor .editor-actions { + flex-shrink: 0; +} + +.list-editor-panel { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +.list-editor-panel .panel-header, +.list-editor-panel .inline-form, +.list-editor-panel .empty-hint { + flex-shrink: 0; +} + +.entry-list-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + margin-top: 1rem; + padding-right: 0.25rem; +} + +.content-pane:has(.list-page-editor) .apply-panel { + flex-shrink: 0; + max-height: min(42vh, 22rem); + overflow-y: auto; +} + +.topbar { + display: none; +} + +.brand-block { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.brand { + font-family: var(--font-display); + font-size: 1.45rem; + font-weight: 600; + color: var(--ink); + text-decoration: none; +} + +.brand-sub, +.eyebrow, +.muted, +.empty-hint, +.page-lede, +.auth-footer { + color: var(--muted); +} + +.topnav { + display: flex; + gap: 1rem; +} + +.topnav a { + color: var(--muted); + font-weight: 600; +} + +.topnav a.active { + color: var(--ink); +} + +.session { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.85rem; +} + +.session-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + font-size: 0.9rem; +} + +.session-meta span { + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; + font-size: 0.72rem; +} + +.page { + width: min(960px, calc(100% - 2rem)); + margin: 0 auto; + padding: 2rem 0 3rem; +} + +.page-title { + margin: 0 0 0.4rem; + font-family: var(--font-display); + font-size: clamp(2rem, 4vw, 2.6rem); + font-weight: 600; +} + +.page-lede { + margin: 0 0 1.5rem; +} + +.auth-layout { + min-height: 100vh; + display: grid; + place-items: center; + padding: 2rem 1rem; +} + +.auth-panel, +.panel, +.domain-row { + background: var(--panel); + border: 1px solid rgba(197, 208, 215, 0.95); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.auth-panel { + width: min(440px, 100%); + padding: 2rem; +} + +.eyebrow { + margin: 0 0 0.4rem; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.75rem; + font-weight: 700; +} + +.auth-panel h1, +.panel h2 { + margin: 0; + font-family: var(--font-display); +} + +.lede, +.panel p { + margin: 0.55rem 0 0; + color: var(--muted); + line-height: 1.45; +} + +.stack-form, +.domain-editor { + display: flex; + flex-direction: column; + gap: 1rem; + margin-top: 1.4rem; +} + +.stack-form label, +.field { + display: flex; + flex-direction: column; + gap: 0.4rem; + font-weight: 600; +} + +.form-error { + color: var(--danger); + margin: 0; +} + +.form-success { + color: var(--success); + margin: 0 0 1rem; +} + +.notice { + margin-top: 1.2rem; + padding: 0.9rem 1rem; + border-radius: 10px; + background: rgba(15, 110, 106, 0.08); +} + +.auth-footer { + margin-top: 1.4rem; +} + +.domain-table { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.domain-row { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.15rem; + color: inherit; + text-decoration: none; + transition: transform 140ms ease, border-color 140ms ease; +} + +.domain-row:hover { + transform: translateY(-1px); + border-color: var(--accent); + text-decoration: none; +} + +.domain-row strong { + display: block; + margin-bottom: 0.2rem; +} + +.domain-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.35rem; + white-space: nowrap; +} + +.state { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.55rem; + border-radius: 999px; + background: rgba(19, 33, 43, 0.08); + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.state-active { + background: rgba(31, 107, 58, 0.12); + color: var(--success); +} + +.tabs { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tab { + background: transparent; + color: var(--muted); + border: 1px solid var(--line); +} + +.tab.active { + background: var(--ink); + color: #fff; + border-color: var(--ink); +} + +.panel { + padding: 1.2rem; +} + +.panel-header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1rem; +} + +.count-pill { + min-width: 2rem; + text-align: center; + padding: 0.25rem 0.55rem; + border-radius: 999px; + background: rgba(15, 110, 106, 0.12); + color: var(--accent); + font-weight: 700; +} + +.inline-form { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 0.65rem; +} + +.entry-list { + list-style: none; + margin: 1rem 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.entry-list li { + display: flex; + justify-content: space-between; + gap: 0.75rem; + align-items: center; + padding: 0.65rem 0.75rem; + border-radius: 10px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(197, 208, 215, 0.8); +} + +.entry-list li.entry-invalid { + border-color: rgba(176, 48, 48, 0.55); + background: rgba(176, 48, 48, 0.06); +} + +.entry-main { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 0; +} + +.entry-reason { + color: var(--danger); + font-size: 0.82rem; + font-weight: 600; +} + +.verify-issue-list { + list-style: none; + margin: 0.75rem 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.verify-issue-list li { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 0.85rem; + align-items: baseline; + font-size: 0.9rem; +} + +.verify-issue-list code { + font-family: var(--font-mono); +} + +.verify-issue-list span { + color: var(--danger); +} + +.list-editor-panel .verify-issue-list, +.list-editor-panel .form-error, +.list-editor-panel .form-success { + flex-shrink: 0; +} + +.entry-list code { + font-family: var(--font-mono); + font-size: 0.92rem; +} + +.check-row { + display: flex; + align-items: center; + gap: 0.65rem; + font-weight: 600; +} + +.check-row input { + width: auto; +} + +.replace-option { + align-items: flex-start; + margin-top: 0.75rem; +} + +.replace-option .check-hint { + display: block; + font-weight: 500; + margin-top: 0.2rem; +} + +.editor-actions { + display: flex; + justify-content: flex-end; + gap: 0.65rem; + flex-wrap: wrap; +} + +.apply-panel { + margin-top: 1.4rem; +} + +.apply-domain-list { + list-style: none; + margin: 0.75rem 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.apply-results { + list-style: none; + margin: 1rem 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.apply-results li { + display: flex; + justify-content: space-between; + gap: 0.75rem; + align-items: center; + padding: 0.55rem 0.7rem; + border-radius: 8px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.72); + font-size: 0.92rem; +} + +.apply-results li.ok span { + color: var(--success); + font-weight: 700; +} + +.apply-results li.fail span { + color: var(--danger); + font-weight: 600; +} + +@media (max-width: 720px) { + .sidebar-layout { + grid-template-columns: 1fr; + } + + .sidebar { + position: relative; + height: auto; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .session { + width: 100%; + justify-content: space-between; + } + + .domain-row { + flex-direction: column; + } + + .domain-meta { + align-items: flex-start; + } + + .inline-form { + grid-template-columns: 1fr; + } + + .content-pane { + width: calc(100% - 2rem); + margin: 0 auto; + } +} diff --git a/web/src/lib/listEntries.ts b/web/src/lib/listEntries.ts new file mode 100644 index 0000000..12b2d7a --- /dev/null +++ b/web/src/lib/listEntries.ts @@ -0,0 +1,225 @@ +/** Compact and validate Migadu-style list entries. */ + +export interface ListEntryIssue { + entry: string + reason: string +} + +export function compactListEntries(entries: string[]): string[] { + const unique: string[] = [] + const seen = new Set() + for (const value of entries) { + const entry = normalizeEntry(value) + if (!entry || seen.has(entry)) { + continue + } + seen.add(entry) + unique.push(entry) + } + + return unique.filter((entry, index) => { + for (let otherIndex = 0; otherIndex < unique.length; otherIndex += 1) { + if (otherIndex === index) { + continue + } + const other = unique[otherIndex] + if (covers(other, entry) && other !== entry) { + return false + } + } + return true + }) +} + +export function normalizeEntry(value: string): string { + let entry = value.trim().toLowerCase() + if (!entry) { + return '' + } + if (entry.startsWith('*') && !entry.includes('@') && entry.includes('.')) { + entry = `*@${entry.slice(1)}` + } + const parts = splitAddress(entry) + if ( + parts && + parts.local === '*' && + parts.domain.startsWith('*') && + !parts.domain.startsWith('*.') + ) { + entry = `*@*.${parts.domain.slice(1)}` + } + return entry +} + +export function validateListEntries(entries: string[]): ListEntryIssue[] { + const issues: ListEntryIssue[] = [] + for (const value of entries) { + const raw = value.trim() + if (!raw) { + continue + } + const reason = validateListEntry(raw) + if (reason) { + issues.push({ entry: raw, reason }) + } + } + return issues +} + +export function validateListEntry(value: string): string { + const raw = value.trim() + if (!raw) { + return 'entry is empty' + } + return validateNormalizedEntry(normalizeEntry(raw)) +} + +function validateNormalizedEntry(entry: string): string { + if (!entry) { + return 'entry is empty' + } + if (/\s/.test(entry)) { + return 'entry must not contain whitespace' + } + if ((entry.match(/@/g) ?? []).length !== 1) { + return 'entry must look like local@domain (e.g. user@example.com or *@example.com)' + } + + const parts = splitAddress(entry) + if (!parts) { + return 'entry must look like local@domain' + } + if (!parts.local) { + return 'local part before @ is empty' + } + if (!validLocalPart(parts.local)) { + return 'invalid local part (use user, *@domain, or *prefix)' + } + if (!parts.domain) { + return 'domain after @ is empty' + } + if (!validDomainPart(parts.domain)) { + return 'invalid domain (use example.com or *.com / *.co.uk)' + } + return '' +} + +function validLocalPart(local: string): boolean { + if (local === '*') { + return true + } + if (local.startsWith('*')) { + const rest = local.slice(1) + return rest === '' || validLocalChars(rest) + } + return validLocalChars(local) +} + +function validLocalChars(value: string): boolean { + if (!value) { + return false + } + return /^[a-z0-9._%+-]+$/i.test(value) +} + +function validDomainPart(domain: string): boolean { + if (domain.startsWith('*.')) { + return validDNSSuffix(domain.slice(2)) + } + return validDNSDomain(domain) +} + +function validDNSSuffix(suffix: string): boolean { + if (!suffix || suffix.length > 253) { + return false + } + if (suffix.startsWith('.') || suffix.endsWith('.')) { + return false + } + const labels = suffix.split('.') + if (labels.length < 1) { + return false + } + for (const label of labels) { + if (!validDNSLabel(label)) { + return false + } + } + const tld = labels[labels.length - 1] + return /^[a-z]+$/i.test(tld) +} + +function validDNSDomain(domain: string): boolean { + if (!domain || domain.length > 253) { + return false + } + if (domain.startsWith('.') || domain.endsWith('.')) { + return false + } + const labels = domain.split('.') + if (labels.length < 2) { + return false + } + for (const label of labels) { + if (!validDNSLabel(label)) { + return false + } + } + const tld = labels[labels.length - 1] + return /^[a-z]+$/i.test(tld) +} + +function validDNSLabel(label: string): boolean { + if (!label || label.length > 63) { + return false + } + if (label.startsWith('-') || label.endsWith('-')) { + return false + } + return /^[a-z0-9-]+$/i.test(label) +} + +export function covers(broader: string, specific: string): boolean { + const normalizedBroader = normalizeEntry(broader) + const normalizedSpecific = normalizeEntry(specific) + if (!normalizedBroader || !normalizedSpecific) { + return false + } + if (normalizedBroader === normalizedSpecific) { + return true + } + + const broaderParts = splitAddress(normalizedBroader) + const specificParts = splitAddress(normalizedSpecific) + if (!broaderParts || !specificParts) { + return false + } + if (broaderParts.local !== '*') { + return false + } + return domainMatches(broaderParts.domain, specificParts.domain) +} + +function domainMatches(pattern: string, domain: string): boolean { + if (pattern === domain) { + return true + } + if (pattern.startsWith('*.')) { + const suffix = pattern.slice(1) + return domain.endsWith(suffix) && domain.length > suffix.length + } + return false +} + +function splitAddress( + entry: string, +): { local: string; domain: string } | null { + const at = entry.lastIndexOf('@') + if (at <= 0 || at === entry.length - 1) { + return null + } + return { + local: entry.slice(0, at), + domain: entry.slice(at + 1), + } +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..481c884 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import { AuthProvider } from './auth/AuthContext' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + + + + + , +) diff --git a/web/src/pages/DomainDetailPage.tsx b/web/src/pages/DomainDetailPage.tsx new file mode 100644 index 0000000..14683a7 --- /dev/null +++ b/web/src/pages/DomainDetailPage.tsx @@ -0,0 +1,277 @@ +import { useEffect, useMemo, useState, type FormEvent } from 'react' +import { useParams } from 'react-router-dom' +import { api, ApiError } from '../api/client' +import type { Domain } from '../api/types' +import { AppShell } from '../components/AppShell' +import { ListEditor } from '../components/ListEditor' + +type TabId = 'blacklist' | 'whitelist' | 'recipient' | 'spam' + +const SPAM_LEVELS = ['default', 'low', 'medium', 'high'] as const + +export function DomainDetailPage() { + const params = useParams() + const domainName = params.domain ? decodeURIComponent(params.domain) : '' + + const [domain, setDomain] = useState(null) + const [draft, setDraft] = useState(null) + const [tab, setTab] = useState('blacklist') + const [error, setError] = useState(null) + const [message, setMessage] = useState(null) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + useEffect(() => { + let cancelled = false + ;(async () => { + if (!domainName) { + return + } + setLoading(true) + setError(null) + try { + const loaded = await api.getDomain(domainName) + if (!cancelled) { + setDomain(loaded) + setDraft(normalizeDomain(loaded)) + } + } catch (err) { + if (!cancelled) { + setError(err instanceof ApiError ? err.message : 'Failed to load domain') + } + } finally { + if (!cancelled) { + setLoading(false) + } + } + })() + return () => { + cancelled = true + } + }, [domainName]) + + const dirty = useMemo(() => { + if (!domain || !draft) { + return false + } + return JSON.stringify(serialize(draft)) !== JSON.stringify(serialize(domain)) + }, [domain, draft]) + + async function handleSave(event: FormEvent) { + event.preventDefault() + if (!draft || !domain) { + return + } + setSaving(true) + setError(null) + setMessage(null) + try { + const updated = await api.updateDomain(domain.name, { + sender_denylist: draft.sender_denylist, + sender_allowlist: draft.sender_allowlist, + recipient_denylist: draft.recipient_denylist, + spam_aggressiveness: String(draft.spam_aggressiveness), + junk_subject_keyword_spam: draft.junk_subject_keyword_spam, + subject_rewriting_enabled: draft.subject_rewriting_enabled, + }) + const normalized = normalizeDomain(updated) + setDomain(normalized) + setDraft(normalized) + setMessage('Settings saved to Migadu.') + } catch (err) { + if (err instanceof ApiError) { + const detail = err.body.migadu_body + ? `${err.message}: ${err.body.migadu_body}` + : err.message + setError(detail) + } else { + setError('Failed to save settings') + } + } finally { + setSaving(false) + } + } + + function handleReset() { + if (domain) { + setDraft(normalizeDomain(domain)) + setMessage(null) + setError(null) + } + } + + return ( + +

+ Blacklist, whitelist, recipient deny, and spam controls for this domain + on Migadu. Shared lists are edited separately and applied manually; to + allow a sender that appears on a shared blacklist, add them to this + domain’s whitelist (whitelist is evaluated before blacklist). +

+ + {loading ?

Loading domain settings…

: null} + {error ?

{error}

: null} + {message ?

{message}

: null} + + {draft ? ( +
+
+ {( + [ + ['blacklist', 'Blacklist'], + ['whitelist', 'Whitelist'], + ['recipient', 'Recipient deny'], + ['spam', 'Spam'], + ] as const + ).map(([id, label]) => ( + + ))} +
+ + {tab === 'blacklist' ? ( + + setDraft({ ...draft, sender_denylist }) + } + /> + ) : null} + + {tab === 'whitelist' ? ( + + setDraft({ ...draft, sender_allowlist }) + } + /> + ) : null} + + {tab === 'recipient' ? ( + + setDraft({ ...draft, recipient_denylist }) + } + /> + ) : null} + + {tab === 'spam' ? ( +
+
+
+

Spam controls

+

Domain-level spam aggressiveness and subject handling.

+
+
+ + + + + + +
+ ) : null} + +
+ + +
+ + ) : null} +
+ ) +} + +function normalizeDomain(domain: Domain): Domain { + return { + ...domain, + sender_denylist: [...(domain.sender_denylist ?? [])], + sender_allowlist: [...(domain.sender_allowlist ?? [])], + recipient_denylist: [...(domain.recipient_denylist ?? [])], + spam_aggressiveness: domain.spam_aggressiveness ?? 'default', + } +} + +function serialize(domain: Domain) { + return { + sender_denylist: domain.sender_denylist, + sender_allowlist: domain.sender_allowlist, + recipient_denylist: domain.recipient_denylist, + spam_aggressiveness: String(domain.spam_aggressiveness), + junk_subject_keyword_spam: domain.junk_subject_keyword_spam, + subject_rewriting_enabled: domain.subject_rewriting_enabled, + } +} diff --git a/web/src/pages/HomePage.tsx b/web/src/pages/HomePage.tsx new file mode 100644 index 0000000..d58fada --- /dev/null +++ b/web/src/pages/HomePage.tsx @@ -0,0 +1,29 @@ +import { Navigate } from 'react-router-dom' +import { AppShell } from '../components/AppShell' +import { useManagedDomains } from '../auth/DomainsContext' + +export function HomePage() { + const { domains, loading } = useManagedDomains() + + if (!loading && domains.length > 0) { + return ( + + ) + } + + return ( + + {loading ? ( +

Loading administered domains…

+ ) : ( +

+ Add a domain in the sidebar. The backend will verify it is reachable + with your configured Migadu API key before registering it here. +

+ )} +
+ ) +} diff --git a/web/src/pages/LoginPage.tsx b/web/src/pages/LoginPage.tsx new file mode 100644 index 0000000..a2235f9 --- /dev/null +++ b/web/src/pages/LoginPage.tsx @@ -0,0 +1,83 @@ +import { useState, type FormEvent } from 'react' +import { Link, Navigate } from 'react-router-dom' +import { ApiError } from '../api/client' +import { useAuth } from '../auth/AuthContext' + +export function LoginPage() { + const { user, status, loading, login } = useAuth() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [submitting, setSubmitting] = useState(false) + + if (!loading && user) { + return + } + + const localEnabled = status?.auth_mode !== 'pangolin' + + async function handleSubmit(event: FormEvent) { + event.preventDefault() + setError(null) + setSubmitting(true) + try { + await login(email, password) + } catch (err) { + setError(err instanceof ApiError ? err.message : 'Login failed') + } finally { + setSubmitting(false) + } + } + + return ( +
+
+

MigaduAdmin

+

Sign in

+

+ Manage domain denylists, allowlists, and spam settings across your + Migadu domains. +

+ + {!localEnabled ? ( +

+ Local login is disabled. Authenticate through Pangolin to continue. +

+ ) : ( +
+ + + {error ?

{error}

: null} + +
+ )} + + {status?.registration_open ? ( +

+ First-time setup? Create the admin account +

+ ) : null} +
+
+ ) +} diff --git a/web/src/pages/RegisterPage.tsx b/web/src/pages/RegisterPage.tsx new file mode 100644 index 0000000..3271d0b --- /dev/null +++ b/web/src/pages/RegisterPage.tsx @@ -0,0 +1,87 @@ +import { useState, type FormEvent } from 'react' +import { Link, Navigate } from 'react-router-dom' +import { ApiError } from '../api/client' +import { useAuth } from '../auth/AuthContext' + +export function RegisterPage() { + const { user, status, loading, register } = useAuth() + const [email, setEmail] = useState('') + const [displayName, setDisplayName] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + const [submitting, setSubmitting] = useState(false) + + if (!loading && user) { + return + } + + if (!loading && status && !status.registration_open) { + return + } + + async function handleSubmit(event: FormEvent) { + event.preventDefault() + setError(null) + setSubmitting(true) + try { + await register(email, password, displayName) + } catch (err) { + setError(err instanceof ApiError ? err.message : 'Registration failed') + } finally { + setSubmitting(false) + } + } + + return ( +
+
+

MigaduAdmin

+

Create admin

+

+ The first registered account becomes the default administrator. +

+ +
+ + + + {error ?

{error}

: null} + +
+ +

+ Already set up? Sign in +

+
+
+ ) +} diff --git a/web/src/pages/SharedListsPage.tsx b/web/src/pages/SharedListsPage.tsx new file mode 100644 index 0000000..e2cf49e --- /dev/null +++ b/web/src/pages/SharedListsPage.tsx @@ -0,0 +1,442 @@ +import { useEffect, useMemo, useState, type FormEvent } from 'react' +import { Link } from 'react-router-dom' +import { api, ApiError } from '../api/client' +import type { ApplySharedListsResult, SharedLists } from '../api/types' +import { useManagedDomains } from '../auth/DomainsContext' +import { AppShell } from '../components/AppShell' +import { ListEditor } from '../components/ListEditor' + +type TabId = 'blacklist' | 'whitelist' | 'recipient' +type PanelMode = 'apply' | 'import' | null + +function emptyLists(): SharedLists { + return { + sender_denylist: [], + sender_allowlist: [], + recipient_denylist: [], + } +} + +function normalizeLists(lists: SharedLists): SharedLists { + return { + sender_denylist: [...(lists.sender_denylist ?? [])], + sender_allowlist: [...(lists.sender_allowlist ?? [])], + recipient_denylist: [...(lists.recipient_denylist ?? [])], + } +} + +export function SharedListsPage() { + const { domains, loading: domainsLoading } = useManagedDomains() + + const [saved, setSaved] = useState(null) + const [draft, setDraft] = useState(null) + const [tab, setTab] = useState('blacklist') + const [error, setError] = useState(null) + const [message, setMessage] = useState(null) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + const [panelMode, setPanelMode] = useState(null) + const [targetAll, setTargetAll] = useState(true) + const [selectedDomains, setSelectedDomains] = useState([]) + const [replaceLists, setReplaceLists] = useState(false) + const [busy, setBusy] = useState(false) + const [panelResults, setPanelResults] = useState( + null, + ) + + useEffect(() => { + let cancelled = false + ;(async () => { + setLoading(true) + setError(null) + try { + const lists = await api.getSharedLists() + if (!cancelled) { + const normalized = normalizeLists(lists) + setSaved(normalized) + setDraft(normalized) + } + } catch (err) { + if (!cancelled) { + setError( + err instanceof ApiError ? err.message : 'Failed to load shared lists', + ) + setSaved(emptyLists()) + setDraft(emptyLists()) + } + } finally { + if (!cancelled) { + setLoading(false) + } + } + })() + return () => { + cancelled = true + } + }, []) + + const dirty = useMemo(() => { + if (!saved || !draft) { + return false + } + return JSON.stringify(draft) !== JSON.stringify(saved) + }, [draft, saved]) + + async function handleSave(event: FormEvent) { + event.preventDefault() + if (!draft) { + return + } + setSaving(true) + setError(null) + setMessage(null) + try { + const updated = normalizeLists(await api.putSharedLists(draft)) + setSaved(updated) + setDraft(updated) + setMessage('Shared lists saved locally. Use Apply to push them to Migadu.') + } catch (err) { + setError(err instanceof ApiError ? err.message : 'Failed to save shared lists') + } finally { + setSaving(false) + } + } + + function handleReset() { + if (saved) { + setDraft(normalizeLists(saved)) + setMessage(null) + setError(null) + } + } + + function openPanel(mode: 'apply' | 'import') { + setPanelMode(mode) + setTargetAll(true) + setSelectedDomains(domains.map((domain) => domain.name)) + setReplaceLists(false) + setPanelResults(null) + setError(null) + setMessage(null) + } + + function toggleDomain(name: string) { + setSelectedDomains((current) => + current.includes(name) + ? current.filter((item) => item !== name) + : [...current, name], + ) + } + + function targetBody() { + return targetAll ? { all: true as const } : { domains: selectedDomains } + } + + async function handleApply() { + setBusy(true) + setError(null) + setMessage(null) + setPanelResults(null) + try { + if (dirty && draft) { + const updated = normalizeLists(await api.putSharedLists(draft)) + setSaved(updated) + setDraft(updated) + } + const results = await api.applySharedLists({ + ...targetBody(), + replace: replaceLists, + }) + setPanelResults(results) + const failed = results.filter((result) => !result.ok).length + const okCount = results.length - failed + const rejectedCount = results.reduce( + (sum, result) => sum + (result.rejected?.length ?? 0), + 0, + ) + const modeLabel = replaceLists ? 'Replaced lists on' : 'Applied shared lists to' + if (failed === 0 && rejectedCount === 0) { + setMessage(`${modeLabel} ${okCount} domain(s).`) + } else if (failed === 0) { + setMessage( + `${modeLabel} ${okCount} domain(s); Migadu rejected ${rejectedCount} entr(y/ies) (skipped). See details below.`, + ) + } else { + setMessage( + `${modeLabel} ${okCount} domain(s); ${failed} failed. See details below.`, + ) + } + } catch (err) { + setError( + err instanceof ApiError ? err.message : 'Failed to apply shared lists', + ) + } finally { + setBusy(false) + } + } + + async function handleImport() { + setBusy(true) + setError(null) + setMessage(null) + setPanelResults(null) + try { + if (dirty && draft) { + await api.putSharedLists(draft) + } + const response = await api.importSharedLists(targetBody()) + const updated = normalizeLists(response.lists) + setSaved(updated) + setDraft(updated) + setPanelResults(response.results ?? []) + const failed = (response.results ?? []).filter((result) => !result.ok).length + const okCount = (response.results ?? []).length - failed + if (failed === 0) { + setMessage( + `Imported lists from ${okCount} domain(s) into shared (merged + compacted).`, + ) + } else { + setMessage( + `Imported from ${okCount} domain(s); ${failed} failed. Shared lists updated from successful imports.`, + ) + } + } catch (err) { + setError( + err instanceof ApiError ? err.message : 'Failed to import into shared lists', + ) + } finally { + setBusy(false) + } + } + + const canRunTargets = + !busy && domains.length > 0 && (targetAll || selectedDomains.length > 0) + + return ( + +

+ Shared blacklist, whitelist, and recipient deny. Import from domains to + merge into shared, or Apply to push to Migadu (merge by default, or + replace domain lists). Broader filters like *@domain win over specifics. + Recipient deny is domain-scoped on apply (only addresses for that domain + are sent). To allow a shared-blocked sender on one domain, add them to + that domain’s whitelist. +

+ + {loading ?

Loading shared lists…

: null} + {error ?

{error}

: null} + {message ?

{message}

: null} + + {draft ? ( +
+
+ {( + [ + ['blacklist', 'Blacklist'], + ['whitelist', 'Whitelist'], + ['recipient', 'Recipient deny'], + ] as const + ).map(([id, label]) => ( + + ))} +
+ +
+ + + + +
+ + {tab === 'blacklist' ? ( + + setDraft({ ...draft, sender_denylist }) + } + /> + ) : null} + + {tab === 'whitelist' ? ( + + setDraft({ ...draft, sender_allowlist }) + } + /> + ) : null} + + {tab === 'recipient' ? ( + + setDraft({ ...draft, recipient_denylist }) + } + /> + ) : null} + + ) : null} + + {panelMode ? ( +
+
+
+

+ {panelMode === 'apply' ? 'Apply to domains' : 'Import from domains'} +

+

+ {panelMode === 'apply' + ? 'Push shared lists to selected domains on Migadu. Unsaved shared edits are saved first.' + : 'Merge denylist, allowlist, and recipient deny from selected domains into shared (compacted). Unsaved shared edits are saved first.'} +

+
+
+ + + + {!targetAll ? ( +
    + {domains.map((domain) => ( +
  • + +
  • + ))} +
+ ) : null} + + {panelMode === 'apply' ? ( + + ) : null} + + {domains.length === 0 ? ( +

+ No administered domains yet. Add one from the sidebar first. +

+ ) : null} + +
+ + {panelMode === 'apply' ? ( + + ) : ( + + )} +
+ + {panelResults ? ( +
    + {panelResults.map((result) => ( +
  • +
    + + {result.domain} + + {result.rejected && result.rejected.length > 0 ? ( +

    + Skipped rejected: {result.rejected.join(', ')} +

    + ) : null} +
    + {result.ok ? 'ok' : result.error || 'failed'} +
  • + ))} +
+ ) : null} +
+ ) : null} +
+ ) +} diff --git a/web/src/pages/StubPage.tsx b/web/src/pages/StubPage.tsx new file mode 100644 index 0000000..c14b9bc --- /dev/null +++ b/web/src/pages/StubPage.tsx @@ -0,0 +1,33 @@ +import { useParams } from 'react-router-dom' +import { AppShell } from '../components/AppShell' +import { useManagedDomains } from '../auth/DomainsContext' + +export function StubPage({ + title, + description, + domainScoped = false, +}: { + title: string + description: string + domainScoped?: boolean +}) { + const params = useParams() + const { domains } = useManagedDomains() + const domainName = params.domain + ? decodeURIComponent(params.domain) + : domains[0]?.name + + return ( + +

{description}

+ {domainScoped ? ( +

+ {domainName + ? `Selected domain: ${domainName}` + : 'Select or add a domain in the sidebar first.'} +

+ ) : null} +

This section is stubbed for a later Migadu API integration.

+
+ ) +} diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..65a3f48 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + }, +})