diff --git a/app/chatgpt-auth.ts b/app/chatgpt-auth.ts deleted file mode 100644 index 8d1fb35..0000000 --- a/app/chatgpt-auth.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; - -export type ChatGPTUser = { - displayName: string; - email: string; - fullName: string | null; -}; - -const USER_EMAIL_HEADER = "oai-authenticated-user-email"; -const USER_FULL_NAME_HEADER = "oai-authenticated-user-full-name"; -const USER_FULL_NAME_ENCODING_HEADER = - "oai-authenticated-user-full-name-encoding"; -const PERCENT_ENCODED_UTF8 = "percent-encoded-utf-8"; -const SIGN_IN_PATH = "/signin-with-chatgpt"; -const SIGN_OUT_PATH = "/signout-with-chatgpt"; -const CALLBACK_PATH = "/callback"; - -export async function getChatGPTUser(): Promise { - const requestHeaders = await headers(); - const email = requestHeaders.get(USER_EMAIL_HEADER); - if (!email) return null; - - const encodedFullName = requestHeaders.get(USER_FULL_NAME_HEADER); - const fullName = - encodedFullName && - requestHeaders.get(USER_FULL_NAME_ENCODING_HEADER) === PERCENT_ENCODED_UTF8 - ? safeDecodeURIComponent(encodedFullName) - : null; - - return { - displayName: fullName ?? email, - email, - fullName, - }; -} - -export async function requireChatGPTUser( - returnTo: string, -): Promise { - const user = await getChatGPTUser(); - if (user) return user; - - redirect(chatGPTSignInPath(returnTo)); -} - -export function chatGPTSignInPath(returnTo: string): string { - const safeReturnTo = safeRelativeReturnPath(returnTo); - return `${SIGN_IN_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`; -} - -export function chatGPTSignOutPath(returnTo = "/"): string { - const safeReturnTo = safeRelativeReturnPath(returnTo); - return `${SIGN_OUT_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`; -} - -function safeRelativeReturnPath(value: string): string { - if (!value.startsWith("/") || value.startsWith("//")) return "/"; - - let url: URL; - try { - url = new URL(value, "https://app.local"); - } catch { - return "/"; - } - if (url.origin !== "https://app.local") return "/"; - if (isReservedAuthPath(url.pathname)) return "/"; - - return `${url.pathname}${url.search}${url.hash}`; -} - -function isReservedAuthPath(pathname: string): boolean { - return ( - pathname === SIGN_IN_PATH || - pathname === SIGN_OUT_PATH || - pathname === CALLBACK_PATH - ); -} - -function safeDecodeURIComponent(value: string): string | null { - try { - return decodeURIComponent(value); - } catch { - return null; - } -} diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index 828a2bf..0000000 --- a/app/globals.css +++ /dev/null @@ -1,8 +0,0 @@ -@import "tailwindcss"; - -:root{--bg:#f2f3ef;--panel:#fff;--ink:#17201c;--muted:#707973;--line:#e2e6e1;--soft:#f7f8f5;--green:#256e4b;--green-dark:#17543a;--blue:#356bff;--coral:#e86f51;--amber:#d99a33;--sidebar:#17241e;--shadow:0 8px 30px rgba(30,46,37,.055);--radius:18px} -*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font-geist-sans),Arial,sans-serif;font-size:14px}button,input,select{font:inherit}button{color:inherit}button:focus-visible,input:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid rgba(53,107,255,.35);outline-offset:2px}.skip-link{position:fixed;left:12px;top:-50px;z-index:100;background:#fff;padding:10px 14px;border-radius:8px}.skip-link:focus{top:12px}.app-shell{min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:238px;background:var(--sidebar);color:#eef4ef;padding:25px 16px 18px;display:flex;flex-direction:column;z-index:30}.sidebar-top{display:flex;align-items:center;justify-content:space-between;padding:0 8px 28px}.brand{display:flex;align-items:center;gap:11px;font-size:16px;font-weight:720;letter-spacing:-.02em}.brand-mark{display:grid;place-items:center;width:31px;height:31px;background:#f2f4ef;color:#183d2d;border-radius:10px;font-family:Georgia,serif;font-size:18px;font-weight:800}.sidebar nav,.sidebar-section{display:flex;flex-direction:column;gap:4px}.sidebar nav button,.sidebar-section>button{border:0;background:transparent;color:#aebbb4;display:flex;align-items:center;gap:12px;padding:10px 11px;border-radius:10px;text-align:left;cursor:pointer;font-weight:540;transition:.16s ease}.sidebar nav button:hover,.sidebar-section>button:hover,.sidebar nav button.active,.sidebar-section>button.active{background:#28372f;color:#fff}.sidebar nav button.active,.sidebar-section>button.active{box-shadow:inset 3px 0 #7ed9a7}.sidebar nav em{margin-left:auto;background:#32433a;color:#d7e5dc;font-size:11px;font-style:normal;min-width:21px;padding:2px 6px;border-radius:10px;text-align:center}.sidebar-section{border-top:1px solid #304037;margin-top:16px;padding-top:17px}.sidebar-section>p{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:#74847a;font-weight:700;padding:0 11px;margin:0 0 7px}.live-dot{width:7px;height:7px;background:#6ee7a4;border-radius:50%;margin-left:auto;box-shadow:0 0 0 4px rgba(110,231,164,.1)}.focus-mini{margin-top:auto;border:1px solid #37493f;background:#213128;border-radius:13px;padding:10px;display:flex;align-items:center;gap:10px;color:#eef4ef;cursor:pointer;text-align:left}.focus-mini.active{border-color:#5c9b78}.focus-mini-icon{width:34px;height:34px;border-radius:9px;background:#2f493a;display:grid;place-items:center;color:#95e6ba}.focus-mini>span:nth-child(2){display:flex;flex-direction:column;gap:2px;flex:1}.focus-mini small{color:#91a299;font-size:10px}.profile{display:flex;align-items:center;gap:10px;padding:17px 7px 0;margin-top:12px;border-top:1px solid #304037}.profile-avatar{width:33px;height:33px;display:grid;place-items:center;background:#d8efe2;color:#1f6748;border-radius:10px;font-size:11px;font-weight:800}.profile>span:nth-child(2){display:flex;flex:1;flex-direction:column}.profile small{color:#86968d;font-size:10px;margin-top:2px}.workspace{margin-left:238px;min-height:100vh}.topbar{height:72px;background:rgba(242,243,239,.92);backdrop-filter:blur(14px);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 32px;position:sticky;top:0;z-index:20}.command-trigger{border:1px solid #dbe0dc;background:#fff;color:#8a928d;border-radius:10px;height:38px;width:300px;display:flex;align-items:center;gap:9px;padding:0 11px;cursor:pointer;text-align:left}.command-trigger span{flex:1}.command-trigger kbd,.command-modal kbd{display:inline-flex;align-items:center;gap:2px;border:1px solid #daddda;border-bottom-width:2px;background:#f6f7f5;border-radius:5px;padding:2px 6px;color:#7b847e;font-size:10px}.top-actions{display:flex;align-items:center;gap:10px}.domain-select{height:38px;display:flex;align-items:center;gap:7px;border:1px solid #dbe0dc;background:#fff;padding:0 10px;border-radius:10px}.domain-select select{appearance:none;border:0;background:transparent;font-size:12px;font-weight:620;outline:0}.domain-select svg{color:#8b948f}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#89938d}.dot.all{background:#55635b}.dot.work,.domain-pill.work,.event.work{background:#4a73f2}.dot.ots,.domain-pill.ots,.event.ots{background:#e27458}.dot.personal,.domain-pill.personal,.event.personal{background:#2e9a6d}.icon-button{border:0;background:transparent;padding:7px;border-radius:8px;color:#727b75;display:inline-grid;place-items:center;cursor:pointer}.icon-button:hover{background:#eef0ed;color:var(--ink)}.notification{position:relative}.notification i{position:absolute;right:6px;top:5px;width:6px;height:6px;background:var(--coral);border:2px solid var(--bg);box-sizing:content-box;border-radius:50%}.quick-add,.primary-button,.secondary-button,.text-button{border:0;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;gap:7px;cursor:pointer;font-weight:650;white-space:nowrap}.quick-add,.primary-button{background:var(--green);color:#fff;padding:10px 14px;box-shadow:0 3px 10px rgba(37,110,75,.15)}.quick-add:hover,.primary-button:hover{background:var(--green-dark)}.secondary-button{background:#fff;border:1px solid var(--line);padding:9px 12px}.secondary-button:hover{background:#f7f8f6}.text-button{background:transparent;color:#5f6a63;padding:7px 2px}.text-button:hover{color:var(--green)}.mobile-menu,.close-mobile{display:none}main{max-width:1500px;margin:0 auto;padding:36px 38px 70px}.page-heading{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:28px}.page-heading h1{font-size:30px;letter-spacing:-.045em;margin:4px 0 7px;line-height:1.08}.page-heading>div>p:last-child{color:var(--muted);margin:0;font-size:13px}.eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:10px;font-weight:760;color:#849089;margin:0;display:flex;align-items:center;gap:6px}.panel{background:var(--panel);border:1px solid rgba(216,222,217,.9);border-radius:var(--radius);box-shadow:var(--shadow)}.hero-grid{display:grid;grid-template-columns:1.75fr repeat(3,1fr);gap:14px;margin-bottom:14px}.focus-card{grid-row:span 2;min-height:190px;padding:22px;display:grid;grid-template-columns:1fr auto;gap:12px;background:#19362a;color:#eff8f2;position:relative;overflow:hidden}.focus-card:after{content:"";position:absolute;width:190px;height:190px;border:44px solid rgba(126,217,167,.08);border-radius:50%;right:-77px;top:-86px}.focus-card .eyebrow{color:#91bea5}.focus-card h2{font-size:22px;letter-spacing:-.035em;margin:8px 0}.focus-card p:not(.eyebrow){color:#b7c9bd;max-width:390px;line-height:1.55;margin:0;font-size:12px}.focus-card .primary-button{align-self:end;justify-self:start;background:#dff2e6;color:#1d5f42;z-index:1}.score-ring{width:86px;height:86px;border-radius:50%;background:conic-gradient(#79daa4 0 82%,rgba(255,255,255,.13) 82% 100%);display:grid;place-items:center;align-content:center;position:relative;z-index:1}.score-ring:before{content:"";position:absolute;inset:7px;border-radius:50%;background:#19362a}.score-ring strong,.score-ring span{z-index:1}.score-ring strong{font-size:25px;line-height:1}.score-ring span{font-size:9px;color:#9db5a7;margin-top:3px}.score-ring.small{width:94px;height:94px;background:conic-gradient(var(--blue) 0 86%,#e8ecf3 86%)}.score-ring.small:before{background:#fff}.score-ring.small span{color:#8b928e}.metric{padding:17px;display:flex;align-items:center;gap:13px;min-height:88px}.metric-icon{width:39px;height:39px;border-radius:12px;display:grid;place-items:center;flex:0 0 auto}.metric-icon svg{width:18px}.metric-icon.blue{background:#eaf0ff;color:#356bff}.metric-icon.coral{background:#fff0eb;color:#dd6548}.metric-icon.mint{background:#e8f6ef;color:#28865d}.metric-icon.amber{background:#fff5dc;color:#bd7b1f}.metric>div{display:flex;flex-direction:column}.metric small{font-size:10px;color:#7a847e;font-weight:620}.metric strong{font-size:20px;margin:3px 0 1px;letter-spacing:-.04em}.metric strong span{font-size:11px;color:#909892;font-weight:550}.metric em,.metric div>em{font-style:normal;font-size:9px;color:#3a9369;display:flex;align-items:center;gap:3px}.main-grid{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(280px,.85fr);gap:14px}.today-panel{padding:21px}.panel-header{display:flex;align-items:center;justify-content:space-between;gap:15px}.panel-header h2{font-size:17px;margin:5px 0 0;letter-spacing:-.025em}.task-list{margin-top:16px}.task-row{display:flex;align-items:center;gap:10px;border-top:1px solid #edf0ed;padding:12px 2px}.task-row.is-done{opacity:.55}.task-row.is-done .task-copy strong{text-decoration:line-through}.check-button{width:27px;height:27px;border:0;background:transparent;padding:0;color:#a3aaa6;cursor:pointer;display:grid;place-items:center}.task-row.is-done .check-button{background:var(--green);color:#fff;border-radius:50%}.task-copy{min-width:0;display:flex;flex-direction:column;gap:4px;flex:1}.task-copy strong{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.task-copy span{color:#8b948e;font-size:9px;display:flex;align-items:center;gap:5px}.priority{font-size:8px;text-transform:uppercase;letter-spacing:.08em;font-weight:750;border-radius:5px;padding:4px 6px}.priority.urgent{background:#ffede8;color:#bd4e36}.priority.high{background:#fff4dd;color:#9a681e}.priority.normal{background:#eef1ef;color:#6e7771}.add-row,.add-card{border:1px dashed #d8ddd9;background:#fafbf9;color:#758078;border-radius:9px;width:100%;padding:9px;display:flex;align-items:center;justify-content:center;gap:6px;cursor:pointer}.habits-panel{padding:21px}.completion-badge{font-size:9px;font-weight:700;color:#25774f;background:#e8f6ee;padding:5px 8px;border-radius:20px}.habit-list{margin-top:13px}.habit-row{width:100%;border:0;border-top:1px solid #edf0ed;background:transparent;display:flex;align-items:center;gap:10px;padding:10px 0;cursor:pointer;text-align:left}.habit-icon{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;font-size:17px;font-weight:750}.habit-icon.mint{background:#e5f5ec;color:#277951}.habit-icon.blue{background:#e9efff;color:#356bff}.habit-icon.amber{background:#fff3d8;color:#ae721d}.habit-icon.coral{background:#ffede7;color:#c75b40}.habit-copy{display:flex;flex-direction:column;gap:2px;flex:1}.habit-copy strong{font-size:11px}.habit-copy small{font-size:9px;color:#9b7744;display:flex;align-items:center;gap:3px}.habit-check{width:22px;height:22px;border:1px solid #ccd2ce;border-radius:7px;display:grid;place-items:center}.habit-row.is-done .habit-check{background:var(--green);border-color:var(--green);color:#fff}.habit-row.is-done .habit-copy strong{text-decoration:line-through;color:#859087}.week-panel,.projects-panel{padding:21px}.chart-summary{display:flex;align-items:end;justify-content:space-between;margin:14px 0}.chart-summary div{display:flex;flex-direction:column}.chart-summary strong{font-size:25px;letter-spacing:-.05em}.chart-summary span{font-size:9px;color:#89928c}.chart-summary em{font-style:normal;font-size:9px;color:#2e8f62;display:flex;align-items:center;gap:4px}.bar-chart{height:105px;border-bottom:1px solid var(--line);display:flex;align-items:flex-end;gap:10px;padding:0 2px}.bar-wrap{flex:1;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:6px}.bar-wrap span{width:100%;max-width:25px;background:#dce7e0;border-radius:5px 5px 2px 2px}.bar-wrap span.active{background:var(--green)}.bar-wrap small{font-size:8px;color:#8b948e;margin-bottom:-17px}.project-list{margin-top:14px}.project-row{display:flex;width:100%;align-items:center;gap:10px;padding:10px 0;border:0;border-top:1px solid #edf0ed;background:transparent;cursor:pointer;text-align:left}.project-icon{width:34px;height:34px;border-radius:10px;color:#fff;display:grid;place-items:center;font-weight:800;font-size:12px}.project-copy{display:flex;flex-direction:column;gap:8px;flex:1}.project-copy>span{display:flex;justify-content:space-between}.project-copy strong{font-size:11px}.project-copy small{font-size:9px;color:#8b948e}.project-percent{font-size:10px;width:35px;text-align:right}.progress{height:5px;background:#edf0ed;border-radius:8px;overflow:hidden}.progress span{display:block;height:100%;border-radius:8px}.agent-panel{grid-column:1/-1;padding:18px 21px;display:flex;align-items:center;gap:14px;background:#fbfaf7}.agent-glow{width:43px;height:43px;background:#ece8ff;color:#6753c4;border-radius:13px;display:grid;place-items:center}.agent-panel>div:nth-child(2){flex:1}.agent-panel h2{font-size:15px;margin:4px 0}.agent-panel p:not(.eyebrow){font-size:10px;color:#78817b;margin:0}.view-stack{display:flex;flex-direction:column;gap:16px}.view-toolbar{display:flex;justify-content:space-between;align-items:center}.segmented{background:#e8ebe7;border-radius:9px;padding:3px;display:inline-flex;gap:2px}.segmented button{border:0;background:transparent;padding:7px 11px;border-radius:7px;color:#6f7972;font-size:11px;cursor:pointer}.segmented button.active{background:#fff;color:#1e2822;box-shadow:0 1px 4px rgba(25,36,30,.1);font-weight:650}.toolbar-actions{display:flex;gap:8px}.kanban{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;align-items:start}.kanban-column{background:#e9ece8;border:1px solid #dee3de;border-radius:15px;padding:11px}.kanban-column>header{display:flex;align-items:center;gap:8px;padding:4px 5px 12px;font-size:11px;font-weight:700}.kanban-column>header span{display:flex;align-items:center;gap:7px}.kanban-column>header small{margin-left:auto;color:#87918a;background:#f7f8f6;border-radius:8px;padding:2px 6px}.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block}.status-to-do{background:#8e9892}.status-in-progress{background:#356bff}.status-done{background:#2e9a6d}.kanban-cards{display:flex;flex-direction:column;gap:9px}.kanban-card{background:#fff;border:1px solid #dfe4df;border-radius:12px;padding:13px;box-shadow:0 3px 10px rgba(26,38,31,.04)}.kanban-card.is-done{opacity:.68}.kanban-top{display:flex;align-items:center;color:#a2aaa5}.kanban-top .priority{margin-left:5px}.kanban-top svg:last-child{margin-left:auto}.kanban-card>button{border:0;background:transparent;padding:11px 0 2px;display:flex;gap:8px;align-items:flex-start;text-align:left;cursor:pointer}.kanban-card>button strong{font-size:12px;line-height:1.4}.task-check{width:17px;height:17px;border:1px solid #cbd1cd;border-radius:50%;display:grid;place-items:center;flex:0 0 auto}.kanban-card.is-done .task-check{background:var(--green);color:#fff}.kanban-card>p{font-size:9px;color:#87918b;margin:7px 0 13px 25px}.kanban-card footer{border-top:1px solid #edf0ed;padding-top:10px;display:flex;align-items:center;justify-content:space-between}.kanban-card footer>span:first-child{font-size:9px;color:#7f8983;display:flex;align-items:center;gap:4px}.domain-pill{color:#fff!important;font-size:8px!important;border-radius:10px;padding:3px 7px}.add-card{margin-top:9px}.habit-score-banner{display:flex;align-items:center;padding:24px 28px;background:#fff}.habit-score-banner>div:first-child{flex:1}.habit-score-banner h2{font-size:22px;margin:6px 0}.habit-score-banner p:not(.eyebrow){margin:0;color:var(--muted)}.habit-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.habit-card{padding:18px}.habit-card-top{display:flex;align-items:center;justify-content:space-between}.habit-icon.large{width:43px;height:43px;border-radius:13px}.habit-card h3{font-size:14px;margin:15px 0 4px}.habit-card>p{font-size:10px;color:#89928c;margin:0}.streak-line{display:flex;align-items:baseline;gap:5px;margin:17px 0 13px;color:#c3772b}.streak-line svg{align-self:center}.streak-line strong{font-size:21px}.streak-line span{font-size:9px;color:#8a938d}.mini-heat{display:grid;grid-template-columns:repeat(7,1fr);gap:3px}.mini-heat i{aspect-ratio:1;border-radius:2px;background:#dfeee5}.mini-heat i.high{background:#48a97a}.mini-heat i.mid{background:#99d2b5}.mini-heat i.low{background:#e8ece8}.habit-complete{margin-top:16px;width:100%;border:1px solid #dce1dd;background:#fafbf9;border-radius:9px;padding:9px;display:flex;align-items:center;justify-content:center;gap:6px;font-size:10px;cursor:pointer}.habit-complete.done{background:#e8f5ed;border-color:#c8e6d4;color:#26744f}.filter-pills{display:flex;gap:8px}.filter-pills button{border:1px solid #dce1dd;background:#fff;border-radius:20px;padding:7px 12px;font-size:10px}.filter-pills button.active{background:#1d2b24;color:#fff}.filter-pills span{opacity:.55;margin-left:4px}.projects-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.project-card{padding:21px}.project-card header{display:flex;align-items:center;gap:8px}.project-icon.large{width:46px;height:46px;border-radius:14px;font-size:16px}.project-card header .domain-pill{margin-left:auto}.project-card h2{font-size:18px;margin:18px 0 7px}.project-card>p{font-size:11px;color:#768079;line-height:1.55;min-height:50px}.project-meta{display:flex;justify-content:space-between;margin:20px 0 8px;font-size:9px;color:#7f8983}.project-card footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #edf0ed;margin-top:18px;padding-top:14px;color:#7f8983;font-size:10px}.project-card footer>span:first-child{display:flex;gap:5px;align-items:center}.avatar-stack{display:flex}.avatar-stack i{width:24px;height:24px;border-radius:50%;background:#dfe8ff;color:#315dc2;display:grid;place-items:center;border:2px solid #fff;font-size:8px;font-style:normal;font-weight:750;margin-left:-5px}.avatar-stack i:last-child{background:#e4f3e9;color:#286949}.notes-shell{display:grid;grid-template-columns:250px 1fr;min-height:650px;overflow:hidden}.notes-shell>aside{border-right:1px solid var(--line);padding:18px 11px;background:#f8f9f7}.notes-shell>aside header{display:flex;align-items:center;justify-content:space-between;padding:0 6px 13px}.notes-search{display:flex;align-items:center;gap:6px;background:#fff;border:1px solid var(--line);padding:8px;border-radius:8px;margin-bottom:10px;color:#8a938d}.notes-search input{border:0;outline:0;background:transparent;width:100%;font-size:10px}.notes-shell>aside>button{width:100%;border:0;background:transparent;border-radius:9px;padding:10px;text-align:left;display:flex;flex-direction:column;gap:4px;cursor:pointer}.notes-shell>aside>button.active{background:#e8ede9}.notes-shell>aside>button strong{font-size:10px}.notes-shell>aside>button span{font-size:8px;color:#909892}.note-editor>header{height:50px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 20px}.breadcrumb{font-size:9px;color:#8b948e;display:flex;align-items:center;gap:5px}.note-body{max-width:720px;margin:0 auto;padding:50px 55px}.note-body h1{font-size:30px;letter-spacing:-.045em;margin:11px 0 15px}.note-lede{font-size:15px!important;color:#526059!important;line-height:1.6!important}.note-body h2{font-size:17px;margin:31px 0 8px}.note-body p{color:#5f6963;line-height:1.7;font-size:12px}.note-body blockquote{margin:25px 0;background:#edf6f1;border-left:3px solid var(--green);padding:16px 18px;display:flex;gap:10px;align-items:flex-start;color:#2c5c43;font-size:12px;line-height:1.5}.note-body ul{padding:0;list-style:none}.note-body li{border-top:1px solid var(--line);padding:11px 0;font-size:11px}.note-body li span{font-size:9px;color:#98a09b;margin-right:12px}.report-hero{padding:28px;display:flex;justify-content:space-between;align-items:center;background:#19362a;color:#fff}.report-hero h2{font-size:24px;margin:6px 0}.report-hero p:not(.eyebrow){color:#b7c9bd;margin:0}.report-hero .eyebrow{color:#99c3aa}.report-hero .primary-button{background:#dff2e6;color:#1d5f42}.report-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.report-card{padding:18px;display:grid;grid-template-columns:auto 1fr;gap:12px}.report-card h3{font-size:12px;margin:2px 0}.report-card p{font-size:9px;color:#8c958f;margin:0}.report-status{grid-column:1/-1;border-top:1px solid var(--line);padding-top:12px;color:#2b8b5e;font-size:9px}.report-card .text-button{grid-column:1/-1;justify-content:flex-start}.recent-table{padding:20px}.table-row{display:grid;grid-template-columns:auto 1fr 100px 80px auto;gap:12px;align-items:center;border-top:1px solid var(--line);padding:13px 0;font-size:10px}.table-row>span:not(.file-icon){color:#828b85}.file-icon{width:30px;height:30px;background:#eef2ff;color:#4269d3;border-radius:8px;display:grid;place-items:center}.calendar-shell{overflow:hidden}.calendar-shell>header{height:65px;display:flex;justify-content:space-between;align-items:center;padding:0 18px;border-bottom:1px solid var(--line)}.calendar-shell>header>div{display:flex;align-items:center;gap:6px}.calendar-shell>header h2{font-size:16px;margin-left:10px}.calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.weekday{padding:11px;border-right:1px solid var(--line);font-size:9px;color:#7e8882;text-transform:uppercase;letter-spacing:.08em}.calendar-day{min-height:112px;border-top:1px solid var(--line);border-right:1px solid var(--line);padding:8px}.calendar-day>span{font-size:10px}.calendar-day.today>span{width:23px;height:23px;display:grid;place-items:center;background:var(--green);color:#fff;border-radius:50%}.calendar-day.muted{background:#fafbf9;color:#b5bbb7}.event{display:block;color:#fff;border-radius:4px;padding:4px 5px;margin-top:5px;font-size:8px;font-style:normal;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.analytics-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.analytics-metrics article{padding:18px;display:flex;flex-direction:column;gap:5px}.analytics-metrics small{color:#818b84}.analytics-metrics strong{font-size:24px;letter-spacing:-.04em}.analytics-metrics span{font-size:9px;color:#7d8781}.analytics-metrics .up{color:#2b8e60;display:flex;gap:3px}.analytics-grid{display:grid;grid-template-columns:1.65fr .75fr;gap:14px}.trend-card,.distribution-card{padding:21px}.area-chart{height:260px;display:flex;gap:7px;align-items:flex-end;border-bottom:1px solid var(--line);padding-top:35px}.area-chart i{flex:1;background:#dce8e0;border-radius:5px 5px 0 0;position:relative}.area-chart i:nth-child(n+7){background:#5ca781}.area-chart span{display:none}.donut{width:145px;height:145px;margin:25px auto;border-radius:50%;background:conic-gradient(#4a73f2 0 48%,#e27458 48% 79%,#2e9a6d 79%);display:grid;place-items:center}.donut:before{content:"";position:absolute;width:93px;height:93px;background:#fff;border-radius:50%}.donut>div{z-index:1;display:flex;flex-direction:column;align-items:center}.donut strong{font-size:21px}.donut span{font-size:9px;color:#848e88}.distribution-card ul{list-style:none;padding:0;margin:0}.distribution-card li{display:flex;align-items:center;gap:7px;border-top:1px solid var(--line);padding:9px 0;font-size:10px}.distribution-card li i{width:8px;height:8px;border-radius:50%}.distribution-card li strong{margin-left:auto}.agent-layout{display:grid;grid-template-columns:280px 1fr;gap:14px}.agent-summary{padding:24px;text-align:center;display:flex;align-items:center;flex-direction:column}.agent-avatar{width:58px;height:58px;border-radius:17px;background:#ece8ff;color:#6652be;display:grid;place-items:center}.agent-summary h2{margin:6px 0}.agent-summary p:not(.eyebrow){font-size:10px;color:#7e8781}.online{font-size:9px;color:#2b8d60;background:#e8f5ed;border-radius:20px;padding:5px 9px}.online i{display:inline-block;width:6px;height:6px;background:#35a86e;border-radius:50%;margin-right:4px}.agent-stats{width:100%;display:grid;grid-template-columns:repeat(3,1fr);margin:24px 0;border-block:1px solid var(--line);padding:15px 0}.agent-stats span{display:flex;flex-direction:column;font-size:8px;color:#89918c}.agent-stats strong{font-size:17px;color:var(--ink)}.activity-feed{padding:22px}.activity-row{display:flex;gap:13px;align-items:flex-start;border-top:1px solid var(--line);padding:17px 0}.activity-icon{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;flex:0 0 auto}.activity-icon.done{background:#e8f5ed;color:#2d8b60}.activity-icon.working{background:#eaf0ff;color:#356bff}.activity-icon.created{background:#fff0eb;color:#d86448}.activity-row>div{flex:1}.activity-row strong{font-size:11px}.activity-row p{font-size:10px;color:#727c76;margin:5px 0}.activity-row span{font-size:8px;color:#99a19c}.settings-layout{display:grid;grid-template-columns:220px 1fr;gap:14px}.settings-layout>aside{padding:10px;height:max-content}.settings-layout>aside button{width:100%;border:0;background:transparent;padding:10px;border-radius:8px;text-align:left;font-size:10px;color:#707a73}.settings-layout>aside button.active{background:#e9eeea;color:#1e2a23;font-weight:700}.settings-content{padding:26px}.settings-content h2{font-size:22px;margin:6px 0}.settings-content>p:not(.eyebrow){color:#78827b}.setting-row{border-top:1px solid var(--line);padding:18px 0;display:flex;align-items:center;justify-content:space-between}.setting-row>div:first-child{display:flex;flex-direction:column;gap:3px}.setting-row strong{font-size:11px}.setting-row span{font-size:9px;color:#858e88}.swatches{display:flex;gap:8px}.swatches button{width:26px;height:26px;border:3px solid #fff;border-radius:50%;box-shadow:0 0 0 1px #d3d8d4}.swatches button.active{box-shadow:0 0 0 2px #1d2b24}.swatches .blue{background:#356bff}.swatches .green{background:#2b8d60}.swatches .coral{background:#e27458}.swatches .violet{background:#795bc5}.switch{width:39px;height:22px;border:0;border-radius:15px;background:#dce1dd;padding:3px}.switch span{display:block;width:16px;height:16px;background:#fff;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,.15)}.modal-backdrop{position:fixed;inset:0;background:rgba(15,25,20,.46);backdrop-filter:blur(5px);z-index:100;display:flex;justify-content:center;align-items:flex-start;padding-top:14vh}.command-modal{width:min(580px,calc(100vw - 28px));background:#fff;border-radius:16px;box-shadow:0 26px 80px rgba(14,26,19,.3);overflow:hidden;border:1px solid #dce2dd}.command-modal>label{height:59px;display:flex;align-items:center;gap:10px;padding:0 16px;border-bottom:1px solid var(--line);color:#7a847e}.command-modal input{border:0;outline:0;flex:1;font-size:13px}.command-results{padding:9px}.command-results>p{font-size:9px;text-transform:uppercase;letter-spacing:.1em;color:#939b96;padding:3px 8px}.command-results button{width:100%;border:0;background:transparent;border-radius:8px;padding:10px;display:flex;align-items:center;gap:10px;cursor:pointer;text-align:left}.command-results button:hover{background:#eef2ee}.command-results button span{flex:1}.command-modal>footer{display:flex;gap:18px;padding:10px 16px;border-top:1px solid var(--line);background:#f8f9f7}.command-modal>footer span{font-size:9px;color:#8b948e;display:flex;gap:4px;align-items:center}.nav-scrim{display:none} -@media(max-width:1180px){.hero-grid{grid-template-columns:1.5fr repeat(2,1fr)}.hero-grid .metric:last-child{display:none}.habit-grid,.report-grid{grid-template-columns:repeat(2,1fr)}.projects-grid{grid-template-columns:repeat(2,1fr)}.analytics-metrics{grid-template-columns:repeat(2,1fr)}} -@media(max-width:900px){.sidebar{transform:translateX(-102%);transition:transform .22s}.sidebar.open{transform:translateX(0)}.workspace{margin-left:0}.mobile-menu,.close-mobile{display:grid}.nav-scrim{display:block;position:fixed;inset:0;border:0;background:rgba(12,20,15,.45);z-index:25}.topbar{padding:0 18px}.command-trigger{width:42px}.command-trigger span,.command-trigger kbd{display:none}.main-grid{grid-template-columns:1fr}.hero-grid{grid-template-columns:1.5fr 1fr}.hero-grid .metric:nth-child(4){display:none}.agent-panel{grid-column:auto}.kanban{overflow-x:auto;grid-template-columns:repeat(3,280px)}.notes-shell{grid-template-columns:210px 1fr}.analytics-grid{grid-template-columns:1fr}.agent-layout{grid-template-columns:1fr}.agent-summary{display:grid;grid-template-columns:auto 1fr auto;text-align:left;gap:10px}.agent-stats,.agent-summary .secondary-button{grid-column:1/-1}.settings-layout{grid-template-columns:180px 1fr}} -@media(max-width:640px){.topbar{height:62px}.quick-add span,.domain-select{display:none}main{padding:24px 14px 90px}.page-heading{align-items:flex-start}.page-heading h1{font-size:25px}.page-heading>button{padding:9px}.hero-grid{grid-template-columns:1fr}.focus-card{min-height:210px}.metric{display:flex!important}.hero-grid .metric:last-child{display:none!important}.main-grid{display:flex;flex-direction:column}.agent-panel{align-items:flex-start;flex-wrap:wrap}.agent-panel .secondary-button{width:100%}.habit-grid,.projects-grid,.report-grid,.analytics-metrics{grid-template-columns:1fr}.notes-shell{grid-template-columns:1fr}.notes-shell>aside{display:none}.note-body{padding:32px 22px}.view-toolbar{align-items:flex-start;gap:10px}.toolbar-actions .secondary-button{display:none}.calendar-shell{overflow-x:auto}.calendar-grid,.calendar-shell>header{min-width:760px}.projects-grid{grid-template-columns:1fr}.analytics-grid{display:block}.distribution-card{margin-top:14px}.agent-summary{display:flex;text-align:center}.settings-layout{grid-template-columns:1fr}.settings-layout>aside{display:flex;overflow:auto}.settings-layout>aside button{white-space:nowrap;width:auto}.setting-row{align-items:flex-start;gap:14px;flex-direction:column}.report-hero{align-items:flex-start;gap:20px;flex-direction:column}.table-row{grid-template-columns:auto 1fr auto}.table-row>span:nth-child(3),.table-row>span:nth-child(4){display:none}} -@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}} diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index a9b08d3..0000000 --- a/app/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Project E — Your personal operating system", - description: "Tasks, habits, projects, notes, reports, and agents in one calm workspace.", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index a2bec03..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ProjectEApp from "./project-e-app"; - -export default function Home() { - return ; -} diff --git a/app/project-e-app.tsx b/app/project-e-app.tsx deleted file mode 100644 index 33b59f9..0000000 --- a/app/project-e-app.tsx +++ /dev/null @@ -1,235 +0,0 @@ -"use client"; - -import { - Activity, - BarChart3, - Bell, - Bot, - CalendarDays, - Check, - CheckCircle2, - ChevronDown, - ChevronRight, - Circle, - Clock3, - Command, - FileBarChart, - Flame, - FolderKanban, - GripVertical, - LayoutDashboard, - ListTodo, - Menu, - MoreHorizontal, - NotebookPen, - Pause, - Play, - Plus, - Search, - Settings, - Sparkles, - Target, - TimerReset, - TrendingUp, - X, - type LucideIcon, -} from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; - -type View = "Dashboard" | "Tasks" | "Habits" | "Projects" | "Notes" | "Reports" | "Calendar" | "Analytics" | "Agent Activity" | "Settings"; -type Task = { id: number; title: string; project: string; time: string; priority: "Urgent" | "High" | "Normal"; done: boolean; status: "To do" | "In progress" | "Done"; domain: "Personal" | "Work" | "OTS" }; - -const nav: { label: View; icon: LucideIcon }[] = [ - { label: "Dashboard", icon: LayoutDashboard }, { label: "Tasks", icon: ListTodo }, - { label: "Habits", icon: Flame }, { label: "Projects", icon: FolderKanban }, - { label: "Notes", icon: NotebookPen }, { label: "Reports", icon: FileBarChart }, - { label: "Calendar", icon: CalendarDays }, { label: "Analytics", icon: BarChart3 }, - { label: "Agent Activity", icon: Bot }, { label: "Settings", icon: Settings }, -]; - -const initialTasks: Task[] = [ - { id: 1, title: "Finalize Q3 product brief", project: "Project E", time: "9:00 AM", priority: "Urgent", done: false, status: "In progress", domain: "Work" }, - { id: 2, title: "Review OTS storefront copy", project: "OTS Growth", time: "11:30 AM", priority: "High", done: false, status: "To do", domain: "OTS" }, - { id: 3, title: "30 minute strength session", project: "Personal OS", time: "4:00 PM", priority: "Normal", done: false, status: "To do", domain: "Personal" }, - { id: 4, title: "Map MCP permission model", project: "Project E", time: "Yesterday", priority: "High", done: true, status: "Done", domain: "Work" }, - { id: 5, title: "Archive June campaign assets", project: "OTS Growth", time: "Jul 15", priority: "Normal", done: false, status: "In progress", domain: "OTS" }, - { id: 6, title: "Plan long weekend itinerary", project: "Personal OS", time: "Jul 17", priority: "Normal", done: false, status: "To do", domain: "Personal" }, -]; - -const habits = [ - { id: 1, icon: "↟", name: "Morning walk", streak: 18, color: "mint", done: true }, - { id: 2, icon: "◒", name: "Read 20 pages", streak: 12, color: "blue", done: false }, - { id: 3, icon: "✦", name: "Daily reflection", streak: 7, color: "amber", done: false }, - { id: 4, icon: "⌁", name: "No screens after 10", streak: 4, color: "coral", done: false }, -]; - -const projects = [ - { name: "Project E", domain: "Work", progress: 72, due: "Sep 30", tasks: "18 / 25", color: "#356bff", icon: "E" }, - { name: "OTS Growth", domain: "OTS", progress: 54, due: "Aug 12", tasks: "13 / 24", color: "#ea7658", icon: "O" }, - { name: "Personal OS", domain: "Personal", progress: 81, due: "Ongoing", tasks: "17 / 21", color: "#299667", icon: "P" }, -]; - -const week = [ - { day: "M", value: 58 }, { day: "T", value: 78 }, { day: "W", value: 66 }, - { day: "T", value: 91 }, { day: "F", value: 74 }, { day: "S", value: 42 }, { day: "S", value: 67 }, -]; - -function Logo() { - return
EProject E
; -} - -function Progress({ value, color = "var(--green)" }: { value: number; color?: string }) { - return
; -} - -function Dashboard({ tasks, onToggleTask, habitDone, onToggleHabit, onNavigate }: { tasks: Task[]; onToggleTask: (id: number) => void; habitDone: Record; onToggleHabit: (id: number) => void; onNavigate: (view: View) => void }) { - const done = tasks.filter(t => t.done).length; - return <> -
-
-

Focus score

Good momentum.

You’re ahead of last week. One focused block will close your highest-impact task.

-
82/ 100
- -
-
Tasks complete{done + 11}/ 18 18% this week
-
Best streak18 daysMorning walk
-
Focused time14.5 hrs 2.1h vs last week
-
- -
-
-

Monday, July 13

Today’s plan

-
- {tasks.slice(0, 4).map(task =>
- -
{task.title}{task.project} · {task.time}
- {task.priority} -
)} -
- -
- -
-

Daily rhythm

Habits

{Object.values(habitDone).filter(Boolean).length}/4 done
-
- {habits.map(habit => { const isDone = habitDone[habit.id]; return })} -
-
- -
-

July 7–13

Weekly pulse

-
76%completion rate
+8% vs last week
-
{week.map((d, i) =>
{d.day}
)}
-
- -
-

Across your life

Active projects

-
{projects.map(p => )}
-
- -
-

Agent workspace

Hermes finished a task

“Project health brief” is ready to review, with 3 risks and 5 next actions.

-
-
- ; -} - -function TasksView({ tasks, onToggle }: { tasks: Task[]; onToggle: (id: number) => void }) { - return
{(["To do", "In progress", "Done"] as const).map(status =>
{status}{tasks.filter(t => t.status === status).length}
{tasks.filter(t => t.status === status).map(task =>
{task.priority}

{task.project}

{task.time}{task.domain}
)}
)}
; -} - -function HabitsView({ habitDone, onToggle }: { habitDone: Record; onToggle: (id: number) => void }) { - return

Your rhythm

Consistency is compounding

You completed 86% of scheduled habits in the past 30 days.

86score
{habits.map(h =>
{h.icon}

{h.name}

Every day · Personal

{h.streak}day streak
{Array.from({ length: 28 }).map((_, i) => 24 ? "low" : i % 5 === 0 ? "mid" : "high"} key={i} />)}
)}
; -} - -function ProjectsView() { - return
{projects.map((p, index) =>
{p.icon}{p.domain}

{p.name}

{index === 0 ? "A calm, agent-native system for every commitment." : index === 1 ? "Make the storefront and campaigns work as one growth engine." : "Build routines and systems that create more spacious days."}

{p.progress}% complete{p.tasks} tasks
{p.due}MH
)}
; -} - -function NotesView() { - const notes = ["Project E — product principles", "July operating review", "OTS campaign concepts", "Reading notes: Slow Productivity"]; - const [selected, setSelected] = useState(notes[0]); - return
Notes Work

# project-e · work · strategy

{selected}

A personal operating system should reduce cognitive weight, not become another system to maintain.

North star

Project E brings tasks, habits, projects, and knowledge into one calm surface. The model stays flexible; views do the organizing.

Design for the moment a person returns after a chaotic week. The system should make the next step obvious.

Working principles

  • 01 Three equal pillars, never one buried inside another.
  • 02 Capture quickly, organize progressively.
  • 03 External agents operate through explicit permissions.
; -} - -function ReportsView() { - const items = [{ icon: BarChart3, title: "Weekly Summary", sub: "Jul 7–13", color: "blue" }, { icon: CalendarDays, title: "Monthly Review", sub: "June 2026", color: "mint" }, { icon: Target, title: "Project Health", sub: "Project E", color: "coral" }, { icon: Flame, title: "Habit Analysis", sub: "Last 30 days", color: "amber" }]; - return

Reports

Turn activity into perspective.

Generate a structured review from your actual work, habits, and time.

{items.map(({ icon: Icon, ...r }) =>

{r.title}

{r.sub}

Ready
)}

Recent reports

{["Project E weekly health", "June time audit", "OTS campaign retrospective"].map((name, i) =>
{name}{i === 0 ? "Weekly" : i === 1 ? "Time audit" : "Custom"}Jul {13 - i * 4}
)}
; -} - -function CalendarView() { - const days = Array.from({ length: 35 }, (_, i) => i - 1); - return

July 2026

{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => {d})}{days.map((d, i) =>
31 ? "muted" : ""}`} key={i}>{d < 1 ? 29 + d : d > 31 ? d - 31 : d}{d === 13 && <>9:00 Product brief11:30 OTS copy4:00 Strength}{d === 15 && Project milestone}{d === 17 && Trip planning}
)}
; -} - -function AnalyticsView() { - return
Completion rate76% 8.2%
Habit consistency86% 4.1%
Deep work14.5h 2.1h
Active streaks4Best: 18 days

12 week view

Productivity trend

{[35, 47, 42, 58, 54, 69, 63, 77, 72, 88, 80, 92].map((v, i) => {v})}

Time distribution

Where time went

14.5htracked
  • Work 48%
  • OTS 31%
  • Personal 21%
; -} - -function AgentView() { - const events = [{ type: "done", icon: Check, title: "Completed “Project health brief”", agent: "Hermes", time: "12 minutes ago", text: "Created a report with 3 risks, 5 actions, and a milestone forecast." }, { type: "working", icon: Activity, title: "Analyzing OTS campaign performance", agent: "Hermes", time: "Started 28 minutes ago", text: "Reviewing 14 tasks and 3 linked notes." }, { type: "created", icon: Plus, title: "Created note “July research synthesis”", agent: "Claude", time: "Yesterday, 4:18 PM", text: "Generated from 8 source notes with backlinks preserved." }]; - return

Active agent

Hermes

Planning and analysis partner · Full access

Online
24actions8tasks4reports

Audit trail

Agent activity

{events.map(({ icon: Icon, ...event }) =>
{event.title}

{event.text}

{event.agent} · {event.time}
)}
; -} - -function SettingsView() { - return

Preferences

Appearance

Make Project E feel right for the way you work.

Color modeUse a light or dark interface.
Accent colorUsed for actions and progress.
DensityControl information spacing.
Reduced motionMinimize interface animation.
; -} - -function CommandPalette({ onClose, onNavigate }: { onClose: () => void; onNavigate: (v: View) => void }) { - const [query, setQuery] = useState(""); - const results = nav.filter(n => n.label.toLowerCase().includes(query.toLowerCase())); - return
e.stopPropagation()}>

Jump to

{results.map(({ label, icon: Icon }) => )}
navigate open
; -} - -export default function ProjectEApp() { - const [view, setView] = useState("Dashboard"); - const [tasks, setTasks] = useState(initialTasks); - const [habitDone, setHabitDone] = useState>({ 1: true, 2: false, 3: false, 4: false }); - const [domain, setDomain] = useState("All domains"); - const [commandOpen, setCommandOpen] = useState(false); - const [mobileNav, setMobileNav] = useState(false); - const [timerActive, setTimerActive] = useState(false); - - useEffect(() => { - const onKey = (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); setCommandOpen(true); } - if (event.key === "Escape") { setCommandOpen(false); setMobileNav(false); } - }; - window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, []); - - const filteredTasks = useMemo(() => domain === "All domains" ? tasks : tasks.filter(t => t.domain === domain), [tasks, domain]); - const toggleTask = (id: number) => setTasks(current => current.map(t => t.id === id ? { ...t, done: !t.done, status: !t.done ? "Done" : "To do" } : t)); - const navigate = (next: View) => { setView(next); setMobileNav(false); window.scrollTo({ top: 0, behavior: "smooth" }); }; - - let content; - if (view === "Dashboard") content = setHabitDone(v => ({ ...v, [id]: !v[id] }))} onNavigate={navigate} />; - else if (view === "Tasks") content = ; - else if (view === "Habits") content = setHabitDone(v => ({ ...v, [id]: !v[id] }))} />; - else if (view === "Projects") content = ; - else if (view === "Notes") content = ; - else if (view === "Reports") content = ; - else if (view === "Calendar") content = ; - else if (view === "Analytics") content = ; - else if (view === "Agent Activity") content = ; - else content = ; - - return
- Skip to content - - {mobileNav &&
-

{view === "Dashboard" ? "Your day, at a glance" : "Project E"}

{view === "Dashboard" ? "Good morning, Matt." : view}

{view === "Dashboard" ? "Monday, July 13 · You have 3 priorities and 4 habits today." : view === "Tasks" ? "Move work forward without losing the thread." : view === "Habits" ? "Small actions, visible momentum." : view === "Projects" ? "Every outcome has a home." : view === "Notes" ? "Connect ideas to the work they shape." : view === "Reports" ? "Step back and see what changed." : view === "Calendar" ? "Your commitments, in time." : view === "Analytics" ? "Patterns behind your progress." : view === "Agent Activity" ? "Every agent action, visible and reversible." : "Tune Project E to fit your work."}

{view !== "Dashboard" && view !== "Settings" && }
{content}
-
- {commandOpen && setCommandOpen(false)} onNavigate={navigate} />} - ; -} diff --git a/apps/web/app/(dashboard)/calendar/page.tsx b/apps/web/app/(dashboard)/calendar/page.tsx index c7a521c..297ecf5 100644 --- a/apps/web/app/(dashboard)/calendar/page.tsx +++ b/apps/web/app/(dashboard)/calendar/page.tsx @@ -1,15 +1,16 @@ 'use client'; -import { useEffect, useState, useMemo, Suspense } from 'react'; -import { Filter } from 'lucide-react'; +import { useEffect, useState, useMemo, useCallback, Suspense } from 'react'; +import { Filter, ChevronLeft, ChevronRight, CalendarDays, Calendar as CalendarIcon } from 'lucide-react'; import dynamic from 'next/dynamic'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; +import { useRouter } from 'next/navigation'; -// Lazy load react-big-calendar (~60KB + date-fns) +// Lazy load react-big-calendar const BigCalendar = dynamic( () => import('@/components/calendar/big-calendar-wrapper').then((m) => m.BigCalendarWrapper), { @@ -27,24 +28,48 @@ interface CalendarEvent { title: string; start: Date; end: Date; - type: 'task' | 'project' | 'milestone'; - domain: string; + type: 'task' | 'habit' | 'project' | 'milestone'; + entityType: string; + entityId: string; color: string; + domainId: string; href: string; + priority?: string; + difficulty?: string; + status?: string; } export default function CalendarPage() { + const router = useRouter(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [currentDate, setCurrentDate] = useState(new Date()); + const [view, setView] = useState<'month' | 'week' | 'day'>('month'); const [showTasks, setShowTasks] = useState(true); + const [showHabits, setShowHabits] = useState(true); const [showProjects, setShowProjects] = useState(true); const [showMilestones, setShowMilestones] = useState(true); const [selectedDomains, setSelectedDomains] = useState([]); - const [domainOptions, setDomainOptions] = useState<{id: string; name: string}[]>([]); + const [domainOptions, setDomainOptions] = useState<{id: string; name: string; color: string | null}[]>([]); + const [currentDomainId, setCurrentDomainId] = useState(null); + // Auto-switch to day view on mobile useEffect(() => { - fetchEvents(); + const checkWidth = () => { + if (window.innerWidth < 640 && view !== 'day') { + setView('day'); + } + }; + checkWidth(); + window.addEventListener('resize', checkWidth); + return () => window.removeEventListener('resize', checkWidth); + }, []); + + // Get current domain from URL or default + useEffect(() => { + const pathParts = window.location.pathname.split('/'); + // Try to find domain from sidebar or use first domain fetchDomains(); }, []); @@ -54,136 +79,182 @@ export default function CalendarPage() { if (res.ok) { const data = await res.json(); setDomainOptions(data.items || []); + if (data.items?.length > 0 && !currentDomainId) { + setCurrentDomainId(data.items[0].id); + } } } catch {} } - async function fetchEvents() { + const fetchEvents = useCallback(async (domainId: string, from: Date, to: Date) => { + if (!domainId) return; setLoading(true); setError(null); try { - const [tasksResponse, projectsResponse, milestonesResponse] = await Promise.all([ - fetch('/api/tasks?perPage=500'), - fetch('/api/projects?perPage=500'), - fetch('/api/milestones?perPage=500'), - ]); - - if (!tasksResponse.ok || !projectsResponse.ok || !milestonesResponse.ok) { - throw new Error('One or more calendar sources could not be loaded.'); - } - - const [tasksData, projectsData, milestonesData] = await Promise.all([ - tasksResponse.json(), - projectsResponse.json(), - milestonesResponse.json(), - ]); - - const calendarEvents: CalendarEvent[] = []; - - // Add tasks - if (tasksData.items) { - for (const task of tasksData.items) { - if (task.due_date) { - const date = new Date(task.due_date); - calendarEvents.push({ - id: `task-${task.id}`, - title: task.title, - start: date, - end: date, - type: 'task', - domain: task.domain ?? 'personal', - color: '#3b82f6', - href: '/tasks', - }); - } - } - } - - // Add projects - if (projectsData.items) { - for (const project of projectsData.items) { - if (project.due_date) { - const date = new Date(project.due_date); - calendarEvents.push({ - id: `project-${project.id}`, - title: project.name, - start: date, - end: date, - type: 'project', - domain: project.domain ?? 'personal', - color: '#8b5cf6', - href: `/projects/${project.id}`, - }); - } - } - } - - // Add milestones - if (milestonesData.items) { - for (const milestone of milestonesData.items) { - if (milestone.due_date) { - const date = new Date(milestone.due_date); - calendarEvents.push({ - id: `milestone-${milestone.id}`, - title: milestone.name || milestone.title, - start: date, - end: date, - type: 'milestone', - domain: milestone.domain ?? 'work', - color: '#f59e0b', - href: milestone.project_id ? `/projects/${milestone.project_id}` : '/projects', - }); - } - } - } - + const params = new URLSearchParams({ + from: from.toISOString(), + to: to.toISOString(), + types: ['task', 'habit', 'project', 'milestone'].join(','), + }); + const res = await fetch(`/api/domains/${domainId}/calendar/events?${params}`); + if (!res.ok) throw new Error('Failed to fetch events'); + const data = await res.json(); + const calendarEvents: CalendarEvent[] = (data.events || []).map((e: any) => ({ + ...e, + start: new Date(e.start), + end: new Date(e.end), + })); setEvents(calendarEvents); - } catch (error) { - console.error('Failed to fetch calendar events:', error); + } catch (err) { + console.error('Failed to fetch calendar events:', err); setError('Calendar events could not be loaded. Please try again.'); } finally { setLoading(false); } + }, []); + + // Fetch events when domain or date range changes + useEffect(() => { + if (!currentDomainId) return; + const range = getViewRange(currentDate, view); + fetchEvents(currentDomainId, range.from, range.to); + }, [currentDomainId, currentDate, view, fetchEvents]); + + // Calendar keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + if ( + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.tagName === 'SELECT' || + target.isContentEditable + ) { + return; + } + + switch (e.key.toLowerCase()) { + case 't': + navigate('today'); + e.preventDefault(); + break; + case 'm': + setView('month'); + e.preventDefault(); + break; + case 'w': + setView('week'); + e.preventDefault(); + break; + case 'd': + setView('day'); + e.preventDefault(); + break; + case 'arrowleft': + navigate('prev'); + e.preventDefault(); + break; + case 'arrowright': + navigate('next'); + e.preventDefault(); + break; + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [currentDate, view]); + + function getViewRange(date: Date, v: string): { from: Date; to: Date } { + const from = new Date(date); + const to = new Date(date); + switch (v) { + case 'month': + from.setDate(1); + from.setHours(0, 0, 0, 0); + to.setMonth(to.getMonth() + 1, 0); + to.setHours(23, 59, 59, 999); + // Add buffer for week overlap + from.setDate(from.getDate() - 7); + to.setDate(to.getDate() + 7); + break; + case 'week': { + const day = from.getDay(); + from.setDate(from.getDate() - day); + from.setHours(0, 0, 0, 0); + to.setDate(to.getDate() + (6 - day)); + to.setHours(23, 59, 59, 999); + break; + } + case 'day': + from.setHours(0, 0, 0, 0); + to.setHours(23, 59, 59, 999); + break; + } + return { from, to }; } + const navigate = (direction: 'prev' | 'next' | 'today') => { + const d = new Date(currentDate); + switch (direction) { + case 'prev': + if (view === 'month') d.setMonth(d.getMonth() - 1); + else if (view === 'week') d.setDate(d.getDate() - 7); + else d.setDate(d.getDate() - 1); + break; + case 'next': + if (view === 'month') d.setMonth(d.getMonth() + 1); + else if (view === 'week') d.setDate(d.getDate() + 7); + else d.setDate(d.getDate() + 1); + break; + case 'today': + d.setTime(Date.now()); + break; + } + setCurrentDate(d); + }; + + const handleEventDrop = async (event: CalendarEvent, newStart: Date) => { + if (event.entityType !== 'task') return; + try { + const res = await fetch(`/api/domains/${currentDomainId}/tasks/${event.entityId}/schedule`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dueDate: newStart.toISOString() }), + }); + if (!res.ok) throw new Error('Failed to reschedule'); + // Refresh events + const range = getViewRange(currentDate, view); + fetchEvents(currentDomainId!, range.from, range.to); + } catch (err) { + console.error('Failed to reschedule task:', err); + } + }; + const filteredEvents = useMemo(() => { return events.filter((event) => { - // Filter by type if (event.type === 'task' && !showTasks) return false; + if (event.type === 'habit' && !showHabits) return false; if (event.type === 'project' && !showProjects) return false; if (event.type === 'milestone' && !showMilestones) return false; - - // Filter by domain - if (selectedDomains.length > 0 && !selectedDomains.includes(event.domain)) { - return false; - } - + if (selectedDomains.length > 0 && !selectedDomains.includes(event.domainId)) return false; return true; }); - }, [events, showTasks, showProjects, showMilestones, selectedDomains]); + }, [events, showTasks, showHabits, showProjects, showMilestones, selectedDomains]); - function toggleDomain(domain: string) { + const toggleDomain = (domainId: string) => { setSelectedDomains((prev) => - prev.includes(domain) ? prev.filter((d) => d !== domain) : [...prev, domain] + prev.includes(domainId) ? prev.filter((d) => d !== domainId) : [...prev, domainId] ); - } + }; - if (loading) { - return ( -
-

Loading calendar...

-
- ); - } - - if (error) { - return ( -
-

{error}

- -
- ); - } + const formatTitle = () => { + const opts: Intl.DateTimeFormatOptions = {}; + if (view === 'month') { opts.month = 'long'; opts.year = 'numeric'; } + else if (view === 'week') { opts.month = 'short'; opts.day = 'numeric'; } + else { opts.weekday = 'long'; opts.month = 'long'; opts.day = 'numeric'; opts.year = 'numeric'; } + return currentDate.toLocaleDateString('en-US', opts); + }; return (
@@ -194,6 +265,36 @@ export default function CalendarPage() {
+ {/* Toolbar */} +
+ +
+ + +
+

{formatTitle()}

+
+ {(['month', 'week', 'day'] as const).map((v) => ( + + ))} +
+
+
{/* Filters sidebar */} @@ -206,36 +307,31 @@ export default function CalendarPage() { {/* Entity types */}
-

Show

+

Show

- setShowTasks(checked === true)} - /> + setShowTasks(c === true)} />