` and animate the wrapper instead.
**Incorrect (animating SVG directly - no hardware acceleration):**
```tsx
function LoadingSpinner() {
return (
)
}
```
**Correct (animating wrapper div - hardware accelerated):**
```tsx
function LoadingSpinner() {
return (
)
}
```
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-conditional-render.md
================================================
---
title: Use Explicit Conditional Rendering
impact: LOW
impactDescription: prevents rendering 0 or NaN
tags: rendering, conditional, jsx, falsy-values
---
## Use Explicit Conditional Rendering
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
**Incorrect (renders "0" when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
{count && {count} }
)
}
// When count = 0, renders:
0
// When count = 5, renders:
5
```
**Correct (renders nothing when count is 0):**
```tsx
function Badge({ count }: { count: number }) {
return (
{count > 0 ? {count} : null}
)
}
// When count = 0, renders:
// When count = 5, renders:
5
```
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-content-visibility.md
================================================
---
title: CSS content-visibility for Long Lists
impact: HIGH
impactDescription: faster initial render
tags: rendering, css, content-visibility, long-lists
---
## CSS content-visibility for Long Lists
Apply `content-visibility: auto` to defer off-screen rendering.
**CSS:**
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
**Example:**
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
{messages.map(msg => (
))}
)
}
```
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-hoist-jsx.md
================================================
---
title: Hoist Static JSX Elements
impact: LOW
impactDescription: avoids re-creation
tags: rendering, jsx, static, optimization
---
## Hoist Static JSX Elements
Extract static JSX outside components to avoid re-creation.
**Incorrect (recreates element every render):**
```tsx
function LoadingSkeleton() {
return
}
function Container() {
return (
{loading && }
)
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = (
)
function Container() {
return (
{loading && loadingSkeleton}
)
}
```
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md
================================================
---
title: Prevent Hydration Mismatch Without Flickering
impact: MEDIUM
impactDescription: avoids visual flicker and hydration errors
tags: rendering, ssr, hydration, localStorage, flicker
---
## Prevent Hydration Mismatch Without Flickering
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
**Incorrect (breaks SSR):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
return (
{children}
)
}
```
Server-side rendering will fail because `localStorage` is undefined.
**Incorrect (visual flickering):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
if (stored) {
setTheme(stored)
}
}, [])
return (
{children}
)
}
```
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
**Correct (no flicker, no hydration mismatch):**
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
{children}
>
)
}
```
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
================================================
FILE: .claude/skills/react-best-practices/rules/rendering-svg-precision.md
================================================
---
title: Optimize SVG Precision
impact: LOW
impactDescription: reduces file size
tags: rendering, svg, optimization, svgo
---
## Optimize SVG Precision
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
**Incorrect (excessive precision):**
```svg
```
**Correct (1 decimal place):**
```svg
```
**Automate with SVGO:**
```bash
npx svgo --precision=1 --multipass icon.svg
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-defer-reads.md
================================================
---
title: Defer State Reads to Usage Point
impact: MEDIUM
impactDescription: avoids unnecessary subscriptions
tags: rerender, searchParams, localStorage, optimization
---
## Defer State Reads to Usage Point
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect (subscribes to all searchParams changes):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
return
Share
}
```
**Correct (reads on demand, no subscription):**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
return
Share
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-dependencies.md
================================================
---
title: Narrow Effect Dependencies
impact: LOW
impactDescription: minimizes effect re-runs
tags: rerender, useEffect, dependencies, optimization
---
## Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect (re-runs on any user field change):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
}
}, [width])
// Correct: runs only on boolean transition
const isMobile = width < 768
useEffect(() => {
if (isMobile) {
enableMobileMode()
}
}, [isMobile])
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-derived-state.md
================================================
---
title: Subscribe to Derived State
impact: MEDIUM
impactDescription: reduces re-render frequency
tags: rerender, derived-state, media-query, optimization
---
## Subscribe to Derived State
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect (re-renders on every pixel change):**
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return
}
```
**Correct (re-renders only when boolean changes):**
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-functional-setstate.md
================================================
---
title: Use Functional setState Updates
impact: MEDIUM
impactDescription: prevents stale closures and unnecessary callback recreations
tags: react, hooks, useState, useCallback, callbacks, closures
---
## Use Functional setState Updates
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
**Incorrect (requires state as dependency):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
return
}
```
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
**Correct (stable callbacks, no stale closures):**
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
return
}
```
**Benefits:**
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2. **No stale closures** - Always operates on the latest state value
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
**When to use functional updates:**
- Any setState that depends on the current state value
- Inside useCallback/useMemo when state is needed
- Event handlers that reference state
- Async operations that update state
**When direct updates are fine:**
- Setting state to a static value: `setCount(0)`
- Setting state from props/arguments only: `setName(newName)`
- State doesn't depend on previous value
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-lazy-state-init.md
================================================
---
title: Use Lazy State Initialization
impact: MEDIUM
impactDescription: wasted computation on every render
tags: react, hooks, useState, performance, initialization
---
## Use Lazy State Initialization
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
**Incorrect (runs on every render):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
// When query changes, buildSearchIndex runs again unnecessarily
return
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
return
}
```
**Correct (runs only once):**
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
return
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
return
}
```
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-memo.md
================================================
---
title: Extract to Memoized Components
impact: MEDIUM
impactDescription: enables early returns
tags: rerender, memo, useMemo, optimization
---
## Extract to Memoized Components
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect (computes avatar even when loading):**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return
}, [user])
if (loading) return
return {avatar}
}
```
**Correct (skips computation when loading):**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return
})
function Profile({ user, loading }: Props) {
if (loading) return
return (
)
}
```
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
================================================
FILE: .claude/skills/react-best-practices/rules/rerender-transitions.md
================================================
---
title: Use Transitions for Non-Urgent Updates
impact: MEDIUM
impactDescription: maintains UI responsiveness
tags: rerender, transitions, startTransition, performance
---
## Use Transitions for Non-Urgent Updates
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
**Incorrect (blocks UI on every scroll):**
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/server-after-nonblocking.md
================================================
---
title: Use after() for Non-Blocking Operations
impact: MEDIUM
impactDescription: faster response times
tags: server, async, logging, analytics, side-effects
---
## Use after() for Non-Blocking Operations
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
**Incorrect (blocks response):**
```tsx
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
**Correct (non-blocking):**
```tsx
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
```
The response is sent immediately while logging happens in the background.
**Common use cases:**
- Analytics tracking
- Audit logging
- Sending notifications
- Cache invalidation
- Cleanup tasks
**Important notes:**
- `after()` runs even if the response fails or redirects
- Works in Server Actions, Route Handlers, and Server Components
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
================================================
FILE: .claude/skills/react-best-practices/rules/server-cache-lru.md
================================================
---
title: Cross-Request LRU Caching
impact: HIGH
impactDescription: caches across requests
tags: server, cache, lru, cross-request
---
## Cross-Request LRU Caching
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
const cache = new LRUCache({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
================================================
FILE: .claude/skills/react-best-practices/rules/server-cache-react.md
================================================
---
title: Per-Request Deduplication with React.cache()
impact: MEDIUM
impactDescription: deduplicates within request
tags: server, cache, react-cache, deduplication
---
## Per-Request Deduplication with React.cache()
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```typescript
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({
where: { id: session.user.id }
})
})
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
================================================
FILE: .claude/skills/react-best-practices/rules/server-parallel-fetching.md
================================================
---
title: Parallel Data Fetching with Component Composition
impact: CRITICAL
impactDescription: eliminates server-side waterfalls
tags: server, rsc, parallel-fetching, composition
---
## Parallel Data Fetching with Component Composition
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
**Incorrect (Sidebar waits for Page's fetch to complete):**
```tsx
export default async function Page() {
const header = await fetchHeader()
return (
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return {items.map(renderItem)}
}
```
**Correct (both fetch simultaneously):**
```tsx
async function Header() {
const data = await fetchHeader()
return {data}
}
async function Sidebar() {
const items = await fetchSidebarItems()
return {items.map(renderItem)}
}
export default function Page() {
return (
)
}
```
**Alternative with children prop:**
```tsx
async function Layout({ children }: { children: ReactNode }) {
const header = await fetchHeader()
return (
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return {items.map(renderItem)}
}
export default function Page() {
return (
)
}
```
================================================
FILE: .claude/skills/react-best-practices/rules/server-serialization.md
================================================
---
title: Minimize Serialization at RSC Boundaries
impact: HIGH
impactDescription: reduces data transfer size
tags: server, rsc, serialization, props
---
## Minimize Serialization at RSC Boundaries
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
**Incorrect (serializes all 50 fields):**
```tsx
async function Page() {
const user = await fetchUser() // 50 fields
return
}
'use client'
function Profile({ user }: { user: User }) {
return {user.name}
// uses 1 field
}
```
**Correct (serializes only 1 field):**
```tsx
async function Page() {
const user = await fetchUser()
return
}
'use client'
function Profile({ name }: { name: string }) {
return {name}
}
```
================================================
FILE: .dockerignore
================================================
node_modules
.next
dist
.turbo
.git
*.log
.env*
!.env.template
================================================
FILE: .github/FUNDING.yml
================================================
github: ridafkih
================================================
FILE: .github/workflows/checks.yml
================================================
name: Check Code Standards
on:
pull_request:
branches: [main]
jobs:
types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run types
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run lint
unused:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run unused
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run test
================================================
FILE: .github/workflows/docker-publish.yml
================================================
name: Publish Docker Images
on:
push:
tags:
- "v*.*.*"
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
jobs:
build-core:
strategy:
matrix:
arch: [amd64, arm64]
package:
- name: api
dockerfile: services/api/Dockerfile
- name: cron
dockerfile: services/cron/Dockerfile
- name: worker
dockerfile: services/worker/Dockerfile
- name: mcp
dockerfile: services/mcp/Dockerfile
- name: web
dockerfile: applications/web/Dockerfile
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package.name }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
flavor: suffix=-${{ matrix.arch }}
- uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.package.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.package.name }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.package.name }}-${{ matrix.arch }}
build-convenience:
strategy:
matrix:
package: [standalone, services]
arch: [amd64, arm64]
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
flavor: suffix=-${{ matrix.arch }}
- uses: docker/build-push-action@v6
with:
context: .
file: docker/${{ matrix.package }}/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.package }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.package }}-${{ matrix.arch }}
merge-core-manifests:
runs-on: ubuntu-latest
needs: build-core
permissions:
contents: read
packages: write
strategy:
matrix:
package:
- api
- cron
- worker
- mcp
- web
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
- name: Create multi-arch manifest
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
for tag in $TAGS; do
docker buildx imagetools create -t "$tag" \
"${tag}-amd64" \
"${tag}-arm64"
done
merge-convenience-manifests:
runs-on: ubuntu-latest
needs: build-convenience
permissions:
contents: read
packages: write
strategy:
matrix:
package: [standalone, services]
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/keeper-${{ matrix.package }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
- name: Create multi-arch manifest
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
for tag in $TAGS; do
docker buildx imagetools create -t "$tag" \
"${tag}-amd64" \
"${tag}-arm64"
done
deploy-ec2:
runs-on: ubuntu-latest
needs: merge-core-manifests
if: ${{ !contains(github.ref, '-') }}
concurrency:
group: production-deploy
cancel-in-progress: false
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
source: "deploy/compose.yaml,deploy/Caddyfile,docker/caddy/Dockerfile"
target: /home/ubuntu
- uses: appleboy/ssh-action@v1.2.4
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
set -euo pipefail
exec 9>/tmp/keeper-deploy.lock
flock 9
cd /home/ubuntu
cd deploy
docker compose pull
docker compose up -d --build --remove-orphans --wait
docker image prune -af --filter "until=24h"
================================================
FILE: .gitignore
================================================
node_modules
.env
.idea
.DS_Store
.turbo/
dist/
*.tsbuildinfo
screenshots/
applications/mobile/
.pki/
================================================
FILE: .oxlintrc.json
================================================
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "typescript", "unicorn", "promise"],
"env": {
"browser": true,
"node": true,
"es2024": true
},
"globals": {
"Bun": "readonly"
},
"categories": {
"correctness": "error",
"suspicious": "error",
"pedantic": "error",
"perf": "error",
"style": "error",
"restriction": "error"
},
"rules": {
"eqeqeq": ["error", "always"],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"import/no-cycle": "error",
"import/no-self-import": "error",
"import/no-named-export": "off",
"import/prefer-default-export": "off",
"import/max-dependencies": "off",
"import/no-anonymous-default-export": "off",
"import/consistent-type-specifier-style": "off",
"import/no-duplicates": "off",
"import/no-nodejs-modules": "off",
"import/no-relative-parent-imports": "off",
"unicorn/no-null": "off",
"unicorn/prefer-global-this": "off",
"unicorn/prefer-top-level-await": "off",
"unicorn/no-process-exit": "off",
"react/jsx-max-depth": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": "off",
"react/no-unescaped-entities": "off",
"react/jsx-props-no-spreading": "off",
"promise/avoid-new": "off",
"promise/prefer-await-to-then": "off",
"promise/prefer-await-to-callbacks": "off",
"no-duplicate-imports": "off",
"no-default-export": "off",
"no-await-in-loop": "off",
"no-plusplus": "off",
"no-continue": "off",
"no-magic-numbers": "off",
"no-warning-comments": "off",
"no-eq-null": "off",
"max-lines-per-function": "off",
"max-statements": "off",
"max-lines": "off",
"max-params": "off",
"max-classes-per-file": "off",
"sort-imports": "off",
"sort-keys": "off",
"id-length": ["error", { "exceptions": ["x", "y"] }]
},
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/.next/**",
"**/.turbo/**",
"**/coverage/**"
]
}
================================================
FILE: Caddyfile
================================================
keeper.localhost {
tls internal
reverse_proxy host.docker.internal:5173
}
:80 {
reverse_proxy host.docker.internal:5173
}
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
.
================================================
FILE: NOTICE
================================================
Copyright (C) 2025 Rida F'kih
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
================================================
FILE: README.md
================================================

# About
Keeper is a simple & open-source calendar syncing tool. It allows you to pull events from remotely hosted iCal or ICS links, and push them to one or many calendars so the time slots can align across them all.
# Features
- Aggregating calendar events from remote sources
- Event content agnostic syncing engine
- Push aggregate events to one or more calendars
- MCP (Model Context Protocol) server for AI agent calendar access
- Open source under AGPL-3.0
- Easy to self-host
- Easy-to-purge remote events
# Bug Reports & Feature Requests
If you encounter a bug or have an idea for a feature, you may [open an issue on GitHub](https://github.com/ridafkih/keeper.sh/issues) and it will be triaged and addressed as soon as possible.
# Contributing
High-value and high-quality contributions are appreciated. Before working on large features you intend to see merged, please open an issue first to discuss beforehand.
## Local Development
The dev environment runs behind HTTPS at `https://keeper.localhost` using a [Caddy](https://caddyserver.com/) reverse proxy with automatic TLS. The `.localhost` TLD resolves to `127.0.0.1` automatically per [RFC 6761](https://datatracker.ietf.org/doc/html/rfc6761) — no `/etc/hosts` entry is needed.
### Prerequisites
- [Bun](https://bun.sh/) (v1.3.11+)
- [Docker](https://docs.docker.com/get-started/) & Docker Compose
### Getting Started
```bash
bun install
```
#### Generate and Trust a Root CA
The dev environment runs behind HTTPS via Caddy. You need to generate a local root certificate authority and trust it so your browser accepts the certificate.
```bash
mkdir -p .pki
openssl req -x509 -new -nodes \
-newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout .pki/root.key -out .pki/root.crt \
-days 3650 -subj "/CN=Keeper.sh CA"
```
Then trust it on your platform:
**macOS**
```bash
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain .pki/root.crt
```
**Linux**
```bash
sudo cp .pki/root.crt /usr/local/share/ca-certificates/keeper-dev-root.crt
sudo update-ca-certificates
```
#### Start the Dev Environment
```bash
bun dev
```
This starts PostgreSQL, Redis, and a Caddy reverse proxy via Docker Compose, along with the API, web, MCP, and cron services locally. Once running, open `https://keeper.localhost`.
### Architecture
| Service | Local Port | Accessed Via |
| -------- | ---------- | ------------------------------------ |
| Caddy | 443 | `https://keeper.localhost` |
| Web | 5173 | Proxied by Caddy |
| API | 3000 | Proxied by Web at `/api` |
| MCP | 3001 | Proxied by Web at `/mcp` |
| Postgres | 5432 | `postgresql://postgres:postgres@localhost:5432/postgres` |
| Redis | 6379 | `redis://localhost:6379` |
# Qs
## Why does this exist?
Because I needed it. Ever since starting [Sedna](https://sedna.sh/)—the AI governance platform—I've had to work across three calendars. One for my business, one for work, and one for personal.
Meetings have landed on top of one-another a frustratingly high number of times.
## Why not use _this other service_?
I've probably tried it. It was probably too finicky, ended up making me waste hours of my time having to delete stale events it didn't seem to want to track anymore, or just didn't sync reliably.
## How does the syncing engine work?
- If we have a local event but no corresponding "source → destination" mapping for an event, we push the event to the destination calendar.
- If we have a mapping for an event, but the source ID is not present on the source any longer, we delete the event from the destination.
- Any events with markers of having been created by Keeper, but with no corresponding local tracking, we remove it. This is only done for backwards compatibility.
Events are flagged as having been created by Keeper either using a `@keeper.sh` suffix on the remote UID, or in the case of a platform like Outlook that doesn't support custom UIDs, we just put it in a `"keeper.sh"` category.
# Cloud Hosted
I've made Keeper easy to self-host, but whether you simply want to support the project or don't want to deal with the hassle or overhead of configuring and running your own infrastructure cloud hosting is always an option.
Head to [keeper.sh](https://keeper.sh) to get started with the cloud-hosted version. Use code `README` for 25% off.
| | Free | Pro (Cloud-Hosted) | Pro (Self-Hosted) |
| --------------------- | ---------- | ------------------ | ----------------- |
| **Monthly Price** | $0 USD | $5 USD | $0 |
| **Annual Price** | $0 USD | $42 USD (-30%) | $0 |
| **Refresh Interval** | 30 minutes | 1 minute | 1 minute |
| **Source Limit** | 2 | ∞ | ∞ |
| **Destination Limit** | 1 | ∞ | ∞ |
# Self Hosted
By hosting Keeper yourself, you get all premium features for free, can guarantee data governance and autonomy, and it's fun. If you'll be self-hosting, please consider supporting me and development of the project by sponsoring me on GitHub.
There are seven images currently available, two of them are designed for convenience, while the five are designed to serve the granular underlying services.
> [!NOTE]
>
> **Migrating from a previous version?** If you are upgrading from the older Next.js-based release, see the [migration guide](https://github.com/ridafkih/keeper.sh/issues/140) for environment variable changes. The new web server will also print a migration notice at startup if it detects old environment variables.
## Environment Variables
| Name | Service(s) | Description |
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DATABASE_URL | `api`, `cron`, `worker`, `mcp` | PostgreSQL connection URL. e.g. `postgres://user:pass@postgres:5432/keeper` |
| REDIS_URL | `api`, `cron`, `worker` | Redis connection URL. Must be the same Redis instance across all services. e.g. `redis://redis:6379` |
| WORKER_JOB_QUEUE_ENABLED | `cron` | Required. Set to `true` to enqueue sync jobs to the worker queue, or `false` to disable. If unset, the cron service will exit with a migration notice. |
| BETTER_AUTH_URL | `api`, `mcp` | The base URL used for auth redirects. e.g. `http://localhost:3000` |
| BETTER_AUTH_SECRET | `api`, `mcp` | Secret key for session signing. e.g. `openssl rand -base64 32` |
| API_PORT | `api` | Port the Bun API listens on. Defaults to `3001` in container images. |
| ENV | `web` | Optional. Runtime environment. One of `development`, `production`, or `test`. Defaults to `production`. |
| PORT | `web` | Port the web server listens on. Defaults to `3000` in container images. |
| VITE_API_URL | `web` | The URL the web server uses to proxy requests to the Bun API. e.g. `http://api:3001` |
| COMMERCIAL_MODE | `api`, `cron` | Enable Polar billing flow. Set to `true` if using Polar for subscriptions. |
| POLAR_ACCESS_TOKEN | `api`, `cron` | Optional. Polar API token for subscription management. |
| POLAR_MODE | `api`, `cron` | Optional. Polar environment, `sandbox` or `production`. |
| POLAR_WEBHOOK_SECRET | `api` | Optional. Secret to verify Polar webhooks. |
| ENCRYPTION_KEY | `api`, `cron`, `worker` | Key for encrypting CalDAV credentials at rest. e.g. `openssl rand -base64 32` |
| RESEND_API_KEY | `api` | Optional. API key for sending emails via Resend. |
| PASSKEY_RP_ID | `api` | Optional. Relying party ID for passkey authentication. |
| PASSKEY_RP_NAME | `api` | Optional. Relying party display name for passkeys. |
| PASSKEY_ORIGIN | `api` | Optional. Origin allowed for passkey flows (e.g., `https://keeper.example.com`). |
| GOOGLE_CLIENT_ID | `api`, `cron`, `worker` | Optional. Required for Google Calendar integration. |
| GOOGLE_CLIENT_SECRET | `api`, `cron`, `worker` | Optional. Required for Google Calendar integration. |
| MICROSOFT_CLIENT_ID | `api`, `cron`, `worker` | Optional. Required for Microsoft Outlook integration. |
| MICROSOFT_CLIENT_SECRET | `api`, `cron`, `worker` | Optional. Required for Microsoft Outlook integration. |
| POSTGRES_PASSWORD | `standalone` | Optional. Custom password for the internal PostgreSQL database in `keeper-standalone`. If unset, defaults to `keeper`. The database is not exposed outside the container, so this is low risk, but can be set for defense in depth. |
| BLOCK_PRIVATE_RESOLUTION | `api`, `cron` | Optional. Set to `true` to block outbound fetches (ICS subscriptions, CalDAV servers) from resolving to private/reserved network addresses. Prevents SSRF. Defaults to `false` for backward compatibility with self-hosted setups that use local CalDAV/ICS servers. |
| PRIVATE_RESOLUTION_WHITELIST | `api`, `cron` | Optional. When `BLOCK_PRIVATE_RESOLUTION` is `true`, this comma-separated list of hostnames or IPs is exempt from the restriction. e.g. `192.168.1.50,radicale.local,10.0.2.12` |
| TRUSTED_ORIGINS | `api` | Optional. Comma-separated list of additional trusted origins for CSRF protection. e.g. `http://192.168.1.100,http://keeper.local,https://keeper.example.com` |
| MCP_PUBLIC_URL | `api`, `mcp` | Optional. Public URL of the MCP resource. Enables OAuth on the API and identifies the MCP server to clients. e.g. `https://keeper.example.com/mcp` |
| VITE_MCP_URL | `web` | Optional. Internal URL the web server uses to proxy `/mcp` requests to the MCP service. e.g. `http://mcp:3002` |
| MCP_PORT | `mcp` | Optional. Port the MCP server listens on. e.g. `3002` |
| OTEL_EXPORTER_OTLP_ENDPOINT | `api`, `cron`, `worker`, `mcp`, `web` | Optional. When set, enables forwarding structured logs to an OpenTelemetry collector via [`pino-opentelemetry-transport`](https://github.com/Vunovati/pino-opentelemetry-transport). The transport runs in a dedicated worker thread and does not affect application performance. e.g. `https://otel-collector.example.com:4318` |
| OTEL_EXPORTER_OTLP_PROTOCOL | `api`, `cron`, `worker`, `mcp`, `web` | Optional. Protocol used by the OTLP exporter. Defaults to `http/protobuf` per the OpenTelemetry spec. e.g. `http/protobuf`, `grpc`, `http/json` |
| OTEL_EXPORTER_OTLP_HEADERS | `api`, `cron`, `worker`, `mcp`, `web` | Optional. Headers sent with every OTLP export request. Use this for authentication (e.g. Basic auth or API keys). e.g. `Authorization=Basic dXNlcjpwYXNz` |
The following environment variables are baked into the web image at **build time**. They are pre-configured in the official Docker images and only need to be set if you are building from source.
| Name | Description |
| --------------------------------- | ------------------------------------------------------------------ |
| VITE_COMMERCIAL_MODE | Toggle commercial mode in the web UI (`true`/`false`). |
| POLAR_PRO_MONTHLY_PRODUCT_ID | Optional. Polar monthly product ID to power in-app upgrade links. |
| POLAR_PRO_YEARLY_PRODUCT_ID | Optional. Polar yearly product ID to power in-app upgrade links. |
| VITE_VISITORS_NOW_TOKEN | Optional. [visitors.now](https://visitors.now) token for analytics |
| VITE_GOOGLE_ADS_ID | Optional. Google Ads conversion tracking ID (e.g., `AW-123456789`) |
| VITE_GOOGLE_ADS_CONVERSION_LABEL | Optional. Google Ads conversion label for purchase tracking |
> [!NOTE]
>
> - `keeper-standalone` auto-configures everything internally — both the web server and Bun API sit behind a single Caddy reverse proxy on port `80`.
> - `keeper-services` runs the web, API, cron, and worker services inside one container. The web server proxies `/api` requests internally, so only port `3000` needs to be exposed.
> - For individual images, only the `web` container needs to be exposed. The API is accessed internally via `VITE_API_URL`.
## Images
| Tag | Description | Included Services |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `keeper-standalone:2.9` | The "standalone" image is everything you need to get up and running with Keeper with as little configuration as possible. | `keeper-web`, `keeper-api`, `keeper-cron`, `keeper-worker`, `redis`, `postgresql`, `caddy` |
| `keeper-services:2.9` | If you'd like for the Redis & Database to exist outside of the container, you can use the "services" image to launch without them included in the image. | `keeper-web`, `keeper-api`, `keeper-cron`, `keeper-worker` |
| `keeper-web:2.9` | An image containing the Vite SSR web interface. | `keeper-web` |
| `keeper-api:2.9` | An image containing the Bun API service. | `keeper-api` |
| `keeper-cron:2.9` | An image containing the Bun cron service. Requires `keeper-worker` for destination syncing. | `keeper-cron` |
| `keeper-worker:2.9` | An image containing the BullMQ worker that processes calendar sync jobs enqueued by `keeper-cron`. | `keeper-worker` |
| `keeper-mcp:2.9` | An image containing the MCP server for AI agent calendar access. Optional — only needed if using MCP clients. | `keeper-mcp` |
> [!TIP]
>
> Pin your images to a major.minor version tag (e.g., `2.9`) rather than `latest`. This prevents breaking changes from automatically applying when you pull new images.
## Prerequisites
### Docker & Docker Compose
In order to install Docker Compose, please refer to the [official Docker documentation.](https://docs.docker.com/compose/install/).
### Google OAuth Credentials
> [!TIP]
>
> This is optional, although you will not be able to set Google Calendar as a destination without this.
Reference the [official Google Cloud Platform documentation](https://support.google.com/cloud/answer/15549257) to generate valid credentials for Google OAuth. You must grant your consent screen the `calendar.events`, `calendar.calendarlist.readonly`, and `userinfo.email` scopes.
Once this is configured, set the client ID and client secret as the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` environment variables at runtime.
### Microsoft Azure Credentials
> [!TIP]
>
> Once again, this is optional. If you do not configure this, you will not be able to configure Microsoft Outlook as a destination.
Microsoft does not appear to do documentation well, the best I could find for non-legacy instructions on configuring OAuth is this [community thread.](https://learn.microsoft.com/en-us/answers/questions/4705805/how-to-set-up-oauth-2-0-for-outlook). The required scopes are `Calendars.ReadWrite`, `User.Read`, and `offline_access`. The client ID and secret for Microsoft go into the `MICROSOFT_CLIENT_ID` and `MICROSOFT_CLIENT_SECRET` environment variables respectively.
## Standalone Container
While you'd typically want to run containers granularly, if you just want to get up and running, a convenience image `keeper-standalone:2.9` has been provided. This container contains the `cron`, `worker`, `web`, `api` services as well as a configured `redis`, `database`, and `caddy` instance that puts everything behind the same port. While this is the easiest way to spin up Keeper, it is not recognized as best-practice.
### Generate `keeper-standalone` Environment Variables
The following will generate a `.env` file that contains the key used to generate sessions, as well as the key that is used to encrypt CalDAV credentials at rest.
> [!IMPORTANT]
>
> If you plan on accessing Keeper from a URL _other than_ http://localhost,
> you will need to set the `TRUSTED_ORIGINS` environment variable. This should
> be a comma-delimited list of protocol-hostname inclusive origins you will be using.
>
> Here is an example where we would be accessing Keeper from the LAN IP and where we
> are routing Keeper through a reverse proxy that hosts it at https://keeper.example.com/
>
> ```bash
> TRUSTED_ORIGINS=http://10.0.0.2,https://keeper.example.com
> ```
>
> Without this, you will fail CSRF checks on the `better-auth` package.
```bash
cat > .env << EOF
# BETTER_AUTH_SECRET and ENCRYPTION_KEY are required.
# TRUSTED_ORIGINS is required if you plan on accessing Keeper from an
# origin other than http://localhost/
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -base64 32)
TRUSTED_ORIGINS=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
EOF
```
### Run `keeper-standalone` with Docker
If you'd like to just run using the Docker CLI, you can use the following command. I would however recommend [using a compose.yaml](#run-standalone-with-docker-compose) file.
```bash
docker run -d \
-p 80:80 \
-v keeper-data:/var/lib/postgresql/data \
--env-file .env \
ghcr.io/ridafkih/keeper-standalone:2.9
```
### Run `keeper-standalone` with Docker Compose
If you'd prefer to use a `compose.yaml` file, the following is an example. Remember to [populate your .env file first](#generate-keeper-standalone-environment-variables).
```yaml
services:
keeper:
image: ghcr.io/ridafkih/keeper-standalone:2.9
ports:
- "80:80"
volumes:
- keeper-data:/var/lib/postgresql/data
environment:
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
TRUSTED_ORIGINS: ${TRUSTED_ORIGINS}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
volumes:
keeper-data:
```
Once that's configured, you can launch Keeper using the following command.
```bash
docker compose up -d
```
With all said and done, you can access Keeper at http://localhost/. You can use a reverse-proxy like Nginx or Caddy to put Keeper behind a domain on your network.
## Collective Services Image
If you'd like to bring your own Redis and PostgreSQL, you can use the `keeper-services` image. This contains the `cron`, `web` and `api` services in one.
### Generate `keeper-services` Environment Variables
```bash
cat > .env << EOF
# DATABASE_URL and REDIS_URL are required.
# *_CLIENT_ID and *_CLIENT_SECRET are optional.
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -base64 32)
DATABASE_URL=postgres://keeper:keeper@postgres:5432/keeper
REDIS_URL=redis://redis:6379
BETTER_AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
EOF
```
### Run `keeper-services` with Docker Compose
Once you've populated your environment variables, you can choose to run `redis` and `postgres` alongside the `keeper-services` image to get up and running.
```yaml
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: keeper
POSTGRES_PASSWORD: keeper
POSTGRES_DB: keeper
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keeper -d keeper"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
keeper:
image: ghcr.io/ridafkih/keeper-services:latest
environment:
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
postgres-data:
redis-data:
```
Once that's configured, you can launch Keeper using the following command.
```bash
docker compose up -d
```
## Individual Service Images
While running services individually is considered best-practice, it is verbose and more complicated to configure. Each service is hosted in its own image.
### Generate Individual Service Environment Variables
```bash
cat > .env << EOF
# The only optional variables are *_CLIENT_ID, *_CLIENT_SECRET
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -base64 32)
VITE_API_URL=http://api:3001
POSTGRES_USER=keeper
POSTGRES_PASSWORD=keeper
POSTGRES_DB=keeper
REDIS_URL=redis://redis:6379
BETTER_AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
EOF
```
### Configure Individual Service `compose.yaml`
```yaml
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keeper -d keeper"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
api:
image: ghcr.io/ridafkih/keeper-api:latest
environment:
API_PORT: 3001
DATABASE_URL: postgres://keeper:keeper@postgres:5432/keeper
REDIS_URL: redis://redis:6379
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
cron:
image: ghcr.io/ridafkih/keeper-cron:latest
environment:
DATABASE_URL: postgres://keeper:keeper@postgres:5432/keeper
REDIS_URL: redis://redis:6379
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
web:
image: ghcr.io/ridafkih/keeper-web:latest
environment:
VITE_API_URL: ${VITE_API_URL}
PORT: 3000
ports:
- "3000:3000"
depends_on:
api:
condition: service_started
volumes:
postgres-data:
redis-data:
```
Once that's configured, you can launch Keeper using the following command.
```bash
docker compose up -d
```
# MCP (Model Context Protocol)
Keeper includes an optional MCP server that lets AI agents (such as Claude) access your calendar data through a standardized protocol. The MCP server authenticates via OAuth 2.1 with a consent flow hosted by the web application.
## Available Tools
| Tool | Description |
| ----------------- | ---------------------------------------------------------------------------------------------------- |
| `list_calendars` | List all calendars connected to Keeper, including provider name and account. |
| `get_events` | Get calendar events within a date range. Accepts ISO 8601 datetimes and an IANA timezone identifier. |
| `get_event_count` | Get the total number of calendar events synced to Keeper. |
## Connecting an MCP Client
To connect an MCP-compatible client (e.g. Claude Code, Claude Desktop), point it at your MCP server URL. The client will be guided through the OAuth consent flow to authorize read access to your calendar data.
Example Claude Code MCP configuration:
```json
{
"mcpServers": {
"keeper": {
"type": "url",
"url": "https://keeper.example.com/mcp"
}
}
}
```
## Self-Hosted MCP Setup
> [!NOTE]
>
> MCP is fully optional. All MCP-related environment variables are optional across every service and image. If they are not set, Keeper starts normally without MCP functionality. Existing self-hosted deployments are unaffected.
The MCP server is proxied through the web service at `/mcp`, the same way the API is proxied at `/api`. MCP is **not** bundled in the `keeper-standalone` or `keeper-services` convenience images — run the `keeper-mcp` image as a separate container alongside them.
To enable MCP on a self-hosted instance:
1. Run the `keeper-mcp` container with `MCP_PORT`, `MCP_PUBLIC_URL`, `DATABASE_URL`, `BETTER_AUTH_SECRET`, and `BETTER_AUTH_URL`.
2. Set `MCP_PUBLIC_URL` on the `api` service to the same value (e.g. `https://keeper.example.com/mcp`).
3. Set `VITE_MCP_URL` on the `web` service to the internal URL of the MCP container (e.g. `http://mcp:3002`).
# Modules
## Applications
1. [@keeper.sh/api](./applications/api)
2. [@keeper.sh/cron](./applications/cron)
3. [@keeper.sh/mcp](./applications/mcp)
4. [@keeper.sh/web](./applications/canary-web)
5. @keeper.sh/cli _(Coming Soon)_
6. @keeper.sh/mobile _(Coming Soon)_
7. @keeper.sh/ssh _(Coming Soon)_
## Modules
1. [@keeper.sh/auth](./packages/auth)
1. [@keeper.sh/auth-plugin-username-only](./packages/auth-plugin-username-only)
1. [@keeper.sh/broadcast](./packages/broadcast)
1. [@keeper.sh/broadcast-client](./packages/broadcast-client)
1. [@keeper.sh/calendar](./packages/calendar)
1. [@keeper.sh/constants](./packages/constants)
1. [@keeper.sh/data-schemas](./packages/data-schemas)
1. [@keeper.sh/database](./packages/database)
1. [@keeper.sh/date-utils](./packages/date-utils)
1. [@keeper.sh/encryption](./packages/encryption)
1. [@keeper.sh/env](./packages/env)
1. [@keeper.sh/fixtures](./packages/fixtures)
1. [@keeper.sh/keeper-api](./packages/keeper-api)
1. [@keeper.sh/oauth](./packages/oauth)
1. [@keeper.sh/oauth-google](./packages/oauth-google)
1. [@keeper.sh/oauth-microsoft](./packages/oauth-microsoft)
1. [@keeper.sh/premium](./packages/premium)
1. [@keeper.sh/provider-caldav](./packages/provider-caldav)
1. [@keeper.sh/provider-core](./packages/provider-core)
1. [@keeper.sh/provider-fastmail](./packages/provider-fastmail)
1. [@keeper.sh/provider-google-calendar](./packages/provider-google-calendar)
1. [@keeper.sh/provider-icloud](./packages/provider-icloud)
1. [@keeper.sh/provider-outlook](./packages/provider-outlook)
1. [@keeper.sh/provider-registry](./packages/provider-registry)
1. [@keeper.sh/pull-calendar](./packages/pull-calendar)
1. [@keeper.sh/sync-calendar](./packages/sync-calendar)
1. [@keeper.sh/sync-events](./packages/sync-events)
1. [@keeper.sh/typescript-config](./packages/typescript-config)
================================================
FILE: applications/web/.eslintrc.cjs
================================================
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
================================================
FILE: applications/web/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: applications/web/Dockerfile
================================================
FROM oven/bun:1 AS base
WORKDIR /app
FROM base AS source
COPY . .
FROM base AS prune
RUN bun install --global turbo
COPY --from=source /app .
RUN turbo prune @keeper.sh/web --docker
FROM base AS build
COPY --from=prune /app/out/json/ .
RUN bun install --frozen-lockfile
COPY --from=prune /app/out/full/ .
RUN bun run --cwd applications/web build
FROM base AS runtime
COPY --from=prune /app/out/json/ .
RUN bun install --frozen-lockfile --production
COPY --from=build /app/applications/web/dist ./applications/web/dist
COPY --from=build /app/applications/web/public ./applications/web/public
COPY --from=prune /app/out/full/packages/otelemetry ./packages/otelemetry
RUN bun link --cwd packages/otelemetry
COPY --from=source /app/applications/web/entrypoint.sh ./applications/web/entrypoint.sh
RUN chmod +x ./applications/web/entrypoint.sh
WORKDIR /app/applications/web
ENV ENV=production
EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]
================================================
FILE: applications/web/README.md
================================================
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
================================================
FILE: applications/web/entrypoint.sh
================================================
#!/bin/sh
set -e
exec bun dist/server-entry/index.js 2>&1 | keeper-otelemetry
================================================
FILE: applications/web/eslint.config.js
================================================
import js from "@eslint/js";
import tseslint from "@typescript-eslint/eslint-plugin";
import tsparser from "@typescript-eslint/parser";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
export default [
{ ignores: ["dist"] },
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parser: tsparser,
ecmaVersion: 2020,
globals: { window: true, document: true },
},
plugins: {
"@typescript-eslint": tseslint,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...tseslint.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
},
{
files: ["**/routes/**/*.{ts,tsx}"],
rules: { "react-refresh/only-export-components": "off" },
},
];
================================================
FILE: applications/web/index.html
================================================
================================================
FILE: applications/web/package.json
================================================
{
"name": "@keeper.sh/web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "bun src/server/index.ts",
"build": "vite build --outDir dist/client && vite build --ssr src/server.tsx --outDir dist/server && bun scripts/build.ts",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"types": "tsc --noEmit",
"test": "bun x --bun vitest run",
"preview": "vite preview",
"start": "bun scripts/start.ts"
},
"dependencies": {
"@better-auth/passkey": "^1.5.5",
"@keeper.sh/data-schemas": "workspace:*",
"@keeper.sh/otelemetry": "workspace:*",
"@polar-sh/checkout": "^0.2.0",
"@tanstack/react-router": "^1.163.3",
"arktype": "^2.2.0",
"better-auth": "^1.5.5",
"clsx": "^2.1.1",
"entrykit": "^0.1.3",
"fast-xml-parser": "^5.4.2",
"jotai": "^2.18.0",
"linkedom": "^0.18.12",
"lucide-react": "^0.576.0",
"motion": "^12.34.4",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"streamdown": "^2.4.0",
"swr": "^2.4.1",
"tailwind-variants": "^3.2.2",
"widelogger": "^0.7.0",
"yaml": "^2.8.2"
},
"devDependencies": {
"@babel/preset-typescript": "^7.28.5",
"@eslint/js": "^10.0.1",
"@rolldown/plugin-babel": "^0.2.0",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/router-plugin": "^1.164.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"@vitejs/plugin-react": "^6.0.0",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^10.0.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vite": "^8.0.0",
"vite-plugin-svgr": "^4.5.0",
"vitest": "^4.1.4"
}
}
================================================
FILE: applications/web/plugins/blog.ts
================================================
import { readFileSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import type { Plugin } from "vite";
import { type } from "arktype";
import { parse as parseYaml } from "yaml";
const blogPostMetadataSchema = type({
"+": "reject",
blurb: "string >= 1",
createdAt: "string.date.iso",
description: "string >= 1",
"slug?": /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
tags: "string[]",
title: "string >= 1",
updatedAt: "string.date.iso",
});
type BlogPostMetadata = typeof blogPostMetadataSchema.infer;
interface ProcessedBlogPost {
content: string;
metadata: BlogPostMetadata;
slug: string;
}
function toIsoDate(value: string): string {
return value.slice(0, 10);
}
function normalizeMetadataInput(value: unknown): unknown {
if (typeof value !== "object" || value === null) return value;
const normalized: Record = { ...value };
if (normalized.createdAt instanceof Date) {
normalized.createdAt = normalized.createdAt.toISOString();
}
if (normalized.updatedAt instanceof Date) {
normalized.updatedAt = normalized.updatedAt.toISOString();
}
return normalized;
}
function splitFrontmatter(
raw: string,
filePath: string,
): { content: string; data: unknown } {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
if (!match) {
throw new Error(
`Blog post "${filePath}" must start with a YAML frontmatter block.`,
);
}
let parsed: unknown;
try {
parsed = parseYaml(match[1]);
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown YAML parse error";
throw new Error(
`Blog frontmatter parsing failed for "${filePath}": ${message}`,
);
}
return {
content: raw.slice(match[0].length).trimStart(),
data: parsed ?? {},
};
}
function parseMetadata(value: unknown, filePath: string): BlogPostMetadata {
const result = blogPostMetadataSchema(normalizeMetadataInput(value));
if (result instanceof type.errors) {
throw new Error(
`Blog metadata is invalid for "${filePath}": ${result}`,
);
}
if (result.tags.length === 0) {
throw new Error(
`Blog metadata tags must contain at least one tag in "${filePath}".`,
);
}
return {
...result,
createdAt: toIsoDate(result.createdAt),
updatedAt: toIsoDate(result.updatedAt),
};
}
function createSlug(title: string): string {
return title
.toLowerCase()
.trim()
.replace(/['"]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function removeRedundantLeadingHeading(
content: string,
title: string,
): string {
const lines = content.split("\n");
const firstLine = lines[0]?.trim() ?? "";
if (!firstLine.startsWith("# ")) return content;
const headingText = firstLine.slice(2).trim().toLowerCase();
if (headingText !== title.trim().toLowerCase()) return content;
let nextIndex = 1;
while (nextIndex < lines.length && lines[nextIndex].trim().length === 0) {
nextIndex += 1;
}
return lines.slice(nextIndex).join("\n");
}
function processBlogDirectory(blogDir: string): ProcessedBlogPost[] {
const files = readdirSync(blogDir)
.filter((f) => f.endsWith(".mdx"))
.sort();
const slugCounts = new Map();
const posts = files.map((file) => {
const filePath = join(blogDir, file);
const raw = readFileSync(filePath, "utf-8");
const { content: rawContent, data } = splitFrontmatter(raw, file);
const metadata = parseMetadata(data, file);
const content = removeRedundantLeadingHeading(rawContent, metadata.title);
const hasCustomSlug = typeof metadata.slug === "string";
const baseSlug = hasCustomSlug ? metadata.slug : createSlug(metadata.title);
const seenCount = slugCounts.get(baseSlug) ?? 0;
if (hasCustomSlug && seenCount > 0) {
throw new Error(`Duplicate blog slug "${baseSlug}" found in metadata.`);
}
slugCounts.set(baseSlug, seenCount + 1);
const slug = seenCount === 0 ? baseSlug : `${baseSlug}-${seenCount + 1}`;
return { content, metadata, slug };
});
return posts.sort((a, b) =>
b.metadata.createdAt.localeCompare(a.metadata.createdAt),
);
}
const VIRTUAL_MODULE_ID = "virtual:blog-posts";
const RESOLVED_ID = `\0${VIRTUAL_MODULE_ID}`;
export function blogPlugin(): Plugin {
let blogDir: string;
return {
name: "keeper-blog",
configResolved(config) {
blogDir = resolve(config.root, "src/content/blog");
},
resolveId(id) {
if (id === VIRTUAL_MODULE_ID) return RESOLVED_ID;
},
load(id) {
if (id !== RESOLVED_ID) return;
const posts = processBlogDirectory(blogDir);
return `export const blogPosts = ${JSON.stringify(posts)};`;
},
handleHotUpdate({ file, server }) {
if (file.startsWith(blogDir) && file.endsWith(".mdx")) {
const module = server.moduleGraph.getModuleById(RESOLVED_ID);
if (module) {
server.moduleGraph.invalidateModule(module);
return [module];
}
}
},
};
}
================================================
FILE: applications/web/plugins/sitemap.ts
================================================
import { readdirSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import type { Plugin } from "vite";
import { XMLBuilder } from "fast-xml-parser";
import { parse as parseYaml } from "yaml";
const SITE_URL = "https://keeper.sh";
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
interface SitemapEntry {
loc: string;
lastmod: string;
}
const staticEntries: SitemapEntry[] = [
{ loc: `${SITE_URL}/`, lastmod: "2026-03-09" },
{ loc: `${SITE_URL}/blog`, lastmod: "2026-03-09" },
{ loc: `${SITE_URL}/privacy`, lastmod: "2025-12-01" },
{ loc: `${SITE_URL}/terms`, lastmod: "2025-12-01" },
];
function parseFrontmatter(raw: string): Record {
const [, match] = raw.match(FRONTMATTER_PATTERN);
if (!match) return {};
return parseYaml(match);
}
function discoverBlogEntries(blogDir: string): SitemapEntry[] {
const files = readdirSync(blogDir).filter((f) => f.endsWith(".mdx"));
return files.map((file) => {
const raw = readFileSync(join(blogDir, file), "utf-8");
const frontmatter = parseFrontmatter(raw);
if (typeof frontmatter.slug !== "string") {
throw new Error(`Blog post "${file}" is missing a slug.`);
}
if (typeof frontmatter.updatedAt !== "string") {
throw new Error(`Blog post "${file}" is missing updatedAt.`);
}
return {
loc: `${SITE_URL}/blog/${frontmatter.slug}`,
lastmod: frontmatter.updatedAt.slice(0, 10),
};
});
}
const xmlBuilder = new XMLBuilder({
format: true,
ignoreAttributes: false,
suppressEmptyNode: true,
});
function buildSitemapXml(entries: SitemapEntry[]): string {
const document = {
"?xml": { "@_version": "1.0", "@_encoding": "UTF-8" },
urlset: {
"@_xmlns": "http://www.sitemaps.org/schemas/sitemap/0.9",
url: entries.map((entry) => ({
loc: entry.loc,
lastmod: entry.lastmod,
})),
},
};
return String(xmlBuilder.build(document));
}
export function sitemapPlugin(): Plugin {
let blogDir: string;
return {
name: "keeper-sitemap",
apply: "build",
configResolved(config) {
blogDir = resolve(config.root, "src/content/blog");
},
generateBundle() {
const blogEntries = discoverBlogEntries(blogDir);
const entries = [...staticEntries, ...blogEntries];
this.emitFile({
type: "asset",
fileName: "sitemap.xml",
source: buildSitemapXml(entries),
});
},
};
}
================================================
FILE: applications/web/public/llms-full.txt
================================================
# Keeper.sh — Full Context
> Open-source calendar event syncing. Synchronize events between your personal, work, business and school calendars.
Keeper.sh is an open-source (AGPL-3.0) calendar synchronization service built by Rida F'kih. It keeps time slots aligned across multiple calendar providers using a pull-compare-push sync engine. The project is self-hostable and offers a hosted version at keeper.sh.
## Problem
People with calendars across multiple providers (Google Calendar, Outlook, iCloud, FastMail) end up with fragmented availability. Different people see incomplete windows into their schedule, causing constant overlap and scheduling friction. Existing tools were too manual, had finicky sync behavior, lacked privacy controls, or didn't offer self-hosting.
## How It Works
### Sync Engine
Keeper.sh uses a pull-compare-push architecture. On each sync cycle:
1. **Pull**: Events are fetched from configured source calendars
2. **Compare**: Events are compared against known state
3. **Push**: Changes are pushed to destination calendars
Core sync operations:
- **Add**: Source event with no mapping on destination gets created
- **Delete**: Mapping exists but source event is gone — destination event gets removed
- **Cleanup**: Keeper-created events with no mapping are cleaned up
Events created by Keeper are tagged with a `@keeper.sh` suffix on their remote UID. For Outlook (which doesn't support custom UIDs), a `"keeper.sh"` category is used instead.
Race conditions are prevented using Redis-backed generation counters. If two syncs target the same calendar simultaneously, the later one backs off.
For Google and Outlook, Keeper uses incremental sync tokens — only fetching what changed since the last sync, making cycles fast even for calendars with thousands of events.
### Supported Providers
**OAuth providers:**
- **Google Calendar** — full read/write, supports filtering Focus Time, Working Location, and Out of Office events
- **Microsoft Outlook** — full read/write via Microsoft Graph API
**CalDAV-based providers:**
- **FastMail** — pre-configured CalDAV
- **iCloud** — pre-configured CalDAV with app-specific password support
- **Generic CalDAV** — any CalDAV-compatible server
- **iCal/ICS feeds** — read-only URL-based calendars
Each calendar has capability flags: "pull" (read events from) and "push" (write events to). iCal feeds are pull-only. OAuth and CalDAV calendars support both.
### Privacy Controls
By default, only busy/free time blocks are shared. Event titles, descriptions, locations, and attendee lists are stripped before syncing.
Per-source calendar settings:
- Include or exclude event titles (replace with "Busy")
- Include or exclude descriptions and locations
- Custom event name templates using `{{event_name}}` or `{{calendar_name}}` placeholders
- Skip all-day events
- Skip Google-specific event types (Focus Time, Working Location, Out of Office)
### iCal Feed
Keeper generates a unique, token-authenticated URL combining events from selected source calendars. Subscribable from any calendar app supporting iCal (Apple Calendar, Thunderbird, etc.). Respects all privacy settings.
### Source and Destination Mappings
Connect calendar accounts, then configure which calendars are sources and which are destinations. A single source can push to multiple destinations. A single destination can receive from multiple sources.
Setup flow:
1. Connect an account (OAuth, CalDAV, or iCal URL)
2. Select calendars
3. Rename for clearer labels
4. Map sources to destinations
## Pricing
- **Free**: 2 linked accounts, 2 calendars per account, 30-minute sync intervals, aggregated iCal feed
- **Pro ($5/month)**: Unlimited accounts, unlimited calendars, 1-minute sync intervals, priority support
Self-hosted instances get Pro-tier features by default.
## Self-Hosting
The entire stack runs on PostgreSQL, Redis, Bun, and Vite + React.
Three deployment models:
- **Standalone**: Single `compose.yaml` bundling Caddy, PostgreSQL, Redis, API, web, and cron
- **Services-only**: Application containers only (bring your own PostgreSQL and Redis)
- **Individual containers**: Separate `keeper-web`, `keeper-api`, `keeper-cron`, and `keeper-mcp` images
Google Calendar and Outlook require OAuth app setup (Google Cloud / Azure). CalDAV providers and iCal feeds work without OAuth.
## MCP (Model Context Protocol)
Keeper includes an optional MCP server that gives AI agents (such as Claude) read-only access to calendar data. The server authenticates via OAuth 2.1 with a user consent flow and is proxied through the web service at `/mcp`.
### Available Tools
- **list_calendars** — List all calendars connected to Keeper, including provider name and account
- **get_events** — Get calendar events within a date range (ISO 8601 datetimes + IANA timezone)
- **get_event_count** — Get the total number of synced calendar events
### Connecting
Point any MCP-compatible client at the `/mcp` endpoint of a Keeper instance. The client will be guided through the OAuth consent flow to authorize read access.
MCP is fully optional — all related environment variables are optional across every service. Existing deployments are unaffected when upgrading.
## Technical Architecture
- **CalDAV as universal protocol**: RFC 4791 as the common layer for FastMail, iCloud, and standards-compliant servers
- **Encrypted credentials at rest**: CalDAV passwords and OAuth tokens encrypted with configurable key
- **Content hashing for iCal feeds**: Skips processing if feed content unchanged, keeps snapshots for 6 hours
- **Real-time sync status**: WebSocket endpoint broadcasts sync progress to dashboard
- **MCP via OAuth 2.1**: Separate MCP server with JWT-based session resolution, proxied through the web service at `/mcp`
## Links
- Homepage: https://keeper.sh
- Blog: https://keeper.sh/blog
- Privacy Policy: https://keeper.sh/privacy
- Terms & Conditions: https://keeper.sh/terms
- GitHub: https://github.com/ridafkih/keeper.sh
## Maintainer
Maintained by Rida F'kih (https://rida.dev)
================================================
FILE: applications/web/public/llms.txt
================================================
# Keeper.sh
> Open-source calendar event syncing. Synchronize events between your personal, work, business and school calendars.
Keeper.sh is an open-source (AGPL-3.0) calendar synchronization service that keeps time slots aligned across multiple calendar providers. It uses a pull-compare-push architecture to sync events between Google Calendar, Outlook, FastMail, iCloud, CalDAV, and iCal feeds.
## Key Features
- Universal calendar sync across Google Calendar, Outlook, Apple Calendar, FastMail, CalDAV, and iCal feeds
- Privacy-first: only busy/free time blocks are shared by default, with granular controls for titles, descriptions, and locations
- Aggregated iCal feed generation for sharing availability externally
- MCP (Model Context Protocol) server for AI agent calendar access via OAuth 2.1
- Self-hostable with Docker Compose (standalone, services-only, or individual containers)
- Free tier (30-minute sync, 2 accounts) and Pro tier ($5/month, 1-minute sync, unlimited)
## Links
- [Homepage](https://keeper.sh)
- [Blog](https://keeper.sh/blog)
- [Privacy Policy](https://keeper.sh/privacy)
- [Terms & Conditions](https://keeper.sh/terms)
- [GitHub Repository](https://github.com/ridafkih/keeper.sh)
- [Full LLM Context](https://keeper.sh/llms-full.txt)
================================================
FILE: applications/web/public/robots.txt
================================================
User-agent: *
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: GPTBot
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: ChatGPT-User
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: ClaudeBot
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: PerplexityBot
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: Google-Extended
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: Amazonbot
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: Applebot
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: Bytespider
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
User-agent: Cohere-AI
Allow: /
Disallow: /dashboard
Disallow: /auth
Disallow: /login
Disallow: /register
Disallow: /verify-email
Disallow: /verify-authentication
Disallow: /reset-password
Disallow: /forgot-password
Sitemap: https://keeper.sh/sitemap.xml
================================================
FILE: applications/web/public/site.webmanifest
================================================
{
"name": "Keeper.sh",
"short_name": "Keeper",
"start_url": "/",
"icons": [
{
"src": "/180x180-light-on-dark.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/180x180-light-on-dark.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/512x512-on-light.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#FAFAFA",
"background_color": "#FAFAFA",
"display": "standalone"
}
================================================
FILE: applications/web/scripts/build.ts
================================================
import { build } from "bun";
await build({
entrypoints: ["src/server/index.ts"],
outdir: "./dist/server-entry",
target: "bun",
splitting: false,
external: [
"entrykit",
"linkedom",
"fast-xml-parser",
"vite",
"yaml",
"widelogger",
"pino-opentelemetry-transport",
],
});
================================================
FILE: applications/web/scripts/start.ts
================================================
import { existsSync } from "node:fs";
const serverEntryUrl = new URL("../dist/server-entry/index.js", import.meta.url);
if (!existsSync(serverEntryUrl)) {
throw new Error(
"Missing production server entry at applications/web/dist/server-entry/index.js. Run `bun run build` before `bun run start`.",
);
}
await import(serverEntryUrl.href);
================================================
FILE: applications/web/src/components/analytics-scripts.tsx
================================================
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { useLocation } from "@tanstack/react-router";
import type { PublicRuntimeConfig } from "@/lib/runtime-config";
import {
identify,
resolveEffectiveConsent,
track,
} from "@/lib/analytics";
import { useSession } from "@/hooks/use-session";
const subscribe = (callback: () => void): (() => void) => {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
};
const getServerSnapshot = (): boolean => false;
function resolveConsentLabel(hasConsent: boolean): "granted" | "denied" {
if (hasConsent) return "granted";
return "denied";
}
function AnalyticsScripts({ runtimeConfig }: { runtimeConfig: PublicRuntimeConfig }) {
const { gdprApplies, googleAdsId, visitorsNowToken } = runtimeConfig;
const getSnapshot = useCallback(
() => resolveEffectiveConsent(gdprApplies),
[gdprApplies],
);
const hasConsent = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const location = useLocation();
const consentState = resolveConsentLabel(hasConsent);
const { user } = useSession();
const identifiedUserId = useRef(null);
useEffect(() => {
track("page_view", { path: location.pathname });
}, [location.pathname]);
useEffect(() => {
if (!user) return;
if (identifiedUserId.current === user.id) return;
identifiedUserId.current = user.id;
identify({ id: user.id, email: user.email, name: user.name }, { gdprApplies });
}, [user, gdprApplies]);
return (
<>
{visitorsNowToken && (
)}
{googleAdsId && (
<>
>
)}
>
);
}
export { AnalyticsScripts };
================================================
FILE: applications/web/src/components/cookie-consent.tsx
================================================
import { useCallback, useState } from "react";
import type { PropsWithChildren } from "react";
import { AnimatePresence, LazyMotion } from "motion/react";
import * as m from "motion/react-m";
import { loadMotionFeatures } from "@/lib/motion-features";
import { Text } from "@/components/ui/primitives/text";
import { TextLink } from "@/components/ui/primitives/text-link";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { hasConsentChoice, setAnalyticsConsent, track } from "@/lib/analytics";
const CARD_ENTER = { opacity: 0, y: 10, filter: "blur(4px)" };
const CARD_VISIBLE = { opacity: 1, y: 0, filter: "blur(0px)" };
const CARD_EXIT = { opacity: 0, y: 10, filter: "blur(4px)" };
const COLLAPSE_ANIMATE = { height: "auto" };
const COLLAPSE_EXIT = { height: 0 };
const CARD_TRANSITION = { duration: 0.2 };
const COLLAPSE_TRANSITION = { duration: 0.2, delay: 0.15 };
function resolveConsentEventName(consent: boolean): string {
if (consent) return "consent_granted";
return "consent_denied";
}
function ConsentBannerContent({ children }: PropsWithChildren) {
return (
{children}
);
}
function ConsentBannerActions({ children }: PropsWithChildren) {
return (
{children}
);
}
function ConsentBannerCard({ children }: PropsWithChildren) {
return (
{children}
);
}
function CookieConsent() {
const [visible, setVisible] = useState(() => !hasConsentChoice());
const handleChoice = useCallback((consent: boolean): void => {
track(resolveConsentEventName(consent));
setAnalyticsConsent(consent);
setVisible(false);
}, []);
return (
{visible && (
Can Keeper{" "}
use cookies for analytics?
handleChoice(true)}>
Yes
handleChoice(false)}>
No
)}
);
}
export { CookieConsent };
================================================
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu-editable.tsx
================================================
import { use, useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from "react";
import Pencil from "lucide-react/dist/esm/icons/pencil";
import { cn } from "@/utils/cn";
import { ItemDisabledContext, MenuVariantContext } from "./navigation-menu.contexts";
import {
DISABLED_LABEL_TONE,
LABEL_TONE,
navigationMenuItemIconStyle,
navigationMenuItemStyle,
} from "./navigation-menu.styles";
import { NavigationMenuItemLabel } from "./navigation-menu-items";
import { Text } from "@/components/ui/primitives/text";
type NavigationMenuEditableItemProps = {
onCommit: (value: string) => Promise | void;
label?: string;
children?: ReactNode;
defaultEditing?: boolean;
disabled?: boolean;
className?: string;
} & ({ value: string } | { getValue: () => string });
export function NavigationMenuEditableItem(props: NavigationMenuEditableItemProps) {
const {
onCommit,
label,
children,
defaultEditing,
disabled,
className,
} = props;
const resolveValue = () => "getValue" in props ? props.getValue() : props.value;
const [editing, setEditing] = useState(defaultEditing ?? false);
const startEditing = () => setEditing(true);
const stopEditing = () => setEditing(false);
if (editing) {
return (
{
await onCommit(trimmed);
stopEditing();
}}
onCancel={stopEditing}
/>
);
}
return (
{children ?? }
);
}
type NavigationMenuEditableTemplateItemProps = {
onCommit: (value: string) => Promise | void;
label?: string;
valueContent?: ReactNode;
children?: ReactNode;
renderInput: (value: string) => ReactNode;
defaultEditing?: boolean;
disabled?: boolean;
className?: string;
} & ({ value: string } | { getValue: () => string });
export function NavigationMenuEditableTemplateItem(props: NavigationMenuEditableTemplateItemProps) {
const {
onCommit,
label,
valueContent,
children,
renderInput,
defaultEditing,
disabled,
className,
} = props;
const resolveValue = () => "getValue" in props ? props.getValue() : props.value;
const [editing, setEditing] = useState(defaultEditing ?? false);
const startEditing = () => setEditing(true);
const stopEditing = () => setEditing(false);
if (editing) {
return (
{
await onCommit(trimmed);
stopEditing();
}}
onCancel={stopEditing}
/>
);
}
return (
{children ?? }
);
}
function useEditableCommit(
value: string,
onCommit: (value: string) => Promise | void,
onCancel: () => void,
) {
const inputRef = useRef(null);
const committingRef = useRef(false);
const commit = async () => {
if (committingRef.current) return;
const trimmed = inputRef.current?.value.trim();
if (!trimmed || trimmed === value) {
onCancel();
return;
}
committingRef.current = true;
try {
await onCommit(trimmed);
} finally {
committingRef.current = false;
}
};
const handleKeyDown = (event: ReactKeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
void commit();
}
if (event.key === "Escape") {
onCancel();
}
};
const inputProps = {
ref: inputRef,
type: "text" as const,
defaultValue: value,
autoComplete: "off",
onBlur: () => {
void commit();
},
onKeyDown: handleKeyDown,
autoFocus: true,
};
return { inputProps };
}
function EditableItemInput({
value,
label,
className,
onCommit,
onCancel,
}: {
value: string;
label?: string;
className?: string;
onCommit: (value: string) => Promise | void;
onCancel: () => void;
}) {
const variant = use(MenuVariantContext);
const { inputProps } = useEditableCommit(value, onCommit, onCancel);
const inputClass = cn(
"min-w-0 text-sm tracking-tight bg-transparent cursor-text outline-none",
label ? "flex-1 text-right" : "flex-1",
);
return (
{label && {label} }
);
}
function EditableTemplateItemInput({
value,
label,
className,
renderInput,
onCommit,
onCancel,
}: {
value: string;
label?: string;
className?: string;
renderInput: (value: string) => ReactNode;
onCommit: (value: string) => Promise | void;
onCancel: () => void;
}) {
const variant = use(MenuVariantContext);
const { inputProps } = useEditableCommit(value, onCommit, onCancel);
const inputClass = cn(
"min-w-0 text-sm tracking-tight bg-transparent cursor-text outline-none",
label ? "flex-1 text-right" : "flex-1",
);
return (
);
}
function TemplateInputOverlay({
inputRef,
defaultValue,
renderInput,
label,
}: {
inputRef: React.RefObject;
defaultValue: string;
renderInput: (value: string) => ReactNode;
label?: string;
}) {
const [liveValue, setLiveValue] = useState(defaultValue);
const overlayRef = useRef(null);
useEffect(() => {
const input = inputRef.current;
if (!input) return;
const handleInput = () => setLiveValue(input.value);
const handleScroll = () => {
if (overlayRef.current) overlayRef.current.scrollLeft = input.scrollLeft;
};
input.addEventListener("input", handleInput);
input.addEventListener("scroll", handleScroll);
return () => {
input.removeEventListener("input", handleInput);
input.removeEventListener("scroll", handleScroll);
};
}, [inputRef]);
return (
{renderInput(liveValue)}
);
}
function EditableItemDefaultValue({ value, label }: { value: ReactNode; label?: string }) {
const variant = use(MenuVariantContext);
const disabled = use(ItemDisabledContext);
return (
{value}
);
}
function EditableItemDisplay({
label,
children,
disabled: disabledProp,
className,
onStartEditing,
}: {
label?: string;
children?: ReactNode;
disabled?: boolean;
className?: string;
onStartEditing: () => void;
}) {
const variant = use(MenuVariantContext);
const disabledFromContext = use(ItemDisabledContext);
const disabled = disabledProp || disabledFromContext;
return (
!disabled && onStartEditing()}
disabled={disabled}
className={navigationMenuItemStyle({ variant, interactive: !disabled, className })}
>
{label && {label} }
{children}
);
}
================================================
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu-items.tsx
================================================
import type { ComponentPropsWithoutRef, PropsWithChildren } from "react";
import { use } from "react";
import { Link } from "@tanstack/react-router";
import ArrowRight from "lucide-react/dist/esm/icons/arrow-right";
import { cn } from "@/utils/cn";
import {
InsidePopoverContext,
ItemDisabledContext,
ItemIsLinkContext,
MenuVariantContext,
} from "./navigation-menu.contexts";
import {
DISABLED_LABEL_TONE,
LABEL_TONE,
navigationMenuItemIconStyle,
navigationMenuItemStyle,
navigationMenuStyle,
navigationMenuToggleThumb,
navigationMenuToggleTrack,
type MenuVariant,
} from "./navigation-menu.styles";
import { CheckboxIndicator } from "@/components/ui/primitives/checkbox";
import { Text } from "@/components/ui/primitives/text";
type NavigationMenuProps = PropsWithChildren<{
variant?: MenuVariant;
className?: string;
}>;
export function NavigationMenu({
children,
variant,
className,
}: NavigationMenuProps) {
return (
);
}
type NavigationMenuItemProps = PropsWithChildren<{
className?: string;
}>;
type NavigationMenuLinkItemProps = PropsWithChildren<{
to?: ComponentPropsWithoutRef["to"];
onMouseEnter?: () => void;
disabled?: boolean;
className?: string;
}>;
type NavigationMenuButtonItemProps = PropsWithChildren<{
onClick?: () => void;
disabled?: boolean;
className?: string;
}>;
type NavigationMenuItemLabelProps = PropsWithChildren<{
className?: string;
}>;
type NavigationMenuItemTrailingProps = PropsWithChildren<{
className?: string;
}>;
export function NavigationMenuItem({
className,
children,
}: NavigationMenuItemProps) {
const variant = use(MenuVariantContext);
const insidePopover = use(InsidePopoverContext);
const itemClass = navigationMenuItemStyle({ variant, interactive: false, className });
const Wrapper = insidePopover ? "div" : "li";
const content = {children} ;
return (
{content}
);
}
export function NavigationMenuLinkItem({
to,
onMouseEnter,
disabled,
className,
children,
}: NavigationMenuLinkItemProps) {
const variant = use(MenuVariantContext);
const insidePopover = use(InsidePopoverContext);
const interactive = Boolean(to) && !disabled;
const itemClass = navigationMenuItemStyle({ variant, interactive, className });
const Wrapper = insidePopover ? "div" : "li";
const content = {children} ;
return (
{interactive ? (
{content}
) : (
{content}
)}
);
}
export function NavigationMenuButtonItem({
onClick,
disabled,
className,
children,
}: NavigationMenuButtonItemProps) {
const variant = use(MenuVariantContext);
const insidePopover = use(InsidePopoverContext);
const interactive = Boolean(onClick) && !disabled;
const itemClass = navigationMenuItemStyle({ variant, interactive, className });
const Wrapper = insidePopover ? "div" : "li";
const content = {children} ;
return (
{content}
);
}
export function NavigationMenuItemIcon({ children }: PropsWithChildren) {
const variant = use(MenuVariantContext);
const disabled = use(ItemDisabledContext);
return {children}
;
}
export function NavigationMenuItemLabel({
children,
className,
}: NavigationMenuItemLabelProps) {
const variant = use(MenuVariantContext);
const disabled = use(ItemDisabledContext);
const toneMap = disabled ? DISABLED_LABEL_TONE : LABEL_TONE;
return (
{children}
);
}
export function NavigationMenuEmptyItem({ children }: PropsWithChildren) {
const variant = use(MenuVariantContext);
return (
{children}
);
}
export function NavigationMenuItemTrailing({
children,
className,
}: NavigationMenuItemTrailingProps) {
const isLink = use(ItemIsLinkContext);
const variant = use(MenuVariantContext);
return (
{children}
{isLink && (
)}
);
}
type NavigationMenuToggleableItemProps = PropsWithChildren<{
checked: boolean;
onCheckedChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
}>;
export function NavigationMenuCheckboxItem({
checked,
onCheckedChange,
disabled,
className,
children,
}: NavigationMenuToggleableItemProps) {
const variant = use(MenuVariantContext);
return (
!disabled && onCheckedChange(!checked)}
className={navigationMenuItemStyle({ variant, interactive: !disabled, className })}
>
{children}
);
}
export function NavigationMenuToggleItem({
checked,
onCheckedChange,
disabled,
className,
children,
}: NavigationMenuToggleableItemProps) {
const variant = use(MenuVariantContext);
return (
!disabled && onCheckedChange(!checked)}
className={navigationMenuItemStyle({ variant, interactive: !disabled, className })}
>
{children}
);
}
================================================
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu-popover.tsx
================================================
import { use, useCallback, useEffect, useRef, useState, type PropsWithChildren, type ReactNode } from "react";
import { useSetAtom } from "jotai";
import { AnimatePresence, LazyMotion } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import * as m from "motion/react-m";
import ChevronsUpDown from "lucide-react/dist/esm/icons/chevrons-up-down";
import { cn } from "@/utils/cn";
import { popoverOverlayAtom } from "@/state/popover-overlay";
import {
InsidePopoverContext,
ItemDisabledContext,
MenuVariantContext,
PopoverContext,
usePopover,
} from "./navigation-menu.contexts";
import {
navigationMenuItemIconStyle,
navigationMenuItemStyle,
navigationMenuStyle,
} from "./navigation-menu.styles";
const POPOVER_INITIAL = { opacity: 1 } as const;
const SHADOW_HIDDEN = { boxShadow: "0 0 0 0 rgba(0,0,0,0)" } as const;
const SHADOW_VISIBLE = {
boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)",
} as const;
const TRIGGER_INITIAL = { height: "fit-content" as const, filter: "blur(4px)", opacity: 1 };
const TRIGGER_ANIMATE = { height: 0, filter: "blur(0)", opacity: 0 };
const TRIGGER_EXIT = { height: "fit-content" as const, filter: "blur(0)", opacity: 1 };
const CONTENT_INITIAL = { height: 0, filter: "blur(0)", opacity: 0 };
const CONTENT_ANIMATE = { height: "fit-content" as const, filter: "blur(0)", opacity: 1 };
const CONTENT_EXIT = { height: 0, filter: "blur(4px)", opacity: 0 };
const POPOVER_CONTENT_STYLE = { maxHeight: "16rem" } as const;
type NavigationMenuPopoverProps = {
trigger: ReactNode;
children: ReactNode;
disabled?: boolean;
};
export function NavigationMenuPopover({
trigger,
children,
disabled,
}: NavigationMenuPopoverProps) {
const [expanded, setExpanded] = useState(false);
const [present, setPresent] = useState(false);
const containerRef = useRef(null);
const setOverlay = useSetAtom(popoverOverlayAtom);
const variant = use(MenuVariantContext);
const close = useCallback(() => {
setExpanded(false);
setOverlay(false);
}, [setOverlay]);
const open = useCallback(() => {
setExpanded(true);
setPresent(true);
setOverlay(true);
}, [setOverlay]);
const toggle = useCallback(() => {
if (expanded) {
close();
return;
}
open();
}, [expanded, close, open]);
useEffect(() => () => setOverlay(false), [setOverlay]);
useEffect(() => {
if (!expanded) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
close();
}
};
const onPointerDown = (event: PointerEvent) => {
if (
containerRef.current
&& event.target instanceof Node
&& !containerRef.current.contains(event.target)
) {
close();
}
};
document.addEventListener("keydown", onKeyDown);
document.addEventListener("pointerdown", onPointerDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("pointerdown", onPointerDown);
};
}, [expanded, close]);
return (
{trigger}
setPresent(false)}>
{expanded && {children} }
);
}
function NavigationMenuPopoverPanel({ children }: PropsWithChildren) {
const { triggerContent } = usePopover();
const variant = use(MenuVariantContext);
return (
{triggerContent}
{children}
);
}
================================================
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu.contexts.ts
================================================
import { createContext, use, type ReactNode } from "react";
import type { MenuVariant } from "./navigation-menu.styles";
export const MenuVariantContext = createContext("default");
export const ItemIsLinkContext = createContext(false);
export const InsidePopoverContext = createContext(false);
export const ItemDisabledContext = createContext(false);
type PopoverContextValue = {
expanded: boolean;
toggle: () => void;
close: () => void;
triggerContent: ReactNode;
};
export const PopoverContext = createContext(null);
export function usePopover() {
const context = use(PopoverContext);
if (!context) {
throw new Error(
"NavigationMenuPopover subcomponents must be used within NavigationMenuPopover",
);
}
return context;
}
================================================
FILE: applications/web/src/components/ui/composites/navigation-menu/navigation-menu.styles.ts
================================================
import { tv, type VariantProps } from "tailwind-variants/lite";
export const navigationMenuStyle = tv({
base: "flex flex-col rounded-2xl p-0.5",
variants: {
variant: {
default: "bg-background-elevated border border-border-elevated shadow-xs",
highlight: "relative before:absolute before:top-0.5 before:inset-x-0 before:h-px before:bg-linear-to-r before:mx-4 before:z-10 before:from-transparent before:to-transparent dark:bg-blue-700 dark:before:via-blue-400 bg-blue-500 before:via-blue-300"
},
},
defaultVariants: {
variant: "default",
},
});
export type MenuVariant = VariantProps["variant"];
export const navigationMenuItemStyle = tv({
base: "rounded-[0.875rem] flex items-center gap-3 p-3.5 sm:p-3 w-full",
variants: {
variant: {
default: "",
highlight: "bg-linear-to-t dark:to-blue-600 dark:from-blue-700 to-blue-500 from-blue-600",
},
interactive: {
true: "hover:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
false: "",
},
},
compoundVariants: [
{ variant: "default", interactive: true, className: "hover:bg-background-hover" },
{ variant: "highlight", interactive: true, className: "hover:brightness-110" },
],
defaultVariants: {
variant: "default",
interactive: true,
},
});
export const navigationMenuItemIconStyle = tv({
base: "shrink-0",
variants: {
variant: {
default: "text-foreground-muted",
highlight: "text-white",
},
disabled: {
true: "text-foreground-disabled",
},
},
defaultVariants: {
variant: "default",
},
});
export const navigationMenuToggleTrack = tv({
base: "w-8 h-5 rounded-full shrink-0 flex items-center p-0.5",
variants: {
variant: {
default: "",
highlight: "",
},
checked: {
true: "",
false: "",
},
disabled: {
true: "opacity-30",
false: "",
},
},
compoundVariants: [
{ variant: "default", checked: false, className: "bg-interactive-border" },
{ variant: "default", checked: true, className: "bg-foreground" },
{ variant: "highlight", checked: false, className: "bg-foreground-inverse-muted" },
{ variant: "highlight", checked: true, className: "bg-foreground-inverse" },
],
defaultVariants: {
variant: "default",
checked: false,
disabled: false,
},
});
export const navigationMenuToggleThumb = tv({
base: "size-4 rounded-full",
variants: {
variant: {
default: "bg-background-elevated",
highlight: "bg-foreground",
},
checked: {
true: "ml-auto",
false: "",
},
},
defaultVariants: {
variant: "default",
checked: false,
},
});
export const navigationMenuCheckbox = tv({
base: "size-4 rounded shrink-0 flex items-center justify-center border",
variants: {
variant: {
default: "border-interactive-border",
highlight: "border-foreground-inverse-muted",
},
checked: {
true: "",
false: "",
},
},
compoundVariants: [
{ variant: "default", checked: true, className: "bg-foreground border-foreground" },
{ variant: "highlight", checked: true, className: "bg-foreground-inverse border-foreground-inverse" },
],
defaultVariants: {
variant: "default",
checked: false,
},
});
export const navigationMenuCheckboxIcon = tv({
base: "shrink-0",
variants: {
variant: {
default: "text-foreground-inverse",
highlight: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export const LABEL_TONE: Record, "muted" | "inverse" | "highlight"> = {
default: "muted",
highlight: "highlight",
};
export const DISABLED_LABEL_TONE: Record<
NonNullable,
"disabled" | "inverseMuted" | "highlight"
> = {
default: "disabled",
highlight: "highlight",
};
================================================
FILE: applications/web/src/components/ui/primitives/animated-reveal.tsx
================================================
import { AnimatePresence, LazyMotion } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import * as m from "motion/react-m";
import type { ReactNode } from "react";
const HIDDEN = { height: 0, opacity: 0, filter: "blur(4px)" };
const VISIBLE = { height: "fit-content", opacity: 1, filter: "blur(0)" };
const CLIP_STYLE = { overflow: "clip" as const, overflowClipMargin: 4 };
interface AnimatedRevealProps {
show: boolean;
skipInitial?: boolean;
children: ReactNode;
}
export function AnimatedReveal({ show, skipInitial, children }: AnimatedRevealProps) {
return (
{show && (
{children}
)}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/back-button.tsx
================================================
import { useRouter, useCanGoBack, useNavigate } from "@tanstack/react-router";
import ArrowLeft from "lucide-react/dist/esm/icons/arrow-left";
import { Button, ButtonIcon, type ButtonProps } from "./button";
interface BackButtonProps {
fallback?: string;
variant?: ButtonProps["variant"];
size?: ButtonProps["size"];
className?: string;
}
export function BackButton({
fallback = "/dashboard",
variant = "elevated",
size = "compact",
className = "aspect-square",
}: BackButtonProps) {
const router = useRouter();
const canGoBack = useCanGoBack();
const navigate = useNavigate();
const handleBack = () => {
if (canGoBack) return router.history.back();
navigate({ to: fallback });
};
return (
);
}
================================================
FILE: applications/web/src/components/ui/primitives/button.tsx
================================================
import type { ComponentPropsWithoutRef, PropsWithChildren } from "react";
import { Link } from "@tanstack/react-router";
import { tv, type VariantProps } from "tailwind-variants/lite";
const button = tv({
base: "flex items-center gap-1 rounded-xl tracking-tighter border hover:cursor-pointer w-fit font-light focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40 disabled:pointer-events-none",
variants: {
size: {
compact: "px-3 py-1.5 text-sm",
standard: "px-4 py-2.5",
},
variant: {
highlight: "shadow-xs border-transparent bg-foreground text-background hover:bg-foreground-hover",
border: "border-interactive-border shadow-xs bg-background hover:bg-background-hover",
elevated: "border-border-elevated shadow-xs bg-background-elevated hover:bg-background-hover",
ghost: "border-transparent bg-transparent hover:bg-foreground/5",
inverse: "shadow-xs border-transparent bg-white text-neutral-900 hover:bg-neutral-200",
"inverse-ghost": "border-transparent bg-transparent text-neutral-300 hover:bg-white/10 hover:text-white",
destructive: "shadow-xs border-destructive-border bg-destructive-background text-destructive hover:bg-destructive-background-hover",
},
},
defaultVariants: {
size: "standard",
variant: "highlight",
}
})
export type ButtonProps = VariantProps;
type ButtonOptions = ComponentPropsWithoutRef<"button"> & ButtonProps;
type LinkButtonOptions = Omit, "children" | "className"> &
PropsWithChildren & { className?: string }>;
type ExternalLinkButtonOptions = ComponentPropsWithoutRef<"a"> & VariantProps;
export function Button({ children, size, variant, className, ...props }: ButtonOptions) {
return (
{children}
)
}
export function LinkButton({ children, size, variant, className, ...props }: LinkButtonOptions) {
return (
{children}
)
}
export function ExternalLinkButton({ children, size, variant, className, ...props }: ExternalLinkButtonOptions) {
return (
{children}
)
}
export function ButtonText({ children }: PropsWithChildren) {
return {children}
}
export function ButtonIcon({ children }: PropsWithChildren) {
return children
}
================================================
FILE: applications/web/src/components/ui/primitives/checkbox.tsx
================================================
import type { ReactNode } from "react";
import Check from "lucide-react/dist/esm/icons/check";
import { tv } from "tailwind-variants/lite";
import { cn } from "@/utils/cn";
import { Text } from "./text";
const checkboxIndicator = tv({
base: "size-4 rounded shrink-0 flex items-center justify-center border",
variants: {
variant: {
default: "border-interactive-border",
highlight: "border-foreground-inverse-muted",
},
checked: {
true: "",
false: "",
},
},
compoundVariants: [
{ variant: "default", checked: true, className: "bg-foreground border-foreground" },
{ variant: "highlight", checked: true, className: "bg-foreground-inverse border-foreground-inverse" },
],
defaultVariants: {
variant: "default",
checked: false,
},
});
const checkboxIcon = tv({
base: "shrink-0",
variants: {
variant: {
default: "text-foreground-inverse",
highlight: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
type CheckboxVariant = "default" | "highlight";
interface CheckboxIndicatorProps {
checked: boolean;
variant?: CheckboxVariant;
className?: string;
}
export function CheckboxIndicator({ checked, variant, className }: CheckboxIndicatorProps) {
return (
{checked && }
);
}
interface CheckboxProps {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
children?: ReactNode;
className?: string;
}
export function Checkbox({ checked, onCheckedChange, children, className }: CheckboxProps) {
return (
onCheckedChange(!checked)}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
>
{children && {children} }
);
}
================================================
FILE: applications/web/src/components/ui/primitives/collapsible.tsx
================================================
import { type PropsWithChildren, type ReactNode } from "react";
import { tv } from "tailwind-variants/lite";
const collapsible = tv({
base: "group",
});
const collapsibleTrigger = tv({
base: "flex w-full items-center justify-between gap-8 px-4 py-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden text-foreground hover:text-foreground-muted",
});
const collapsibleIcon = tv({
base: "size-4 text-foreground-muted group-open:rotate-180 flex-shrink-0",
});
const collapsibleContent = tv({
base: "px-4 pb-4",
});
type CollapsibleProps = PropsWithChildren<{
trigger: ReactNode;
className?: string;
}>;
export function Collapsible({ trigger, children, className }: CollapsibleProps) {
return (
{trigger}
{children}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/dashboard-heading.tsx
================================================
import type { PropsWithChildren, ReactNode } from "react";
import { tv } from "tailwind-variants/lite";
import { Text } from "./text";
const dashboardHeading = tv({
base: "font-sans font-medium leading-tight tracking-tight text-foreground overflow-hidden truncate",
variants: {
level: {
1: "text-2xl",
2: "text-lg",
3: "text-md",
},
},
});
type HeadingLevel = 1 | 2 | 3;
type HeadingTag = "h1" | "h2" | "h3" | "span" | "p";
type DashboardHeadingProps = PropsWithChildren<{ level: HeadingLevel; as?: HeadingTag; className?: string }>;
const tags = { 1: "h1", 2: "h2", 3: "h3" } as const;
function DashboardHeadingBase({ children, level, as, className }: DashboardHeadingProps) {
const Tag = as ?? tags[level];
return {children} ;
}
export function DashboardHeading1({ children, as, className }: Omit) {
return {children} ;
}
export function DashboardHeading2({ children, as, className }: Omit) {
return {children} ;
}
export function DashboardHeading3({ children, as, className }: Omit) {
return {children} ;
}
type DashboardSectionProps = {
title: ReactNode;
description: ReactNode;
level?: HeadingLevel;
headingClassName?: string;
};
export function DashboardSection({ title, description, level = 2, headingClassName }: DashboardSectionProps) {
return (
{title}
{description}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/delete-confirmation.tsx
================================================
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import { Button, ButtonText } from "./button";
import {
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalTitle,
} from "./modal";
interface DeleteConfirmationProps {
title: string;
description: string;
open: boolean;
onOpenChange: (open: boolean) => void;
deleting: boolean;
onConfirm: () => void;
}
function resolveDeleteLabel(deleting: boolean): string {
if (deleting) return "Deleting...";
return "Delete";
}
export function DeleteConfirmation({
title,
description,
open,
onOpenChange,
deleting,
onConfirm,
}: DeleteConfirmationProps) {
return (
{title}
{description}
{deleting && }
{resolveDeleteLabel(deleting)}
onOpenChange(false)}>
Cancel
);
}
================================================
FILE: applications/web/src/components/ui/primitives/divider.tsx
================================================
import type { PropsWithChildren } from "react";
const dashedLine = "h-px grow bg-[repeating-linear-gradient(to_right,var(--color-interactive-border)_0,var(--color-interactive-border)_4px,transparent_4px,transparent_8px)]";
export function Divider({ children }: PropsWithChildren) {
if (!children) {
return
;
}
return (
);
}
================================================
FILE: applications/web/src/components/ui/primitives/error-state.tsx
================================================
import { Text } from "./text";
import { Button, ButtonText } from "./button";
interface ErrorStateProps {
message?: string;
onRetry?: () => void;
}
export function ErrorState({
message = "Something went wrong. Please try again.",
onRetry,
}: ErrorStateProps) {
return (
{message}
{onRetry && (
Retry
)}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/fade-in.tsx
================================================
import type { PropsWithChildren } from "react";
import { LazyMotion, type HTMLMotionProps, type TargetAndTransition } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import * as m from "motion/react-m";
type Direction = "from-right" | "from-top" | "from-bottom";
const variants: Record = {
"from-right": {
hidden: { opacity: 0, x: 10, filter: "blur(4px)" },
visible: { opacity: 1, x: 0, filter: "blur(0px)" },
},
"from-top": {
hidden: { opacity: 0, y: -10, filter: "blur(4px)" },
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
},
"from-bottom": {
hidden: { opacity: 0, y: 10, filter: "blur(4px)" },
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
},
};
const TRANSITION = { duration: 0.2 } as const;
interface FadeInProps extends HTMLMotionProps<"div"> {
direction: Direction;
}
export function FadeIn({ direction, children, ...props }: PropsWithChildren) {
const { hidden, visible } = variants[direction];
return (
{children}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/github-star-button.tsx
================================================
import { AnimatePresence } from "motion/react";
import Star from "lucide-react/dist/esm/icons/star";
import {
Component,
type PropsWithChildren,
useEffect,
useState,
} from "react";
import useSWRImmutable from "swr/immutable";
import { ButtonText, ExternalLinkButton } from "./button";
import { FadeIn } from "./fade-in";
const SCROLL_THRESHOLD = 32;
const GITHUB_STARS_ENDPOINT_PATH = "/internal/github-stars";
const GITHUB_REPOSITORY_URL = "https://github.com/ridafkih/keeper.sh";
interface GithubStarsResponse {
fetchedAt: string;
count: number;
}
function isGithubStarsResponse(value: unknown): value is GithubStarsResponse {
if (typeof value !== "object" || value === null) return false;
return (
"fetchedAt" in value &&
typeof value.fetchedAt === "string" &&
"count" in value &&
typeof value.count === "number" &&
Number.isInteger(value.count) &&
value.count >= 0
);
}
function formatStarCount(starCount: number): string {
return new Intl.NumberFormat("en-US", {
compactDisplay: "short",
maximumFractionDigits: 1,
notation: "compact",
}).format(starCount);
}
async function fetchGithubStarCount(url: string): Promise {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`GitHub stars request failed: ${response.status} ${response.statusText}`,
);
}
const json: unknown = await response.json();
if (!isGithubStarsResponse(json)) {
throw new Error("Invalid GitHub stars payload");
}
return json.count;
}
interface GithubStarButtonProps {
initialStarCount: number | null;
}
interface GithubStarButtonShellProps {
countLabel?: string;
}
interface GithubStarErrorBoundaryState {
hasError: boolean;
}
class GithubStarErrorBoundary extends Component<
PropsWithChildren,
GithubStarErrorBoundaryState
> {
state: GithubStarErrorBoundaryState = {
hasError: false,
};
static getDerivedStateFromError(): GithubStarErrorBoundaryState {
return {
hasError: true,
};
}
render() {
if (this.state.hasError) {
return ;
}
return this.props.children;
}
}
function GithubStarButtonShell({ countLabel }: GithubStarButtonShellProps) {
return (
{typeof countLabel === "string" ? {countLabel} : null}
);
}
function GithubStarButtonCount({ initialStarCount }: GithubStarButtonProps) {
const { data: starCount, error } = useSWRImmutable(
GITHUB_STARS_ENDPOINT_PATH,
fetchGithubStarCount,
typeof initialStarCount === "number"
? { fallbackData: initialStarCount }
: undefined,
);
if (error) {
return ;
}
if (typeof starCount !== "number") {
return ;
}
const formattedStarCount = formatStarCount(starCount);
return ;
}
export function GithubStarButton({ initialStarCount }: GithubStarButtonProps) {
const [visible, setVisible] = useState(true);
useEffect(() => {
const onScroll = () => setVisible(window.scrollY <= SCROLL_THRESHOLD);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
{visible && (
)}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/heading.tsx
================================================
import type { PropsWithChildren } from "react";
import { tv } from "tailwind-variants/lite";
const heading = tv({
base: "font-lora font-medium leading-tight -tracking-[0.075em] text-foreground",
variants: {
level: {
1: "text-4xl",
2: "text-2xl",
3: "text-xl",
},
},
});
type HeadingLevel = 1 | 2 | 3;
type HeadingTag = "h1" | "h2" | "h3" | "span" | "p";
type HeadingProps = PropsWithChildren<{ level: HeadingLevel; as?: HeadingTag; className?: string }>;
const tags = { 1: "h1", 2: "h2", 3: "h3" } as const;
function HeadingBase({ children, level, as, className }: HeadingProps) {
const Tag = as ?? tags[level];
return {children} ;
}
export function Heading1({ children, as, className }: Omit) {
return {children} ;
}
export function Heading2({ children, as, className }: Omit) {
return {children} ;
}
export function Heading3({ children, as, className }: Omit) {
return {children} ;
}
================================================
FILE: applications/web/src/components/ui/primitives/input.tsx
================================================
import type { ComponentPropsWithoutRef, Ref } from "react";
import { tv, type VariantProps } from "tailwind-variants/lite";
const input = tv({
base: "w-full rounded-xl border border-interactive-border bg-background px-4 py-2.5 text-foreground tracking-tight placeholder:text-foreground-muted disabled:opacity-50 disabled:cursor-not-allowed read-only:cursor-not-allowed read-only:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
variants: {
tone: {
neutral: "",
error: "border-destructive dark:border-destructive",
},
},
defaultVariants: {
tone: "neutral",
},
});
type InputProps = ComponentPropsWithoutRef<"input"> & VariantProps & {
ref?: Ref;
};
export function Input({ tone, className, ref, ...props }: InputProps) {
return ;
}
================================================
FILE: applications/web/src/components/ui/primitives/list.tsx
================================================
import type { ComponentPropsWithoutRef, PropsWithChildren } from "react";
type ListProps = PropsWithChildren>;
type OrderedListProps = PropsWithChildren>;
type ListItemProps = PropsWithChildren>;
export function UnorderedList({ children, className, ...props }: ListProps) {
return (
);
}
export function OrderedList({ children, className, ...props }: OrderedListProps) {
return (
{children}
);
}
export function ListItem({ children, className, ...props }: ListItemProps) {
return (
ol]:mt-2 [&>p]:my-0 [&>ul]:mt-2",
className,
].filter(Boolean).join(" ")}
{...props}
>
{children}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/markdown-component-map.ts
================================================
import type { Components } from "streamdown";
import {
MarkdownBlockquote,
MarkdownCodeBlock,
MarkdownHeadingOne,
MarkdownHeadingThree,
MarkdownHeadingTwo,
MarkdownInlineCode,
MarkdownLink,
MarkdownListItem,
MarkdownOrderedList,
MarkdownParagraph,
MarkdownRule,
MarkdownTable,
MarkdownTableCell,
MarkdownTableHeader,
MarkdownUnorderedList,
} from "./markdown-components";
export const markdownComponents: Components = {
a: MarkdownLink,
blockquote: MarkdownBlockquote,
code: MarkdownInlineCode,
h1: MarkdownHeadingOne,
h2: MarkdownHeadingTwo,
h3: MarkdownHeadingThree,
hr: MarkdownRule,
inlineCode: MarkdownInlineCode,
li: MarkdownListItem,
ol: MarkdownOrderedList,
p: MarkdownParagraph,
pre: MarkdownCodeBlock,
table: MarkdownTable,
td: MarkdownTableCell,
th: MarkdownTableHeader,
ul: MarkdownUnorderedList,
};
================================================
FILE: applications/web/src/components/ui/primitives/markdown-components.tsx
================================================
import type { JSX } from "react";
import { Heading1, Heading2, Heading3 } from "./heading";
import { ListItem, OrderedList, UnorderedList } from "./list";
import { Text } from "./text";
type MarkdownElementProps =
JSX.IntrinsicElements[Tag] & {
node?: unknown;
};
function isExternalHttpLink(href: string): boolean {
return href.startsWith("http://") || href.startsWith("https://");
}
export function MarkdownHeadingOne({ children }: MarkdownElementProps<"h1">) {
return {children} ;
}
export function MarkdownHeadingTwo({ children }: MarkdownElementProps<"h2">) {
return {children} ;
}
export function MarkdownHeadingThree({ children }: MarkdownElementProps<"h3">) {
return {children} ;
}
export function MarkdownParagraph({ children }: MarkdownElementProps<"p">) {
return (
{children}
);
}
export function MarkdownLink({
children,
href,
title,
}: MarkdownElementProps<"a">) {
const normalizedHref = typeof href === "string" ? href : "#";
const normalizedTitle = typeof title === "string" ? title : undefined;
return (
{children}
);
}
export function MarkdownUnorderedList({
children,
}: MarkdownElementProps<"ul">) {
return {children} ;
}
export function MarkdownOrderedList({
children,
}: MarkdownElementProps<"ol">) {
return {children} ;
}
export function MarkdownListItem({ children }: MarkdownElementProps<"li">) {
return {children} ;
}
export function MarkdownInlineCode({ children }: MarkdownElementProps<"code">) {
return (
{children}
);
}
export function MarkdownCodeBlock({
children,
}: MarkdownElementProps<"pre">) {
return (
{children}
);
}
export function MarkdownBlockquote({ children }: MarkdownElementProps<"blockquote">) {
return (
{children}
);
}
export function MarkdownRule() {
return ;
}
export function MarkdownTable({
children,
}: MarkdownElementProps<"table">) {
return (
);
}
export function MarkdownTableHeader({
children,
}: MarkdownElementProps<"th">) {
return {children} ;
}
export function MarkdownTableCell({
children,
}: MarkdownElementProps<"td">) {
return {children} ;
}
================================================
FILE: applications/web/src/components/ui/primitives/modal.tsx
================================================
import type { PropsWithChildren } from "react";
import { createContext, use, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useSetAtom } from "jotai";
import { Heading3 } from "./heading";
import { Text } from "./text";
import { popoverOverlayAtom } from "@/state/popover-overlay";
interface ModalContextValue {
open: boolean;
setOpen: (open: boolean) => void;
}
const ModalContext = createContext(null);
function useModal() {
const ctx = use(ModalContext);
if (!ctx) throw new Error("Modal subcomponents must be used within ");
return ctx;
}
interface ModalProps extends PropsWithChildren {
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function Modal({ children, open: controlledOpen, onOpenChange }: ModalProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = (value: boolean) => {
onOpenChange?.(value);
if (controlledOpen === undefined) setUncontrolledOpen(value);
};
return (
{children}
);
}
export function ModalContent({ children }: PropsWithChildren) {
const { open, setOpen } = useModal();
const contentRef = useRef(null);
const setOverlay = useSetAtom(popoverOverlayAtom);
useEffect(() => {
if (!open) return;
setOverlay(true);
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
setOverlay(false);
};
}, [open, setOpen, setOverlay]);
if (!open) return null;
return createPortal(
{
if (!(event.target instanceof Node)) return;
if (contentRef.current && !contentRef.current.contains(event.target)) {
setOpen(false);
}
}}
>
{children}
,
document.body,
);
}
export function ModalTitle({ children }: PropsWithChildren) {
return {children} ;
}
export function ModalDescription({ children }: PropsWithChildren) {
return {children} ;
}
export function ModalFooter({ children }: PropsWithChildren) {
return {children}
;
}
================================================
FILE: applications/web/src/components/ui/primitives/pagination.tsx
================================================
import type { PropsWithChildren } from "react";
import ChevronLeft from "lucide-react/dist/esm/icons/chevron-left";
import ChevronRight from "lucide-react/dist/esm/icons/chevron-right";
import { Button, LinkButton, ButtonIcon } from "./button";
export function Pagination({ children }: PropsWithChildren) {
return {children}
;
}
export function PaginationPrevious({ to, onMouseEnter }: { to?: string; onMouseEnter?: () => void }) {
if (!to) {
return (
);
}
return (
);
}
export function PaginationNext({ to, onMouseEnter }: { to?: string; onMouseEnter?: () => void }) {
if (!to) {
return (
);
}
return (
);
}
================================================
FILE: applications/web/src/components/ui/primitives/provider-icon-stack.tsx
================================================
import { AnimatePresence, LazyMotion } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import * as m from "motion/react-m";
import { Text } from "./text";
import { ProviderIcon } from "./provider-icon";
interface ProviderIconStackProps {
providers: { provider?: string; calendarType?: string }[];
max?: number;
animate?: boolean;
}
function ProviderIconStackItem({ provider, calendarType }: { provider?: string; calendarType?: string }) {
return (
);
}
const HIDDEN = { opacity: 0, filter: "blur(4px)", width: 0 };
const VISIBLE = { opacity: 1, filter: "blur(0)", width: "auto" };
function resolveInitial(animate: boolean) {
if (animate) return HIDDEN;
return false as const;
}
function ProviderIconStack({ providers, max = 4, animate = false }: ProviderIconStackProps) {
const visible = providers.slice(0, max);
const overflow = providers.length - max;
const initial = resolveInitial(animate);
return (
{visible.map((entry, index) => (
))}
{overflow > 0 && (
+{overflow}
)}
);
}
export { ProviderIconStack };
================================================
FILE: applications/web/src/components/ui/primitives/provider-icon.tsx
================================================
import Calendar from "lucide-react/dist/esm/icons/calendar";
import LinkIcon from "lucide-react/dist/esm/icons/link";
import { providerIcons } from "@/lib/providers";
interface ProviderIconProps {
provider?: string;
calendarType?: string;
size?: number;
}
function resolveIconPath(provider: string | undefined): string | undefined {
if (provider) return providerIcons[provider];
return undefined;
}
function ProviderIcon({ provider, calendarType, size = 15 }: ProviderIconProps) {
if (calendarType === "ical" || provider === "ics") {
return ;
}
const iconPath = resolveIconPath(provider);
if (!iconPath) {
return ;
}
return ;
}
export { ProviderIcon };
================================================
FILE: applications/web/src/components/ui/primitives/shimmer-text.tsx
================================================
import type { PropsWithChildren } from "react";
import { cn } from "@/utils/cn";
type ShimmerTextProps = PropsWithChildren<{
className?: string;
}>;
export function ShimmerText({ children, className }: ShimmerTextProps) {
return (
{children}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/staggered-backdrop-blur.tsx
================================================
const LAYERS = [
{ z: 1, mask: "linear-gradient(to top, rgba(0,0,0,0) 0%, rgb(0,0,0) 12.5%, rgb(0,0,0) 25%, rgba(0,0,0,0) 37.5%)", blur: "blur(0.234375px)" },
{ z: 2, mask: "linear-gradient(to top, rgba(0,0,0,0) 12.5%, rgb(0,0,0) 25%, rgb(0,0,0) 37.5%, rgba(0,0,0,0) 50%)", blur: "blur(0.46875px)" },
{ z: 3, mask: "linear-gradient(to top, rgba(0,0,0,0) 25%, rgb(0,0,0) 37.5%, rgb(0,0,0) 50%, rgba(0,0,0,0) 62.5%)", blur: "blur(0.9375px)" },
{ z: 4, mask: "linear-gradient(to top, rgba(0,0,0,0) 37.5%, rgb(0,0,0) 50%, rgb(0,0,0) 62.5%, rgba(0,0,0,0) 75%)", blur: "blur(1.875px)" },
{ z: 5, mask: "linear-gradient(to top, rgba(0,0,0,0) 50%, rgb(0,0,0) 62.5%, rgb(0,0,0) 75%, rgba(0,0,0,0) 87.5%)", blur: "blur(3.75px)" },
{ z: 6, mask: "linear-gradient(to top, rgba(0,0,0,0) 62.5%, rgb(0,0,0) 75%, rgb(0,0,0) 87.5%, rgba(0,0,0,0) 100%)", blur: "blur(7.5px)" },
{ z: 7, mask: "linear-gradient(to top, rgba(0,0,0,0) 75%, rgb(0,0,0) 87.5%, rgb(0,0,0) 100%)", blur: "blur(15px)" },
{ z: 8, mask: "linear-gradient(to top, rgba(0,0,0,0) 87.5%, rgb(0,0,0) 100%)", blur: "blur(30px)" },
];
export function StaggeredBackdropBlur() {
return (
{LAYERS.map(({ z, mask, blur }) => (
))}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/template-text.tsx
================================================
import { tv } from "tailwind-variants/lite";
import { parseTemplate } from "@/utils/templates";
const templateVariable = tv({
variants: {
state: {
known: "text-template",
unknown: "text-template-muted",
disabled: "text-template-muted",
},
},
defaultVariants: {
state: "unknown",
},
});
interface TemplateTextProps {
template: string;
variables: Record;
disabled?: boolean;
className?: string;
}
export function TemplateText({ template, variables, disabled, className }: TemplateTextProps) {
const segments = parseTemplate(template);
return (
{segments.map((segment, i) => {
if (segment.type === "text") {
return {segment.value} ;
}
const state = disabled ? "disabled" : segment.name in variables ? "known" : "unknown";
return (
{`{{${segment.name}}}`}
);
})}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/text-link.tsx
================================================
import type { ComponentPropsWithoutRef, PropsWithChildren } from "react";
import { Link } from "@tanstack/react-router";
import { tv, type VariantProps } from "tailwind-variants/lite";
const textLink = tv({
base: "tracking-tight underline underline-offset-2",
variants: {
size: {
base: "text-base",
sm: "text-sm",
xs: "text-xs",
},
tone: {
muted: "text-foreground-muted hover:text-foreground",
default: "text-foreground",
},
align: {
center: "text-center",
left: "text-left",
},
},
defaultVariants: {
size: "sm",
tone: "muted",
align: "center",
},
});
type TextLinkProps = Omit, "children" | "className"> &
PropsWithChildren & { className?: string }>;
type ExternalTextLinkProps = ComponentPropsWithoutRef<"a"> &
PropsWithChildren & { className?: string }>;
export function TextLink({ children, size, tone, align, className, ...props }: TextLinkProps) {
return (
{children}
);
}
export function ExternalTextLink({
children,
size,
tone,
align,
className,
...props
}: ExternalTextLinkProps) {
return (
{children}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/text.tsx
================================================
import type { CSSProperties, PropsWithChildren } from "react";
import { tv } from "tailwind-variants/lite";
const text = tv({
base: "tracking-tight",
variants: {
size: {
base: "text-base",
sm: "text-sm",
xs: "text-xs",
},
tone: {
muted: "text-foreground-muted",
disabled: "text-foreground-disabled",
highlight: "text-white",
inverse: "text-foreground-inverse",
inverseMuted: "text-foreground-inverse-muted",
default: "text-foreground",
danger: "text-red-500",
},
align: {
center: "text-center",
left: "text-left",
right: "text-right",
},
},
defaultVariants: {
size: "base",
tone: "muted",
align: "left",
},
});
type TextProps = PropsWithChildren<{
as?: "p" | "span";
size?: "base" | "sm" | "xs";
tone?: "muted" | "disabled" | "inverse" | "inverseMuted" | "default" | "danger" | "highlight";
align?: "center" | "left" | "right";
className?: string;
style?: CSSProperties;
}>;
export function Text({ as = "p", children, size, tone, align, className, style }: TextProps) {
const Element = as;
return {children} ;
}
================================================
FILE: applications/web/src/components/ui/primitives/tooltip.tsx
================================================
import { type PropsWithChildren, type ReactNode, useRef, useState, useCallback } from "react";
import { createPortal } from "react-dom";
import { Text } from "./text";
const GAP = 4;
const ABOVE_CLEARANCE = 32;
type TooltipProps = PropsWithChildren<{
content: ReactNode;
}>;
export function Tooltip({ children, content }: TooltipProps) {
const [visible, setVisible] = useState(false);
const wrapperRef = useRef(null);
const [position, setPosition] = useState({ x: 0, y: 0, above: true });
const show = useCallback(() => {
const child = wrapperRef.current?.firstElementChild;
if (!child) return;
const rect = child.getBoundingClientRect();
const above = rect.top >= ABOVE_CLEARANCE;
setPosition({
x: rect.left + rect.width / 2,
y: above ? rect.top - GAP : rect.bottom + GAP,
above,
});
setVisible(true);
}, []);
const hide = useCallback(() => setVisible(false), []);
return (
{children}
{visible && createPortal(
,
document.body,
)}
);
}
================================================
FILE: applications/web/src/components/ui/primitives/upgrade-hint.tsx
================================================
import type { PropsWithChildren, ReactNode } from "react";
import { Link } from "@tanstack/react-router";
import { getCommercialMode } from "@/config/commercial";
import { Text } from "./text";
function UpgradeHint({ children }: PropsWithChildren) {
if (!getCommercialMode()) return null;
return (
{children}{" "}
Upgrade to Pro
);
}
function PremiumFeatureGate({ locked, children, hint }: { locked: boolean; children: ReactNode; hint: string }) {
if (!locked || !getCommercialMode()) return <>{children}>;
return (
{children}
{hint}{" "}
Upgrade to Pro
);
}
export { UpgradeHint, PremiumFeatureGate };
================================================
FILE: applications/web/src/components/ui/shells/layout.tsx
================================================
import type { PropsWithChildren } from "react";
import { cn } from "@/utils/cn";
const GRID_COLS = "grid grid-cols-[minmax(1rem,1fr)_minmax(auto,48rem)_minmax(1rem,1fr)]";
export function Layout({ children }: PropsWithChildren) {
return (
{children}
)
}
export function LayoutItem({ children }: PropsWithChildren) {
return (
{children}
)
}
export function LayoutRow({ children, className }: PropsWithChildren<{ className?: string }>) {
return (
)
}
================================================
FILE: applications/web/src/components/ui/shells/route-shell.tsx
================================================
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import { BackButton } from "@/components/ui/primitives/back-button";
import { ErrorState } from "@/components/ui/primitives/error-state";
type RouteShellProps = {
backFallback?: string;
} & (
| { status: "loading" }
| { status: "error"; onRetry: () => void }
| { status: "ready" }
);
export function RouteShell(props: RouteShellProps) {
if (props.status === "error") {
return (
);
}
if (props.status === "loading") {
return (
);
}
return null;
}
================================================
FILE: applications/web/src/components/ui/shells/session-slot.tsx
================================================
import { type ReactNode, useSyncExternalStore } from "react";
import { useRouteContext } from "@tanstack/react-router";
import { hasSessionCookie } from "@/lib/session-cookie";
interface SessionSlotProps {
authenticated: ReactNode;
unauthenticated: ReactNode;
}
const subscribe = () => () => {};
const getSnapshot = () => hasSessionCookie();
export function SessionSlot({ authenticated, unauthenticated }: SessionSlotProps) {
const { auth } = useRouteContext({ strict: false });
const getServerSnapshot = () => auth?.hasSession() ?? false;
const isAuthenticated = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isAuthenticated ? authenticated : unauthenticated;
}
================================================
FILE: applications/web/src/config/commercial.ts
================================================
import { getPublicRuntimeConfig } from "@/lib/runtime-config";
export const getCommercialMode = () => getPublicRuntimeConfig().commercialMode;
================================================
FILE: applications/web/src/config/gdpr.ts
================================================
/** ISO 3166-1 alpha-2 country codes for EU/EEA + UK where GDPR applies. */
const GDPR_COUNTRIES = new Set([
"AT",
"BE",
"BG",
"CY",
"CZ",
"DE",
"DK",
"EE",
"ES",
"FI",
"FR",
"GB",
"GR",
"HR",
"HU",
"IE",
"IS",
"IT",
"LI",
"LT",
"LU",
"LV",
"MT",
"NL",
"NO",
"PL",
"PT",
"RO",
"SE",
"SI",
"SK",
]);
export { GDPR_COUNTRIES };
================================================
FILE: applications/web/src/config/plans.ts
================================================
import type { PublicRuntimeConfig } from "@/lib/runtime-config";
export interface PlanConfig {
id: "free" | "pro";
name: string;
description: string;
monthlyPrice: number;
yearlyPrice: number;
monthlyProductId: string | null;
yearlyProductId: string | null;
features: string[];
}
const basePlans: Omit[] = [
{
id: "free",
name: "Free",
description: "For personal use and getting started with calendar sync.",
monthlyPrice: 0,
yearlyPrice: 0,
features: [
"Up to 2 linked accounts",
"Up to 3 sync mappings",
"Aggregated iCal feed",
"Syncing every 30 minutes",
"API access (25 calls/day)",
],
},
{
id: "pro",
name: "Pro",
description: "For power users who need fast syncs, advanced feed controls, and unlimited syncing.",
monthlyPrice: 5,
yearlyPrice: 42,
features: [
"Syncing every 1 minute",
"Unlimited linked accounts",
"Unlimited sync mappings",
"Event filters, exclusions, and iCal feed customization",
"Unlimited API & MCP access",
"Priority support",
],
},
];
export const getPlans = (runtimeConfig: PublicRuntimeConfig): PlanConfig[] =>
basePlans.map((plan): PlanConfig => {
if (plan.id === "pro") {
return {
...plan,
monthlyProductId: runtimeConfig.polarProMonthlyProductId,
yearlyProductId: runtimeConfig.polarProYearlyProductId,
};
}
return { ...plan, monthlyProductId: null, yearlyProductId: null };
});
================================================
FILE: applications/web/src/content/blog/introducing-keeper-blog.mdx
================================================
---
title: "Why I Built an Open-Source Calendar Syncing Tool"
description: "Why I built an open-source calendar syncing tool to sync availability across Google Calendar, Outlook, FastMail, iCloud, Apple Calendar, and CalDAV."
blurb: "I had four calendars across Google, FastMail, and iCloud, and built an open-source calendar syncing tool to keep time slots aligned without duplicates or heavy setup."
createdAt: "2025-12-28"
slug: "why-i-built-an-open-source-calendar-syncing-tool"
updatedAt: "2026-03-09"
tags:
- "calendar"
- "product"
- "sync"
- "self-hosting"
---
I have four calendars spread across providers:
- A work calendar on Google Calendar
- A business calendar on Google Calendar
- A business-personal calendar on FastMail
- A personal calendar on iCloud
That setup gave different people in my life different and incomplete windows into my availability, whether they were my co-founder, investors, coworkers, or friends. The result was constant overlap and way too much scheduling back-and-forth, even though the problem I was trying to solve was simple: block off the same time slots everywhere.
## Why Keeper.sh exists
I tried a few options and kept running into the same issues:
- Too much manual setup
- Finicky sync behavior and duplicate events
- Pricing that felt heavy without a self-hosted path
Some tools only worked between two providers. Others required complex Zapier-style automations that broke silently. And none of them gave me the privacy controls I wanted — I didn't need my dentist appointment title showing up on my work calendar. I just needed the time blocked.
After repeating that cycle enough times, I built Keeper.sh to do exactly what I wanted in the first place: reliable slot syncing without a bunch of ongoing babysitting.
## How the sync engine works
Keeper.sh uses a pull-compare-push architecture. On each sync cycle, it pulls events from your configured source calendars, compares them against what it already knows, and pushes changes to your destination calendars.
The core logic is straightforward:
- **Add:** If a source event has no corresponding mapping on the destination, it gets created
- **Delete:** If a mapping exists but the source event no longer does, the destination event gets removed
- **Cleanup:** Any Keeper-created events with no mapping are cleaned up for consistency
Events created by Keeper are tagged with a `@keeper.sh` suffix on their remote UID so they can be identified later. For platforms like Outlook that don't support custom UIDs, we use a `"keeper.sh"` category instead.
To prevent race conditions, each sync operation acquires a Redis-backed generation counter. If two syncs try to run on the same calendar at the same time, the later one backs off. This keeps things consistent without requiring heavy locking.
For Google and Outlook, Keeper uses incremental sync tokens. Instead of re-fetching every event on each cycle, it asks the provider for only what changed since the last sync. This makes regular sync cycles fast and lightweight, even for calendars with thousands of events.
## What Keeper.sh supports
Keeper.sh works with six calendar providers across two authentication mechanisms:
**OAuth providers:**
- **Google Calendar** — full read/write with support for filtering Focus Time, Working Location, and Out of Office events
- **Microsoft Outlook** — full read/write via the Microsoft Graph API
**CalDAV-based providers:**
- **FastMail** — pre-configured CalDAV with FastMail's server URL
- **iCloud** — pre-configured CalDAV with app-specific password support
- **Generic CalDAV** — any CalDAV-compatible server with username/password auth
- **iCal/ICS feeds** — read-only public URL-based calendars
Each calendar in Keeper has a capability flag: "pull" means it can be used as a source (read events from), and "push" means it can be used as a destination (write events to). iCal feeds, for example, are pull-only since they're read-only by nature. OAuth and CalDAV calendars support both.
## Privacy controls
By default, Keeper.sh shares only busy/free time blocks. Event titles, descriptions, locations, and attendee lists are stripped before syncing. This is the behavior most people actually want — block the time, don't leak the details.
But if you need more flexibility, each source calendar has granular settings:
- **Include or exclude event titles** — show the actual event name or replace it with "Busy"
- **Include or exclude descriptions and locations** — control what metadata gets synced
- **Custom event name templates** — use `{{event_name}}` or `{{calendar_name}}` placeholders to build your own format
- **Skip all-day events** — useful if you don't want holidays or reminders blocking time
- **Skip Google-specific event types** — filter out Focus Time, Working Location, or Out of Office blocks
These same controls apply to the iCal feed. When you generate your aggregated feed URL, you choose exactly what level of detail goes into it.
## The iCal feed
One of the features I use most is the aggregated iCal feed. Keeper generates a unique, token-authenticated URL that combines events from all your selected source calendars into a single feed. You can subscribe to it from any calendar app that supports iCal — Apple Calendar, Thunderbird, or any CalDAV client.
The feed respects all your privacy settings. If you've chosen to exclude event titles, the feed shows "Busy" blocks. If you've included titles and locations, those show up too. It's the same event data, just served as a subscribable feed instead of pushed to a specific calendar.
This is especially useful for sharing availability externally. Instead of giving someone access to your actual calendar, you hand them a feed URL that shows exactly what you want them to see.
## Source and destination mappings
The mapping model is what makes Keeper flexible. You connect calendar accounts, then configure which calendars are sources and which are destinations. A single source can push to multiple destinations, and a single destination can receive from multiple sources.
The setup flow walks you through it:
1. Connect an account (OAuth, CalDAV, or iCal URL)
2. Select which calendars to use
3. Rename them if you want clearer labels
4. Map sources to destinations (and optionally the reverse)
Once configured, syncing is automatic. Free tier users sync every 30 minutes, Pro users sync every minute, and self-hosted instances sync every minute regardless of plan.
## Free vs. Pro
Keeper.sh uses a low-cost freemium model:
- **Free:** 2 source calendars, 1 destination, 30-minute sync intervals, aggregated iCal feed
- **Pro ($5/month):** Unlimited sources, unlimited destinations, 1-minute sync intervals, priority support
If you self-host, all users on your instance get Pro-tier features by default. The commercial tier exists to sustain the hosted version, not to gate core functionality behind a paywall.
## Self-hosting
I open-sourced Keeper.sh because I've gotten really into self-hosting and home servers, and I wanted this to be usable on your own infrastructure. The entire stack runs on:
- **PostgreSQL** for event state, account data, and sync mappings
- **Redis** for sync coordination and session storage
- **Bun** for the API server and cron workers
- **Vite + React** for the web interface
There are three deployment models depending on how much control you want:
**Standalone (easiest):** A single `compose.yaml` that bundles everything — Caddy reverse proxy, PostgreSQL, Redis, API, web, and cron. One command to start, auto-configured.
**Services-only:** Just the application containers (web, API, cron). You bring your own PostgreSQL and Redis. Good if you already have a database server running.
**Individual containers:** Separate `keeper-web`, `keeper-api`, and `keeper-cron` images. The most flexible setup for production deployments.
If you want Google or Outlook connected as sources or destinations, you'll still need:
- A Google OAuth client (Google Cloud)
- A Microsoft OAuth app (Azure)
CalDAV-based providers (FastMail, iCloud, generic CalDAV) and iCal feeds work without any OAuth setup.
The [`compose.yaml` setup in the README](https://github.com/ridafkih/keeper.sh/blob/main/README.md) is the fastest way to get up and running.
## Technical choices
A few decisions that shaped the architecture:
**CalDAV as the universal protocol.** Rather than building custom integrations for every provider, Keeper treats CalDAV (RFC 4791) as the common layer for FastMail, iCloud, and any standards-compliant server. This means adding a new CalDAV provider is mostly a configuration change, not new code.
**Encrypted credentials at rest.** CalDAV passwords and OAuth tokens are encrypted before storage using a configurable encryption key. OAuth tokens include refresh token rotation, so sessions stay valid without storing long-lived credentials.
**Content hashing for iCal feeds.** When pulling from iCal/ICS URLs, Keeper hashes the response content and skips processing if nothing changed. This avoids redundant work when feeds haven't been updated and keeps snapshots for 6 hours in case something goes wrong.
**Real-time sync status.** The API server runs a WebSocket endpoint that broadcasts sync progress to the dashboard. You can watch events get pulled, compared, and pushed in real time rather than wondering if a sync cycle actually ran.
## Built with user feedback
Thanks to user feedback, Keeper.sh has grown way beyond the original basic slot-syncing setup. Community input helped shape features like syncing event titles, descriptions, and locations when needed, along with controls to exclude specific event types from syncing.
Those improvements made Keeper.sh much more flexible for different workflows and privacy preferences, and there's still more coming as people keep sharing real-world use cases.
If you want to try Keeper.sh, you can [sign up for the hosted version](https://keeper.sh/register) or [grab the source on GitHub](https://github.com/ridafkih/keeper.sh).
================================================
FILE: applications/web/src/features/auth/components/auth-form.tsx
================================================
import { useEffect, useRef, type Ref, type SubmitEvent } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useAtomValue, useSetAtom } from "jotai";
import { AnimatePresence, LazyMotion, type TargetAndTransition, type Variants } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import * as m from "motion/react-m";
import ArrowLeft from "lucide-react/dist/esm/icons/arrow-left";
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import {
authFormStatusAtom,
authFormErrorAtom,
authFormStepAtom,
type AuthFormStatus,
} from "@/state/auth-form";
import { authClient } from "@/lib/auth-client";
import {
getEnabledSocialProviders,
resolveCredentialField,
type AuthCapabilities,
} from "@/lib/auth-capabilities";
import { signInWithCredential, signUpWithCredential } from "@/lib/auth";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import {
Button,
LinkButton,
ExternalLinkButton,
ButtonText,
ButtonIcon,
} from "@/components/ui/primitives/button";
import { Divider } from "@/components/ui/primitives/divider";
import { ExternalTextLink, TextLink } from "@/components/ui/primitives/text-link";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Input } from "@/components/ui/primitives/input";
import { Text } from "@/components/ui/primitives/text";
import { resolveErrorMessage } from "@/utils/errors";
import {
getMcpAuthorizationSearch,
resolvePathWithSearch,
resolveClientPostAuthRedirect,
type StringSearchParams,
} from "@/lib/mcp-auth-flow";
import { AuthSwitchPrompt } from "./auth-switch-prompt";
function resolveInputTone(active: boolean | undefined): "error" | "neutral" {
if (active) return "error";
return "neutral";
}
function resolveSwitchSearch(search?: StringSearchParams): StringSearchParams | undefined {
if (!search) return undefined;
const result = getMcpAuthorizationSearch(search);
if (!result) return undefined;
return result;
}
export type AuthScreenCopy = {
heading: string;
subtitle: string;
oauthActionLabel: string;
submitLabel: string;
switchPrompt: string;
switchCta: string;
switchTo: "/login" | "/register";
action: "signIn" | "signUp";
};
type SocialAuthProvider = {
id: "google" | "microsoft";
label: string;
to: "/auth/google" | "/auth/outlook";
iconSrc: string;
};
const SOCIAL_AUTH_PROVIDERS: readonly SocialAuthProvider[] = [
{ id: "google", label: "Google", to: "/auth/google", iconSrc: "/integrations/icon-google.svg" },
{ id: "microsoft", label: "Outlook", to: "/auth/outlook", iconSrc: "/integrations/icon-outlook.svg" },
];
const submitTextVariants: Record = {
idle: { opacity: 1, filter: "none", y: 0, scale: 1 },
loading: { opacity: 0, filter: "blur(2px)", y: -2, scale: 0.75 },
};
const backButtonVariants: Variants = {
hidden: { width: 0, opacity: 0, filter: "blur(2px)" },
visible: { width: "auto", opacity: 1, filter: "blur(0px)" },
};
function resolvePasswordFieldAnimation(step: "email" | "password"): TargetAndTransition {
if (step === "password") {
return { height: "auto", opacity: 1, overflow: "visible" };
}
return { height: 0, opacity: 0, overflow: "hidden" };
}
export function AuthForm({
capabilities,
copy,
authorizationSearch,
}: {
capabilities: AuthCapabilities;
copy: AuthScreenCopy;
authorizationSearch?: StringSearchParams;
}) {
const hasSocialProviders = getEnabledSocialProviders(capabilities).length > 0;
const switchSearch = resolveSwitchSearch(authorizationSearch);
const switchHref = resolvePathWithSearch(copy.switchTo, switchSearch);
return (
<>
{copy.action === "signIn" && capabilities.supportsPasskeys && (
)}
{copy.heading}
{copy.subtitle}
{hasSocialProviders && (
<>
or
>
)}
{copy.switchPrompt}{" "}
{copy.switchCta}
>
);
}
function redirectAfterAuth(authorizationSearch?: StringSearchParams) {
if (typeof window === "undefined" || !window.location) {
return;
}
const redirectTarget = resolveClientPostAuthRedirect(authorizationSearch);
const nextUrl = new URL(redirectTarget, window.location.origin).toString();
window.location.assign(nextUrl);
}
function usePasskeyAutoFill(authorizationSearch?: StringSearchParams) {
const setError = useSetAtom(authFormErrorAtom);
useEffect(() => {
if (typeof PublicKeyCredential === "undefined") {
return;
}
const controller = new AbortController();
const attemptAutoFill = async () => {
const available = await PublicKeyCredential.isConditionalMediationAvailable?.();
if (!available) return;
const { error } = await authClient.signIn.passkey({
autoFill: true,
fetchOptions: { signal: controller.signal },
});
if (error) {
if ("code" in error && error.code === "AUTH_CANCELLED") {
return;
}
setError({ message: error.message ?? "Passkey sign-in failed.", active: true });
return;
}
redirectAfterAuth(authorizationSearch);
};
void attemptAutoFill();
return () => controller.abort();
}, [authorizationSearch, setError]);
}
interface PasskeyAutoFillProps {
authorizationSearch?: StringSearchParams;
}
function PasskeyAutoFill({ authorizationSearch }: PasskeyAutoFillProps) {
usePasskeyAutoFill(authorizationSearch);
return null;
}
function SocialAuthButtons({
capabilities,
oauthActionLabel,
authorizationSearch,
}: {
capabilities: AuthCapabilities;
oauthActionLabel: string;
authorizationSearch?: StringSearchParams;
}) {
const enabledSocialProviders = new Set(getEnabledSocialProviders(capabilities));
const visibleProviders = SOCIAL_AUTH_PROVIDERS.filter((provider) =>
enabledSocialProviders.has(provider.id));
if (visibleProviders.length === 0) {
return null;
}
return (
<>
{visibleProviders.map((provider) => (
{`${oauthActionLabel} with ${provider.label}`}
))}
>
);
}
function FormBackButton({ step, onBack }: { step: "email" | "password"; onBack: () => void }) {
if (step === "password") return ;
return ;
}
function ForgotPasswordLink({
action,
capabilities,
}: {
action: "signIn" | "signUp";
capabilities: AuthCapabilities;
}) {
if (action !== "signIn" || !capabilities.supportsPasswordReset) return null;
return (
Forgot password?
);
}
function resolveAutoComplete(
action: "signIn" | "signUp",
base: string,
capabilities: AuthCapabilities,
): string {
if (action === "signIn" && capabilities.supportsPasskeys) {
if (base === "email" || base === "username") {
return "username webauthn";
}
}
return base;
}
function readFormFieldValue(formData: FormData, fieldName: string): string {
const value = formData.get(fieldName);
if (typeof value === "string") return value;
return "";
}
function CredentialForm({
capabilities,
submitLabel,
action,
authorizationSearch,
}: {
capabilities: AuthCapabilities;
submitLabel: string;
action: "signIn" | "signUp";
authorizationSearch?: StringSearchParams;
}) {
const navigate = useNavigate();
const step = useAtomValue(authFormStepAtom);
const setStep = useSetAtom(authFormStepAtom);
const setStatus = useSetAtom(authFormStatusAtom);
const setError = useSetAtom(authFormErrorAtom);
const passwordRef = useRef(null);
const credentialField = resolveCredentialField(capabilities);
useEffect(() => {
if (typeof sessionStorage === "undefined") {
return;
}
sessionStorage.removeItem("pendingVerificationEmail");
sessionStorage.removeItem("pendingVerificationCallbackUrl");
}, []);
const handleSubmit = async (event: SubmitEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const credential = readFormFieldValue(formData, credentialField.name);
if (step === "email") {
if (!credential) return;
setStep("password");
requestAnimationFrame(() => passwordRef.current?.focus());
return;
}
const password = readFormFieldValue(formData, "password");
if (!password) return;
setStatus("loading");
const redirectTarget = resolveClientPostAuthRedirect(authorizationSearch);
const authActions: Record Promise> = {
signIn: () => signInWithCredential(credential, password, capabilities),
signUp: () => signUpWithCredential(credential, password, capabilities, redirectTarget),
};
try {
await authActions[action]();
} catch (error) {
setStatus("idle");
setError({
message: resolveErrorMessage(error, "Something went wrong. Please try again."),
active: true,
});
return;
}
if (action === "signUp") {
track(ANALYTICS_EVENTS.signup_completed);
} else if (action === "signIn") {
track(ANALYTICS_EVENTS.login_completed);
}
if (action === "signUp" && capabilities.requiresEmailVerification) {
sessionStorage.setItem("pendingVerificationEmail", credential);
sessionStorage.setItem("pendingVerificationCallbackUrl", redirectTarget);
navigate({ to: "/verify-email" });
return;
}
redirectAfterAuth(authorizationSearch);
};
const handleBack = () => {
setStep("email");
setError(null);
};
return (
);
}
function resolveAuthErrorAnimation(active: boolean | undefined): TargetAndTransition {
if (active) return { height: "auto", opacity: 1, filter: "blur(0px)" };
return { height: 0, opacity: 0, filter: "blur(4px)" };
}
function AuthError() {
const error = useAtomValue(authFormErrorAtom);
const active = error?.active;
return (
{error?.message}
);
}
function CredentialInput({
id,
name,
readOnly,
autoComplete,
label,
onFocus,
placeholder,
type,
}: {
id: string;
name: string;
readOnly?: boolean;
autoComplete?: string;
label: string;
onFocus?: () => void;
placeholder: string;
type: "email" | "text";
}) {
const status = useAtomValue(authFormStatusAtom);
const error = useAtomValue(authFormErrorAtom);
const setError = useSetAtom(authFormErrorAtom);
const clearError = () => {
if (error?.active) setError({ ...error, active: false });
};
return (
<>
{label}
>
);
}
function PasswordInput({
ref,
autoComplete,
tabIndex,
}: {
ref?: Ref;
autoComplete?: string;
tabIndex?: number;
}) {
const status = useAtomValue(authFormStatusAtom);
const error = useAtomValue(authFormErrorAtom);
const setError = useSetAtom(authFormErrorAtom);
const clearError = () => {
if (error?.active) setError({ ...error, active: false });
};
return (
<>
Password
>
);
}
function AnimatedBackWrapper({ children }: { children: React.ReactNode }) {
const status = useAtomValue(authFormStatusAtom);
return (
{status !== "loading" && (
{children}
)}
);
}
function BackButton() {
return (
);
}
function StepBackButton({ onBack }: { onBack: () => void }) {
return (
);
}
function SubmitButton({ children }: { children: string }) {
const status = useAtomValue(authFormStatusAtom);
return (
{children}
{status === "loading" && (
)}
);
}
================================================
FILE: applications/web/src/features/auth/components/auth-switch-prompt.tsx
================================================
import type { PropsWithChildren } from "react";
import { Text } from "@/components/ui/primitives/text";
export function AuthSwitchPrompt({ children }: PropsWithChildren) {
return (
{children}
);
}
================================================
FILE: applications/web/src/features/auth/components/caldav-connect-form.tsx
================================================
import { useRef, useState, useTransition } from "react";
import { useNavigate } from "@tanstack/react-router";
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import { useSWRConfig } from "swr";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { Divider } from "@/components/ui/primitives/divider";
import { Input } from "@/components/ui/primitives/input";
import { Text } from "@/components/ui/primitives/text";
import { apiFetch } from "@/lib/fetcher";
import { invalidateAccountsAndSources } from "@/lib/swr";
import { resolveErrorMessage } from "@/utils/errors";
export type CalDAVProvider = "fastmail" | "icloud" | "caldav";
interface ProviderConfig {
serverUrl: string;
showServerUrlInput: boolean;
usernamePlaceholder: string;
usernameInputType: string;
passwordPlaceholder: string;
}
const PROVIDER_CONFIGS: Record = {
fastmail: {
serverUrl: "https://caldav.fastmail.com/",
showServerUrlInput: false,
usernamePlaceholder: "Fastmail Email Address",
usernameInputType: "email",
passwordPlaceholder: "App-Specific Password",
},
icloud: {
serverUrl: "https://caldav.icloud.com/",
showServerUrlInput: false,
usernamePlaceholder: "Apple ID",
usernameInputType: "email",
passwordPlaceholder: "App-Specific Password",
},
caldav: {
serverUrl: "",
showServerUrlInput: true,
usernamePlaceholder: "CalDAV Server Username",
usernameInputType: "text",
passwordPlaceholder: "CalDAV Server Password",
},
};
interface CalendarOption {
url: string;
displayName: string;
}
interface CalDAVConnectFormProps {
provider: CalDAVProvider;
}
function readFormFieldValue(formData: FormData, fieldName: string): string {
const value = formData.get(fieldName);
if (typeof value === "string") return value;
return "";
}
function isRecord(value: unknown): value is Record {
return typeof value === "object" && value !== null;
}
function isCalendarOption(value: unknown): value is CalendarOption {
if (!isRecord(value)) return false;
return typeof value.url === "string" && typeof value.displayName === "string";
}
function parseCalendarOptions(value: unknown): CalendarOption[] | null {
if (!isRecord(value)) return null;
const calendars = value.calendars;
if (!Array.isArray(calendars)) return null;
if (!calendars.every(isCalendarOption)) return null;
return calendars;
}
function parseAuthMethod(value: unknown): "basic" | "digest" {
if (!isRecord(value)) return "basic";
if (value.authMethod === "digest") return "digest";
return "basic";
}
function parseAccountId(value: unknown): string | undefined {
if (!isRecord(value)) return undefined;
if (typeof value.accountId === "string") return value.accountId;
return undefined;
}
export function CalDAVConnectForm({ provider }: CalDAVConnectFormProps) {
const config = PROVIDER_CONFIGS[provider];
const navigate = useNavigate();
const { mutate: globalMutate } = useSWRConfig();
const formRef = useRef(null);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState(null);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
setError(null);
const formData = new FormData(event.currentTarget);
const serverUrl = readFormFieldValue(formData, "serverUrl");
const username = readFormFieldValue(formData, "username");
const password = readFormFieldValue(formData, "password");
if (!serverUrl || !username || !password) {
setError("Missing required credentials");
return;
}
startTransition(async () => {
let discoverResponse: Response;
try {
discoverResponse = await apiFetch("/api/sources/caldav/discover", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ serverUrl, username, password }),
});
} catch (err) {
setError(resolveErrorMessage(err, "Failed to discover calendars"));
return;
}
const discoverPayload = await discoverResponse.json();
const calendars = parseCalendarOptions(discoverPayload);
const authMethod = parseAuthMethod(discoverPayload);
if (!calendars) {
setError("Failed to parse discovered calendars");
return;
}
if (calendars.length === 0) {
setError("No calendars found");
return;
}
let accountId: string | undefined;
try {
const responses = await Promise.all(
calendars.map((calendar) =>
apiFetch("/api/sources/caldav", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
authMethod,
calendarUrl: calendar.url,
name: calendar.displayName,
password,
provider,
serverUrl,
username,
}),
}),
),
);
const firstResponse = responses[0];
if (firstResponse) {
accountId = parseAccountId(await firstResponse.json());
}
} catch {
setError("Failed to import calendars");
return;
}
await invalidateAccountsAndSources(globalMutate);
if (accountId) {
navigate({ to: "/dashboard/accounts/$accountId/setup", params: { accountId } });
} else {
navigate({ to: "/dashboard" });
}
});
};
return (
);
}
================================================
FILE: applications/web/src/features/auth/components/caldav-connect-page.tsx
================================================
import type { ReactNode } from "react";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { ProviderIconPair } from "./oauth-preamble";
import { CalDAVConnectForm, type CalDAVProvider } from "./caldav-connect-form";
interface CalDAVConnectPageProps {
provider: CalDAVProvider;
icon: ReactNode;
heading: string;
description: string;
steps: ReactNode[];
footer?: ReactNode;
}
export function CalDAVConnectPage({
provider,
icon,
heading,
description,
steps,
footer,
}: CalDAVConnectPageProps) {
return (
<>
{icon}
{heading}
{description}
{steps.map((step, index) => (
{step}
))}
{footer}
>
);
}
================================================
FILE: applications/web/src/features/auth/components/ics-connect-form.tsx
================================================
import { useState, useTransition, type SubmitEvent } from "react";
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import { useNavigate } from "@tanstack/react-router";
import { useSWRConfig } from "swr";
import { apiFetch } from "@/lib/fetcher";
import { invalidateAccountsAndSources } from "@/lib/swr";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { Checkbox } from "@/components/ui/primitives/checkbox";
import { Divider } from "@/components/ui/primitives/divider";
import { Input } from "@/components/ui/primitives/input";
import { Text } from "@/components/ui/primitives/text";
function resolveSubmitLabel(pending: boolean): string {
if (pending) return "Subscribing...";
return "Subscribe";
}
export function ICSFeedForm() {
const navigate = useNavigate();
const { mutate: globalMutate } = useSWRConfig();
const [isPending, startTransition] = useTransition();
const [error, setError] = useState(null);
const [requiresAuth, setRequiresAuth] = useState(false);
const handleSubmit = (event: SubmitEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const rawUrl = formData.get("feed-url");
if (!rawUrl || typeof rawUrl !== "string") return;
let url = rawUrl;
if (requiresAuth) {
const username = formData.get("username");
const password = formData.get("password");
if (!username || !password) {
setError("Username and password are required for authenticated feeds.");
return;
}
try {
const parsed = new URL(rawUrl);
parsed.username = encodeURIComponent(String(username));
parsed.password = encodeURIComponent(String(password));
url = parsed.toString();
} catch {
setError("Invalid URL.");
return;
}
}
setError(null);
startTransition(async () => {
let accountId: string | undefined;
try {
const response = await apiFetch("/api/ics", {
body: JSON.stringify({ name: "iCal Feed", url }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const data = await response.json();
accountId = data?.accountId;
} catch {
setError("Failed to subscribe to feed");
return;
}
await invalidateAccountsAndSources(globalMutate);
if (accountId) {
navigate({ to: "/dashboard/accounts/$accountId/setup", params: { accountId } });
} else {
navigate({ to: "/dashboard" });
}
});
};
return (
);
}
================================================
FILE: applications/web/src/features/auth/components/oauth-preamble.tsx
================================================
import type { ReactNode, SubmitEvent } from "react";
import ArrowLeftRight from "lucide-react/dist/esm/icons/arrow-left-right";
import Check from "lucide-react/dist/esm/icons/check";
import KeeperLogo from "@/assets/keeper.svg?react";
import { authClient } from "@/lib/auth-client";
import {
resolvePathWithSearch,
resolveClientPostAuthRedirect,
type StringSearchParams,
} from "@/lib/mcp-auth-flow";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { ExternalTextLink } from "@/components/ui/primitives/text-link";
import { Divider } from "@/components/ui/primitives/divider";
import { Button, ButtonText } from "@/components/ui/primitives/button";
type Provider = "google" | "outlook" | "microsoft-365";
const PROVIDER_LABELS: Record = {
google: "Google",
outlook: "Outlook",
"microsoft-365": "Microsoft 365",
};
const PERMISSIONS = [
"See your email address",
"View a list of your calendars",
"View events, summaries and details",
"Add or remove calendar events",
];
const PROVIDER_SOCIAL_MAP: Partial> = {
google: "google",
outlook: "microsoft",
};
const PROVIDER_API_MAP: Record = {
google: "google",
outlook: "outlook",
"microsoft-365": "outlook",
};
export function PermissionsList({ items }: { items: readonly string[] }) {
return (
{items.map((item) => (
{item}
))}
);
}
interface PreambleLayoutProps {
provider: Provider;
onSubmit: (event: SubmitEvent) => void;
children?: ReactNode;
}
function PreambleLayout({ provider, onSubmit, children }: PreambleLayoutProps) {
return (
<>
Connect {PROVIDER_LABELS[provider]}
Start importing your events and sync them across all your calendars.
{children}
>
);
}
interface AuthOAuthPreambleProps {
provider: Provider;
authorizationSearch?: StringSearchParams;
}
export function AuthOAuthPreamble({
provider,
authorizationSearch,
}: AuthOAuthPreambleProps) {
const handleSubmit = async (event: SubmitEvent) => {
event.preventDefault();
const socialProvider = PROVIDER_SOCIAL_MAP[provider];
if (!socialProvider) return;
await authClient.signIn.social({
callbackURL: resolveClientPostAuthRedirect(authorizationSearch),
provider: socialProvider,
});
};
return (
Don't import my calendars yet, just log me in.
);
}
interface LinkOAuthPreambleProps {
provider: Provider;
}
export function LinkOAuthPreamble({ provider }: LinkOAuthPreambleProps) {
const handleSubmit = (event: SubmitEvent) => {
event.preventDefault();
const apiProvider = PROVIDER_API_MAP[provider];
window.location.href = `/api/sources/authorize?provider=${apiProvider}`;
};
return (
);
}
export function ProviderIconPair({ children }: { children: ReactNode }) {
return (
);
}
================================================
FILE: applications/web/src/features/blog/components/blog-post-cta.tsx
================================================
import { ButtonText, LinkButton } from "@/components/ui/primitives/button";
import { Heading3 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
export function BlogPostCta() {
return (
);
}
================================================
FILE: applications/web/src/features/dashboard/components/event-graph.tsx
================================================
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
import { atom } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import * as m from "motion/react-m";
import { LazyMotion } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import { tv } from "tailwind-variants/lite";
import { eventGraphHoverIndexAtom, eventGraphDraggingAtom } from "@/state/event-graph-hover";
import { fetcher } from "@/lib/fetcher";
import { useAnimatedSWR } from "@/hooks/use-animated-swr";
import { pluralize } from "@/lib/pluralize";
import { Text } from "@/components/ui/primitives/text";
import { useStartOfToday } from "@/hooks/use-start-of-today";
import type { ApiEventSummary } from "@/types/api";
const DAYS_BEFORE = 7;
const DAYS_AFTER = 7;
const TOTAL_DAYS = DAYS_BEFORE + 1 + DAYS_AFTER;
const GRAPH_HEIGHT = 96;
const MIN_BAR_HEIGHT = 20;
const graphBar = tv({
base: "flex-1 rounded-[0.625rem]",
variants: {
period: {
past: "bg-background-hover border border-border-elevated",
today: "bg-emerald-400 border-transparent",
future:
"bg-emerald-400 border-emerald-500 bg-[repeating-linear-gradient(-45deg,transparent_0_4px,var(--color-illustration-stripe)_4px_8px)]",
},
},
});
type Period = "past" | "today" | "future";
const resolvePeriod = (dayOffset: number): Period => {
if (dayOffset < 0) return "past";
if (dayOffset === 0) return "today";
return "future";
};
const MS_PER_DAY = 86_400_000;
const buildGraphUrl = (todayStart: Date): string => {
const from = new Date(todayStart.getTime() - DAYS_BEFORE * MS_PER_DAY);
const to = new Date(todayStart.getTime() + DAYS_AFTER * MS_PER_DAY + MS_PER_DAY - 1);
return `/api/events?from=${from.toISOString()}&to=${to.toISOString()}`;
};
const countEventsByDay = (events: ApiEventSummary[], todayTimestamp: number): number[] => {
const counts = new Array(TOTAL_DAYS).fill(0);
for (const event of events) {
const eventDate = new Date(event.startTime);
const dayOffset = Math.floor((eventDate.getTime() - todayTimestamp) / MS_PER_DAY);
const slotIndex = dayOffset + DAYS_BEFORE;
if (slotIndex >= 0 && slotIndex < TOTAL_DAYS) counts[slotIndex]++;
}
return counts;
};
interface DayData {
count: number;
dayOffset: number;
height: number;
fullLabel: string;
period: Period;
}
const formatDayLabel = (todayStart: Date, dayOffset: number): string => {
const date = new Date(todayStart);
date.setDate(date.getDate() + dayOffset);
return date.toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
};
const GROWTH_SPACE = GRAPH_HEIGHT - MIN_BAR_HEIGHT;
function resolveBarHeight(count: number, maxCount: number): number {
return MIN_BAR_HEIGHT + (count / maxCount) * GROWTH_SPACE;
}
const normalizeDayData = (counts: number[], todayStart: Date): DayData[] => {
const maxCount = Math.max(...counts, 1);
return counts.map((count, slotIndex) => {
const dayOffset = slotIndex - DAYS_BEFORE;
return {
count,
dayOffset,
height: resolveBarHeight(count, maxCount),
fullLabel: formatDayLabel(todayStart, dayOffset),
period: resolvePeriod(dayOffset),
};
});
};
const buildDays = (events: ApiEventSummary[], todayStart: Date): DayData[] => {
const counts = countEventsByDay(events, todayStart.getTime());
return normalizeDayData(counts, todayStart);
};
function resolveWeekTotal(days: DayData[]): number {
return days.reduce((sum, day) => sum + day.count, 0);
}
function resolveEventCount(hoverIndex: number | null, days: DayData[]): number {
if (hoverIndex !== null) return days[hoverIndex].count;
return resolveWeekTotal(days);
}
function resolveLabel(hoverIndex: number | null, days: DayData[]): string {
if (hoverIndex !== null) return days[hoverIndex].fullLabel;
return "This Week";
}
function resolveDataAttr(condition: boolean): "" | undefined {
if (condition) return "";
return undefined;
}
interface EventGraphSummaryProps {
days: DayData[];
}
function EventGraphSummary({ days }: EventGraphSummaryProps) {
return (
);
}
function EventGraphEventCount({ days }: EventGraphSummaryProps) {
const hoverIndex = useAtomValue(eventGraphHoverIndexAtom);
const count = resolveEventCount(hoverIndex, days);
return (
{pluralize(count, "event")}
);
}
function EventGraphLabel({ days }: EventGraphSummaryProps) {
const hoverIndex = useAtomValue(eventGraphHoverIndexAtom);
const label = resolveLabel(hoverIndex, days);
return (
{label}
);
}
const ANIMATED_TRANSITION = { duration: 0.3, ease: [0.4, 0, 0.2, 1] as const };
const INSTANT_TRANSITION = { duration: 0 };
function resolveBarTransition(shouldAnimate: boolean, dayIndex: number) {
if (!shouldAnimate) return INSTANT_TRANSITION;
return { ...ANIMATED_TRANSITION, delay: dayIndex * 0.015 };
}
function useIsActiveDragTarget(index: number): boolean {
const isActiveAtom = useMemo(
() => atom((get) => get(eventGraphDraggingAtom) && get(eventGraphHoverIndexAtom) === index),
[index],
);
return useAtomValue(isActiveAtom);
}
interface EventGraphBarProps {
day: DayData;
dayIndex: number;
shouldAnimate: boolean;
}
const EventGraphBar = memo(function EventGraphBar({ day, dayIndex, shouldAnimate }: EventGraphBarProps) {
const isActive = useIsActiveDragTarget(dayIndex);
const setHoverIndex = useSetAtom(eventGraphHoverIndexAtom);
return (
setHoverIndex(dayIndex)}
>
{day.dayOffset}
);
});
interface EventGraphBarsProps {
days: DayData[];
shouldAnimate: boolean;
}
function EventGraphBars({ days, shouldAnimate }: EventGraphBarsProps) {
const isDragging = useAtomValue(eventGraphDraggingAtom);
const setHoverIndex = useSetAtom(eventGraphHoverIndexAtom);
const setDragging = useSetAtom(eventGraphDraggingAtom);
const containerRef = useRef(null);
const resolveIndexFromTouch = useCallback((touch: React.Touch) => {
const container = containerRef.current;
if (!container) return null;
const rect = container.getBoundingClientRect();
const x = touch.clientX - rect.left;
const index = Math.floor((x / rect.width) * TOTAL_DAYS);
if (index < 0 || index >= TOTAL_DAYS) return null;
return index;
}, []);
const handleTouchStart = useCallback(({ touches }: React.TouchEvent) => {
setDragging(true);
const touch = touches[0];
if (!touch) return;
setHoverIndex(resolveIndexFromTouch(touch));
}, [resolveIndexFromTouch, setHoverIndex, setDragging]);
const handleTouchMove = useCallback((event: React.TouchEvent | TouchEvent) => {
event.preventDefault();
const touch = event.touches[0];
if (!touch) return;
setHoverIndex(resolveIndexFromTouch(touch));
}, [resolveIndexFromTouch, setHoverIndex]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const listener = (event: TouchEvent) => handleTouchMove(event);
container.addEventListener("touchmove", listener, { passive: false });
return () => container.removeEventListener("touchmove", listener);
}, [handleTouchMove]);
const handleTouchEnd = () => {
setDragging(false);
setHoverIndex(null);
};
return (
setHoverIndex(null)}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{days.map((day, dayIndex) => (
))}
);
}
export function EventGraph() {
const todayStart = useStartOfToday();
const graphUrl = buildGraphUrl(todayStart);
const { data: events, shouldAnimate } = useAnimatedSWR(graphUrl, { fetcher });
const days = buildDays(events ?? [], todayStart);
return (
);
}
================================================
FILE: applications/web/src/features/dashboard/components/metadata-row.tsx
================================================
import type { ComponentPropsWithoutRef, ReactNode } from "react";
import type { Link } from "@tanstack/react-router";
import {
NavigationMenuItem,
NavigationMenuLinkItem,
NavigationMenuItemIcon,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import { Text } from "@/components/ui/primitives/text";
import { cn } from "@/utils/cn";
interface MetadataRowProps {
label: string;
value?: string;
icon?: ReactNode;
truncate?: boolean;
to?: ComponentPropsWithoutRef["to"];
}
export function MetadataRow({ label, value, icon, truncate = false, to }: MetadataRowProps) {
const content = (
<>
{label}
{value && (
{value}
)}
{icon && {icon}
}
>
);
if (to) return {content} ;
return {content} ;
}
================================================
FILE: applications/web/src/features/dashboard/components/sync-status-helpers.ts
================================================
import type { CompositeSyncState } from "@/state/sync";
const clampPercent = (value: number): number => {
if (!Number.isFinite(value)) {
return 0;
}
return Math.min(100, Math.max(0, value));
};
const resolveSyncPercent = (composite: CompositeSyncState): number | null => {
if (!composite.hasReceivedAggregate) {
return null;
}
if (composite.syncEventsTotal > 0) {
return clampPercent((composite.syncEventsProcessed / composite.syncEventsTotal) * 100);
}
if (composite.state === "syncing") {
return clampPercent(composite.progressPercent);
}
if (composite.connected && composite.syncEventsRemaining === 0) {
return 100;
}
return null;
};
export { clampPercent, resolveSyncPercent };
================================================
FILE: applications/web/src/features/dashboard/components/sync-status.tsx
================================================
import { useAtomValue } from "jotai";
import { syncStateAtom, syncStatusLabelAtom, syncStatusShimmerAtom } from "@/state/sync";
import { Text } from "@/components/ui/primitives/text";
import { ShimmerText } from "@/components/ui/primitives/shimmer-text";
import { Tooltip } from "@/components/ui/primitives/tooltip";
import { clampPercent, resolveSyncPercent } from "./sync-status-helpers";
function SyncProgressCircle({ percent }: { percent: number }) {
const radius = 5;
const circumference = 2 * Math.PI * radius;
const clampedPercent = clampPercent(percent);
const strokeDashoffset = circumference * (1 - clampedPercent / 100);
return (
);
}
function SyncProgressIndicator() {
const composite = useAtomValue(syncStateAtom);
if (!composite.connected) {
return ;
}
const percent = resolveSyncPercent(composite);
if (percent === null) {
return null;
}
return ;
}
function SyncStatusLabel() {
const label = useAtomValue(syncStatusLabelAtom);
const isSyncing = useAtomValue(syncStatusShimmerAtom);
if (isSyncing) {
return {label} ;
}
return {label} ;
}
function SyncTooltipContent() {
const composite = useAtomValue(syncStateAtom);
if (!composite.connected) return null;
const percent = resolveSyncPercent(composite);
if (percent === null) return null;
return <>{percent.toFixed(2)}%>;
}
export function SyncStatus() {
return (
}>
);
}
================================================
FILE: applications/web/src/features/dashboard/components/upgrade-card.tsx
================================================
import type { PropsWithChildren } from "react";
import { tv } from "tailwind-variants/lite";
const upgradeCard = tv({
base: "relative rounded-2xl p-0.5 before:absolute before:top-0.5 before:inset-x-0 before:h-px before:bg-linear-to-r before:mx-4 before:z-10 before:from-transparent before:to-transparent bg-neutral-900 before:via-neutral-400 dark:bg-neutral-800 dark:before:via-neutral-400",
});
const upgradeCardBody = tv({
base: "rounded-[0.875rem] bg-linear-to-t from-neutral-950 to-neutral-900 dark:from-neutral-900 dark:to-neutral-800 p-4 flex flex-col gap-4",
});
const upgradeCardSection = tv({
base: "flex flex-col",
variants: {
gap: {
sm: "gap-0.5",
md: "gap-1.5",
},
},
defaultVariants: {
gap: "md",
},
});
const upgradeCardToggle = tv({
base: "flex items-center gap-3 py-1.5 hover:cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hover:brightness-110",
});
const upgradeCardToggleTrack = tv({
base: "w-8 h-5 rounded-full shrink-0 flex items-center p-0.5",
variants: {
checked: {
true: "bg-white",
false: "bg-neutral-600",
},
},
});
const upgradeCardToggleThumb = tv({
base: "size-4 rounded-full bg-neutral-900",
variants: {
checked: {
true: "ml-auto",
false: "",
},
},
});
const upgradeCardFeatureRow = tv({
base: "flex items-center gap-2.5",
});
const upgradeCardFeatureIcon = tv({
base: "shrink-0 text-neutral-500",
});
export function UpgradeCard({ children, className }: PropsWithChildren<{ className?: string }>) {
return (
);
}
export function UpgradeCardSection({ children, gap }: PropsWithChildren<{ gap?: "sm" | "md" }>) {
return {children}
;
}
type UpgradeCardToggleProps = PropsWithChildren<{
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}>;
export function UpgradeCardToggle({ checked, onCheckedChange, children }: UpgradeCardToggleProps) {
return (
onCheckedChange(!checked)}
className={upgradeCardToggle()}
>
{children}
);
}
export function UpgradeCardFeature({ children }: PropsWithChildren) {
return {children}
;
}
export function UpgradeCardFeatureIcon({ children }: PropsWithChildren) {
return {children}
;
}
export function UpgradeCardActions({ children }: PropsWithChildren) {
return {children}
;
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-cta.tsx
================================================
import type { PropsWithChildren } from "react";
export function MarketingCtaSection({ children }: PropsWithChildren) {
return (
);
}
export function MarketingCtaCard({ children }: PropsWithChildren) {
return (
);
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-faq.tsx
================================================
import type { PropsWithChildren } from "react";
import { Text } from "@/components/ui/primitives/text";
export function MarketingFaqSection({ children }: PropsWithChildren) {
return ;
}
export function MarketingFaqList({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingFaqItem({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingFaqQuestion({ children }: PropsWithChildren) {
return (
{children}
);
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-feature-bento.tsx
================================================
import type { PropsWithChildren } from "react";
import { cn } from "@/utils/cn";
type MarketingFeatureBentoCardProps = PropsWithChildren<{ className?: string }>;
export function MarketingFeatureBentoSection({ children, id }: PropsWithChildren<{ id?: string }>) {
return ;
}
export function MarketingFeatureBentoGrid({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingFeatureBentoCard({
children,
className,
}: MarketingFeatureBentoCardProps) {
return (
{children}
);
}
const ILLUSTRATION_STYLE = {
backgroundImage:
"repeating-linear-gradient(-45deg, transparent 0 14px, var(--color-illustration-stripe) 14px 15px)",
} as const;
type MarketingFeatureBentoIllustrationProps = PropsWithChildren<{
plain?: boolean;
}>;
export function MarketingFeatureBentoIllustration({ children, plain }: MarketingFeatureBentoIllustrationProps) {
return (
{children}
);
}
export function MarketingFeatureBentoBody({ children }: PropsWithChildren) {
return {children}
;
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-footer.tsx
================================================
import type { PropsWithChildren } from "react";
import { Link } from "@tanstack/react-router";
export function MarketingFooter({ children }: PropsWithChildren) {
return (
);
}
export function MarketingFooterTagline({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingFooterNav({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingFooterNavGroup({ children }: PropsWithChildren) {
return (
);
}
export function MarketingFooterNavGroupLabel({ children }: PropsWithChildren) {
return (
{children}
);
}
type MarketingFooterNavItemProps = PropsWithChildren<{
to?: string;
href?: string;
}>;
export function MarketingFooterNavItem({ children, to, href }: MarketingFooterNavItemProps) {
const className = "text-foreground-muted hover:text-foreground-hover";
if (to) {
return (
{children}
);
}
if (href) {
return (
{children}
);
}
return (
{children}
);
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-header.tsx
================================================
import type { PropsWithChildren } from "react";
import { Link } from "@tanstack/react-router";
import { LayoutRow } from "@/components/ui/shells/layout";
import { StaggeredBackdropBlur } from "@/components/ui/primitives/staggered-backdrop-blur";
export function MarketingHeader({ children }: PropsWithChildren) {
return (
);
}
export function MarketingHeaderBranding({ children, label }: PropsWithChildren<{ label?: string }>) {
return {children};
}
export function MarketingHeaderActions({ children }: PropsWithChildren) {
return (
{children}
);
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-how-it-works.tsx
================================================
import type { PropsWithChildren } from "react";
import { cn } from "@/utils/cn";
import { Text } from "@/components/ui/primitives/text";
export function MarketingHowItWorksSection({ children }: PropsWithChildren) {
return ;
}
const ILLUSTRATION_STYLE = {
backgroundImage:
"repeating-linear-gradient(-45deg, transparent 0 14px, var(--color-illustration-stripe) 14px 15px)",
} as const;
export function MarketingHowItWorksCard({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingHowItWorksRow({ children, className, reverse }: PropsWithChildren<{ className?: string; reverse?: boolean }>) {
return (
{children}
);
}
export function MarketingHowItWorksStepBody({
step,
children,
}: PropsWithChildren<{ step: number }>) {
return (
{step}
{children}
);
}
export function MarketingHowItWorksStepIllustration({ children, align }: PropsWithChildren<{ align?: "left" | "right" }>) {
return (
{children}
{align && (
<>
>
)}
);
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-illustration-calendar.tsx
================================================
import { useAtomValue } from "jotai";
import { LazyMotion } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import * as m from "motion/react-m";
import { memo, type PropsWithChildren } from "react";
import { calendarEmphasizedAtom } from "@/state/calendar-emphasized";
export interface Skew {
rotate: number;
x: number;
y: number;
}
export type SkewTuple = [Skew, Skew, Skew];
const CALENDAR_COLUMNS = 7;
const CALENDAR_ROWS = 6;
const CALENDAR_DAYS_IN_MONTH = 31;
const CALENDAR_CELLS = CALENDAR_COLUMNS * CALENDAR_ROWS;
const CALENDAR_ANIMATION_EASE = [0.16, 0.85, 0.2, 1] as const;
const CALENDAR_DAY_NUMBERS = Array.from(
{ length: CALENDAR_CELLS },
(_, index) => (index % CALENDAR_DAYS_IN_MONTH) + 1,
);
interface MarketingIllustrationCalendarCardProps {
skew: SkewTuple;
}
const toMotionTarget = ({ rotate, x, y }: Skew) => ({ rotate, x, y });
const getAnimatedSkew = (skew: SkewTuple, emphasized: boolean) => {
if (emphasized) return toMotionTarget(skew[2]);
return toMotionTarget(skew[1]);
};
const transformTemplate = ({
x,
y,
rotate,
}: {
x?: unknown;
y?: unknown;
rotate?: unknown;
}) =>
`translateX(${String(x ?? 0)}) translateY(${String(y ?? 0)}) rotate(${String(rotate ?? 0)})`;
interface CalendarDayProps {
day: number;
}
const CalendarDay = memo(function CalendarDay({ day }: CalendarDayProps) {
return (
{day}
);
});
const CalendarGrid = memo(function CalendarGrid() {
return (
{CALENDAR_DAY_NUMBERS.map((day, index) => (
))}
);
});
export function MarketingIllustrationCalendarCard({
skew,
}: MarketingIllustrationCalendarCardProps) {
const emphasized = useAtomValue(calendarEmphasizedAtom);
return (
);
}
export function MarketingIllustrationCalendar({ children }: PropsWithChildren) {
return (
);
}
================================================
FILE: applications/web/src/features/marketing/components/marketing-pricing-section.tsx
================================================
import type { PropsWithChildren } from "react";
import { tv, type VariantProps } from "tailwind-variants/lite";
import { Heading2, Heading3 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { ButtonText, LinkButton } from "@/components/ui/primitives/button";
import CheckIcon from "lucide-react/dist/esm/icons/check";
import InfinityIcon from "lucide-react/dist/esm/icons/infinity";
import MinusIcon from "lucide-react/dist/esm/icons/minus";
type ClassNameProps = PropsWithChildren<{ className?: string }>;
type MarketingPricingCardProps = ClassNameProps & VariantProps;
export type MarketingPricingFeatureValueKind =
| "check"
| "minus"
| "infinity"
| (string & {});
type MarketingPricingPlanCardProps = {
tone?: "default" | "inverse";
name: string;
price: string;
period: string;
description: string;
ctaLabel: string;
};
const marketingPricingCard = tv({
base: "border rounded-2xl p-3 pt-5 flex flex-col shadow-xs",
variants: {
tone: {
default: "border-interactive-border bg-background",
inverse: "border-transparent bg-background-inverse",
},
},
defaultVariants: {
tone: "default",
},
});
export function MarketingPricingSection({ children, id }: PropsWithChildren<{ id?: string }>) {
return ;
}
export function MarketingPricingIntro({ children }: PropsWithChildren) {
return {children}
;
}
export function MarketingPricingComparisonGrid({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingPricingComparisonSpacer() {
return
;
}
export function MarketingPricingCard({
children,
className,
tone,
}: MarketingPricingCardProps) {
return (
{children}
);
}
export function MarketingPricingCardBody({ children }: PropsWithChildren) {
return {children}
;
}
export function MarketingPricingCardCopy({ children }: PropsWithChildren) {
return {children}
;
}
export function MarketingPricingCardAction({ children }: PropsWithChildren) {
return {children}
;
}
const pricingPlanHeading = tv({
variants: {
tone: {
default: "",
inverse: "text-foreground-inverse",
},
},
defaultVariants: {
tone: "default",
},
});
const pricingPlanButton = tv({
base: "w-full justify-center",
variants: {
tone: {
default: "",
inverse: "border-transparent",
},
},
defaultVariants: {
tone: "default",
},
});
const pricingFeatureValueDisplay = tv({
variants: {
tone: {
default: "text-foreground",
muted: "text-foreground-muted",
},
},
defaultVariants: {
tone: "default",
},
});
function resolveCopyTone(tone: "default" | "inverse"): "inverseMuted" | "muted" {
if (tone === "inverse") return "inverseMuted";
return "muted";
}
export function MarketingPricingPlanCard({
tone = "default",
name,
price,
period,
description,
ctaLabel,
}: MarketingPricingPlanCardProps) {
const copyTone = resolveCopyTone(tone);
return (
{name}
{price}
{period}
{description}
{ctaLabel}
);
}
export function MarketingPricingFeatureMatrix({ children }: PropsWithChildren) {
return {children}
;
}
export function MarketingPricingFeatureRow({ children }: PropsWithChildren) {
return (
{children}
);
}
export function MarketingPricingFeatureLabel({ children }: PropsWithChildren) {
return {children}
;
}
export function MarketingPricingFeatureValue({ children }: PropsWithChildren) {
return {children}
;
}
export function MarketingPricingFeatureDisplay({
value,
tone = "default",
}: {
value: MarketingPricingFeatureValueKind;
tone?: "muted" | "default";
}) {
const className = pricingFeatureValueDisplay({ tone });
if (value === "check") {
return ;
}
if (value === "minus") {
return ;
}
if (value === "infinity") {
return ;
}
return (
{value}
);
}
================================================
FILE: applications/web/src/features/marketing/contributors.json
================================================
[
{
"username": "ridafkih",
"name": "Rida F'kih",
"avatarUrl": "/contributors/ridafkih.webp"
},
{
"username": "tilwegener",
"name": "Til Wegener",
"avatarUrl": "/contributors/tilwegener.webp"
},
{
"username": "ivancarlosti",
"name": "Ivan Carlos",
"avatarUrl": "/contributors/ivancarlosti.webp"
},
{
"username": "cnaples79",
"name": "Chase Naples",
"avatarUrl": "/contributors/cnaples79.webp"
},
{
"username": "ankitson",
"name": "Ankit Soni",
"avatarUrl": "/contributors/ankitson.webp"
},
{
"username": "claude",
"name": "Claude",
"avatarUrl": "/contributors/claude.webp"
},
{
"username": "mbaroody",
"name": "Michael Baroody",
"avatarUrl": "/contributors/mbaroody.webp"
}
]
================================================
FILE: applications/web/src/generated/tanstack/route-tree.generated.ts
================================================
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './../../routes/__root'
import { Route as oauthRouteRouteImport } from './../../routes/(oauth)/route'
import { Route as marketingRouteRouteImport } from './../../routes/(marketing)/route'
import { Route as dashboardRouteRouteImport } from './../../routes/(dashboard)/route'
import { Route as authRouteRouteImport } from './../../routes/(auth)/route'
import { Route as marketingIndexRouteImport } from './../../routes/(marketing)/index'
import { Route as marketingTermsRouteImport } from './../../routes/(marketing)/terms'
import { Route as marketingPrivacyRouteImport } from './../../routes/(marketing)/privacy'
import { Route as authVerifyEmailRouteImport } from './../../routes/(auth)/verify-email'
import { Route as authVerifyAuthenticationRouteImport } from './../../routes/(auth)/verify-authentication'
import { Route as authResetPasswordRouteImport } from './../../routes/(auth)/reset-password'
import { Route as authRegisterRouteImport } from './../../routes/(auth)/register'
import { Route as authLoginRouteImport } from './../../routes/(auth)/login'
import { Route as authForgotPasswordRouteImport } from './../../routes/(auth)/forgot-password'
import { Route as oauthDashboardRouteRouteImport } from './../../routes/(oauth)/dashboard/route'
import { Route as oauthAuthRouteRouteImport } from './../../routes/(oauth)/auth/route'
import { Route as marketingBlogRouteRouteImport } from './../../routes/(marketing)/blog/route'
import { Route as marketingBlogIndexRouteImport } from './../../routes/(marketing)/blog/index'
import { Route as dashboardDashboardIndexRouteImport } from './../../routes/(dashboard)/dashboard/index'
import { Route as oauthOauthConsentRouteImport } from './../../routes/(oauth)/oauth/consent'
import { Route as oauthAuthOutlookRouteImport } from './../../routes/(oauth)/auth/outlook'
import { Route as oauthAuthGoogleRouteImport } from './../../routes/(oauth)/auth/google'
import { Route as marketingBlogSlugRouteImport } from './../../routes/(marketing)/blog/$slug'
import { Route as dashboardDashboardReportRouteImport } from './../../routes/(dashboard)/dashboard/report'
import { Route as dashboardDashboardIcalRouteImport } from './../../routes/(dashboard)/dashboard/ical'
import { Route as dashboardDashboardFeedbackRouteImport } from './../../routes/(dashboard)/dashboard/feedback'
import { Route as oauthDashboardConnectRouteRouteImport } from './../../routes/(oauth)/dashboard/connect/route'
import { Route as dashboardDashboardSettingsRouteRouteImport } from './../../routes/(dashboard)/dashboard/settings/route'
import { Route as dashboardDashboardConnectRouteRouteImport } from './../../routes/(dashboard)/dashboard/connect/route'
import { Route as dashboardDashboardAccountsRouteRouteImport } from './../../routes/(dashboard)/dashboard/accounts/route'
import { Route as dashboardDashboardUpgradeIndexRouteImport } from './../../routes/(dashboard)/dashboard/upgrade/index'
import { Route as dashboardDashboardSettingsIndexRouteImport } from './../../routes/(dashboard)/dashboard/settings/index'
import { Route as dashboardDashboardIntegrationsIndexRouteImport } from './../../routes/(dashboard)/dashboard/integrations/index'
import { Route as dashboardDashboardEventsIndexRouteImport } from './../../routes/(dashboard)/dashboard/events/index'
import { Route as dashboardDashboardConnectIndexRouteImport } from './../../routes/(dashboard)/dashboard/connect/index'
import { Route as oauthDashboardConnectOutlookRouteImport } from './../../routes/(oauth)/dashboard/connect/outlook'
import { Route as oauthDashboardConnectMicrosoftRouteImport } from './../../routes/(oauth)/dashboard/connect/microsoft'
import { Route as oauthDashboardConnectIcsFileRouteImport } from './../../routes/(oauth)/dashboard/connect/ics-file'
import { Route as oauthDashboardConnectIcalLinkRouteImport } from './../../routes/(oauth)/dashboard/connect/ical-link'
import { Route as oauthDashboardConnectGoogleRouteImport } from './../../routes/(oauth)/dashboard/connect/google'
import { Route as oauthDashboardConnectFastmailRouteImport } from './../../routes/(oauth)/dashboard/connect/fastmail'
import { Route as oauthDashboardConnectCaldavRouteImport } from './../../routes/(oauth)/dashboard/connect/caldav'
import { Route as oauthDashboardConnectAppleRouteImport } from './../../routes/(oauth)/dashboard/connect/apple'
import { Route as dashboardDashboardSettingsPasskeysRouteImport } from './../../routes/(dashboard)/dashboard/settings/passkeys'
import { Route as dashboardDashboardSettingsChangePasswordRouteImport } from './../../routes/(dashboard)/dashboard/settings/change-password'
import { Route as dashboardDashboardSettingsApiTokensRouteImport } from './../../routes/(dashboard)/dashboard/settings/api-tokens'
import { Route as dashboardDashboardAccountsAccountIdIndexRouteImport } from './../../routes/(dashboard)/dashboard/accounts/$accountId.index'
import { Route as dashboardDashboardAccountsAccountIdSetupRouteImport } from './../../routes/(dashboard)/dashboard/accounts/$accountId.setup'
import { Route as dashboardDashboardAccountsAccountIdCalendarIdRouteImport } from './../../routes/(dashboard)/dashboard/accounts/$accountId.$calendarId'
const oauthRouteRoute = oauthRouteRouteImport.update({
id: '/(oauth)',
getParentRoute: () => rootRouteImport,
} as any)
const marketingRouteRoute = marketingRouteRouteImport.update({
id: '/(marketing)',
getParentRoute: () => rootRouteImport,
} as any)
const dashboardRouteRoute = dashboardRouteRouteImport.update({
id: '/(dashboard)',
getParentRoute: () => rootRouteImport,
} as any)
const authRouteRoute = authRouteRouteImport.update({
id: '/(auth)',
getParentRoute: () => rootRouteImport,
} as any)
const marketingIndexRoute = marketingIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => marketingRouteRoute,
} as any)
const marketingTermsRoute = marketingTermsRouteImport.update({
id: '/terms',
path: '/terms',
getParentRoute: () => marketingRouteRoute,
} as any)
const marketingPrivacyRoute = marketingPrivacyRouteImport.update({
id: '/privacy',
path: '/privacy',
getParentRoute: () => marketingRouteRoute,
} as any)
const authVerifyEmailRoute = authVerifyEmailRouteImport.update({
id: '/verify-email',
path: '/verify-email',
getParentRoute: () => authRouteRoute,
} as any)
const authVerifyAuthenticationRoute =
authVerifyAuthenticationRouteImport.update({
id: '/verify-authentication',
path: '/verify-authentication',
getParentRoute: () => authRouteRoute,
} as any)
const authResetPasswordRoute = authResetPasswordRouteImport.update({
id: '/reset-password',
path: '/reset-password',
getParentRoute: () => authRouteRoute,
} as any)
const authRegisterRoute = authRegisterRouteImport.update({
id: '/register',
path: '/register',
getParentRoute: () => authRouteRoute,
} as any)
const authLoginRoute = authLoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => authRouteRoute,
} as any)
const authForgotPasswordRoute = authForgotPasswordRouteImport.update({
id: '/forgot-password',
path: '/forgot-password',
getParentRoute: () => authRouteRoute,
} as any)
const oauthDashboardRouteRoute = oauthDashboardRouteRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => oauthRouteRoute,
} as any)
const oauthAuthRouteRoute = oauthAuthRouteRouteImport.update({
id: '/auth',
path: '/auth',
getParentRoute: () => oauthRouteRoute,
} as any)
const marketingBlogRouteRoute = marketingBlogRouteRouteImport.update({
id: '/blog',
path: '/blog',
getParentRoute: () => marketingRouteRoute,
} as any)
const marketingBlogIndexRoute = marketingBlogIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => marketingBlogRouteRoute,
} as any)
const dashboardDashboardIndexRoute = dashboardDashboardIndexRouteImport.update({
id: '/dashboard/',
path: '/dashboard/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const oauthOauthConsentRoute = oauthOauthConsentRouteImport.update({
id: '/oauth/consent',
path: '/oauth/consent',
getParentRoute: () => oauthRouteRoute,
} as any)
const oauthAuthOutlookRoute = oauthAuthOutlookRouteImport.update({
id: '/outlook',
path: '/outlook',
getParentRoute: () => oauthAuthRouteRoute,
} as any)
const oauthAuthGoogleRoute = oauthAuthGoogleRouteImport.update({
id: '/google',
path: '/google',
getParentRoute: () => oauthAuthRouteRoute,
} as any)
const marketingBlogSlugRoute = marketingBlogSlugRouteImport.update({
id: '/$slug',
path: '/$slug',
getParentRoute: () => marketingBlogRouteRoute,
} as any)
const dashboardDashboardReportRoute =
dashboardDashboardReportRouteImport.update({
id: '/dashboard/report',
path: '/dashboard/report',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardIcalRoute = dashboardDashboardIcalRouteImport.update({
id: '/dashboard/ical',
path: '/dashboard/ical',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardFeedbackRoute =
dashboardDashboardFeedbackRouteImport.update({
id: '/dashboard/feedback',
path: '/dashboard/feedback',
getParentRoute: () => dashboardRouteRoute,
} as any)
const oauthDashboardConnectRouteRoute =
oauthDashboardConnectRouteRouteImport.update({
id: '/connect',
path: '/connect',
getParentRoute: () => oauthDashboardRouteRoute,
} as any)
const dashboardDashboardSettingsRouteRoute =
dashboardDashboardSettingsRouteRouteImport.update({
id: '/dashboard/settings',
path: '/dashboard/settings',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardConnectRouteRoute =
dashboardDashboardConnectRouteRouteImport.update({
id: '/dashboard/connect',
path: '/dashboard/connect',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardAccountsRouteRoute =
dashboardDashboardAccountsRouteRouteImport.update({
id: '/dashboard/accounts',
path: '/dashboard/accounts',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardUpgradeIndexRoute =
dashboardDashboardUpgradeIndexRouteImport.update({
id: '/dashboard/upgrade/',
path: '/dashboard/upgrade/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardSettingsIndexRoute =
dashboardDashboardSettingsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardIntegrationsIndexRoute =
dashboardDashboardIntegrationsIndexRouteImport.update({
id: '/dashboard/integrations/',
path: '/dashboard/integrations/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardEventsIndexRoute =
dashboardDashboardEventsIndexRouteImport.update({
id: '/dashboard/events/',
path: '/dashboard/events/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardConnectIndexRoute =
dashboardDashboardConnectIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => dashboardDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectOutlookRoute =
oauthDashboardConnectOutlookRouteImport.update({
id: '/outlook',
path: '/outlook',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectMicrosoftRoute =
oauthDashboardConnectMicrosoftRouteImport.update({
id: '/microsoft',
path: '/microsoft',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectIcsFileRoute =
oauthDashboardConnectIcsFileRouteImport.update({
id: '/ics-file',
path: '/ics-file',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectIcalLinkRoute =
oauthDashboardConnectIcalLinkRouteImport.update({
id: '/ical-link',
path: '/ical-link',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectGoogleRoute =
oauthDashboardConnectGoogleRouteImport.update({
id: '/google',
path: '/google',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectFastmailRoute =
oauthDashboardConnectFastmailRouteImport.update({
id: '/fastmail',
path: '/fastmail',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectCaldavRoute =
oauthDashboardConnectCaldavRouteImport.update({
id: '/caldav',
path: '/caldav',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectAppleRoute =
oauthDashboardConnectAppleRouteImport.update({
id: '/apple',
path: '/apple',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const dashboardDashboardSettingsPasskeysRoute =
dashboardDashboardSettingsPasskeysRouteImport.update({
id: '/passkeys',
path: '/passkeys',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardSettingsChangePasswordRoute =
dashboardDashboardSettingsChangePasswordRouteImport.update({
id: '/change-password',
path: '/change-password',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardSettingsApiTokensRoute =
dashboardDashboardSettingsApiTokensRouteImport.update({
id: '/api-tokens',
path: '/api-tokens',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardAccountsAccountIdIndexRoute =
dashboardDashboardAccountsAccountIdIndexRouteImport.update({
id: '/$accountId/',
path: '/$accountId/',
getParentRoute: () => dashboardDashboardAccountsRouteRoute,
} as any)
const dashboardDashboardAccountsAccountIdSetupRoute =
dashboardDashboardAccountsAccountIdSetupRouteImport.update({
id: '/$accountId/setup',
path: '/$accountId/setup',
getParentRoute: () => dashboardDashboardAccountsRouteRoute,
} as any)
const dashboardDashboardAccountsAccountIdCalendarIdRoute =
dashboardDashboardAccountsAccountIdCalendarIdRouteImport.update({
id: '/$accountId/$calendarId',
path: '/$accountId/$calendarId',
getParentRoute: () => dashboardDashboardAccountsRouteRoute,
} as any)
export interface FileRoutesByFullPath {
'/blog': typeof marketingBlogRouteRouteWithChildren
'/auth': typeof oauthAuthRouteRouteWithChildren
'/dashboard': typeof oauthDashboardRouteRouteWithChildren
'/forgot-password': typeof authForgotPasswordRoute
'/login': typeof authLoginRoute
'/register': typeof authRegisterRoute
'/reset-password': typeof authResetPasswordRoute
'/verify-authentication': typeof authVerifyAuthenticationRoute
'/verify-email': typeof authVerifyEmailRoute
'/privacy': typeof marketingPrivacyRoute
'/terms': typeof marketingTermsRoute
'/': typeof marketingIndexRoute
'/dashboard/accounts': typeof dashboardDashboardAccountsRouteRouteWithChildren
'/dashboard/connect': typeof oauthDashboardConnectRouteRouteWithChildren
'/dashboard/settings': typeof dashboardDashboardSettingsRouteRouteWithChildren
'/dashboard/feedback': typeof dashboardDashboardFeedbackRoute
'/dashboard/ical': typeof dashboardDashboardIcalRoute
'/dashboard/report': typeof dashboardDashboardReportRoute
'/blog/$slug': typeof marketingBlogSlugRoute
'/auth/google': typeof oauthAuthGoogleRoute
'/auth/outlook': typeof oauthAuthOutlookRoute
'/oauth/consent': typeof oauthOauthConsentRoute
'/dashboard/': typeof dashboardDashboardIndexRoute
'/blog/': typeof marketingBlogIndexRoute
'/dashboard/settings/api-tokens': typeof dashboardDashboardSettingsApiTokensRoute
'/dashboard/settings/change-password': typeof dashboardDashboardSettingsChangePasswordRoute
'/dashboard/settings/passkeys': typeof dashboardDashboardSettingsPasskeysRoute
'/dashboard/connect/apple': typeof oauthDashboardConnectAppleRoute
'/dashboard/connect/caldav': typeof oauthDashboardConnectCaldavRoute
'/dashboard/connect/fastmail': typeof oauthDashboardConnectFastmailRoute
'/dashboard/connect/google': typeof oauthDashboardConnectGoogleRoute
'/dashboard/connect/ical-link': typeof oauthDashboardConnectIcalLinkRoute
'/dashboard/connect/ics-file': typeof oauthDashboardConnectIcsFileRoute
'/dashboard/connect/microsoft': typeof oauthDashboardConnectMicrosoftRoute
'/dashboard/connect/outlook': typeof oauthDashboardConnectOutlookRoute
'/dashboard/connect/': typeof dashboardDashboardConnectIndexRoute
'/dashboard/events/': typeof dashboardDashboardEventsIndexRoute
'/dashboard/integrations/': typeof dashboardDashboardIntegrationsIndexRoute
'/dashboard/settings/': typeof dashboardDashboardSettingsIndexRoute
'/dashboard/upgrade/': typeof dashboardDashboardUpgradeIndexRoute
'/dashboard/accounts/$accountId/$calendarId': typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
'/dashboard/accounts/$accountId/setup': typeof dashboardDashboardAccountsAccountIdSetupRoute
'/dashboard/accounts/$accountId/': typeof dashboardDashboardAccountsAccountIdIndexRoute
}
export interface FileRoutesByTo {
'/auth': typeof oauthAuthRouteRouteWithChildren
'/dashboard': typeof dashboardDashboardIndexRoute
'/forgot-password': typeof authForgotPasswordRoute
'/login': typeof authLoginRoute
'/register': typeof authRegisterRoute
'/reset-password': typeof authResetPasswordRoute
'/verify-authentication': typeof authVerifyAuthenticationRoute
'/verify-email': typeof authVerifyEmailRoute
'/privacy': typeof marketingPrivacyRoute
'/terms': typeof marketingTermsRoute
'/': typeof marketingIndexRoute
'/dashboard/accounts': typeof dashboardDashboardAccountsRouteRouteWithChildren
'/dashboard/connect': typeof dashboardDashboardConnectIndexRoute
'/dashboard/feedback': typeof dashboardDashboardFeedbackRoute
'/dashboard/ical': typeof dashboardDashboardIcalRoute
'/dashboard/report': typeof dashboardDashboardReportRoute
'/blog/$slug': typeof marketingBlogSlugRoute
'/auth/google': typeof oauthAuthGoogleRoute
'/auth/outlook': typeof oauthAuthOutlookRoute
'/oauth/consent': typeof oauthOauthConsentRoute
'/blog': typeof marketingBlogIndexRoute
'/dashboard/settings/api-tokens': typeof dashboardDashboardSettingsApiTokensRoute
'/dashboard/settings/change-password': typeof dashboardDashboardSettingsChangePasswordRoute
'/dashboard/settings/passkeys': typeof dashboardDashboardSettingsPasskeysRoute
'/dashboard/connect/apple': typeof oauthDashboardConnectAppleRoute
'/dashboard/connect/caldav': typeof oauthDashboardConnectCaldavRoute
'/dashboard/connect/fastmail': typeof oauthDashboardConnectFastmailRoute
'/dashboard/connect/google': typeof oauthDashboardConnectGoogleRoute
'/dashboard/connect/ical-link': typeof oauthDashboardConnectIcalLinkRoute
'/dashboard/connect/ics-file': typeof oauthDashboardConnectIcsFileRoute
'/dashboard/connect/microsoft': typeof oauthDashboardConnectMicrosoftRoute
'/dashboard/connect/outlook': typeof oauthDashboardConnectOutlookRoute
'/dashboard/events': typeof dashboardDashboardEventsIndexRoute
'/dashboard/integrations': typeof dashboardDashboardIntegrationsIndexRoute
'/dashboard/settings': typeof dashboardDashboardSettingsIndexRoute
'/dashboard/upgrade': typeof dashboardDashboardUpgradeIndexRoute
'/dashboard/accounts/$accountId/$calendarId': typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
'/dashboard/accounts/$accountId/setup': typeof dashboardDashboardAccountsAccountIdSetupRoute
'/dashboard/accounts/$accountId': typeof dashboardDashboardAccountsAccountIdIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/(auth)': typeof authRouteRouteWithChildren
'/(dashboard)': typeof dashboardRouteRouteWithChildren
'/(marketing)': typeof marketingRouteRouteWithChildren
'/(oauth)': typeof oauthRouteRouteWithChildren
'/(marketing)/blog': typeof marketingBlogRouteRouteWithChildren
'/(oauth)/auth': typeof oauthAuthRouteRouteWithChildren
'/(oauth)/dashboard': typeof oauthDashboardRouteRouteWithChildren
'/(auth)/forgot-password': typeof authForgotPasswordRoute
'/(auth)/login': typeof authLoginRoute
'/(auth)/register': typeof authRegisterRoute
'/(auth)/reset-password': typeof authResetPasswordRoute
'/(auth)/verify-authentication': typeof authVerifyAuthenticationRoute
'/(auth)/verify-email': typeof authVerifyEmailRoute
'/(marketing)/privacy': typeof marketingPrivacyRoute
'/(marketing)/terms': typeof marketingTermsRoute
'/(marketing)/': typeof marketingIndexRoute
'/(dashboard)/dashboard/accounts': typeof dashboardDashboardAccountsRouteRouteWithChildren
'/(dashboard)/dashboard/connect': typeof dashboardDashboardConnectRouteRouteWithChildren
'/(dashboard)/dashboard/settings': typeof dashboardDashboardSettingsRouteRouteWithChildren
'/(oauth)/dashboard/connect': typeof oauthDashboardConnectRouteRouteWithChildren
'/(dashboard)/dashboard/feedback': typeof dashboardDashboardFeedbackRoute
'/(dashboard)/dashboard/ical': typeof dashboardDashboardIcalRoute
'/(dashboard)/dashboard/report': typeof dashboardDashboardReportRoute
'/(marketing)/blog/$slug': typeof marketingBlogSlugRoute
'/(oauth)/auth/google': typeof oauthAuthGoogleRoute
'/(oauth)/auth/outlook': typeof oauthAuthOutlookRoute
'/(oauth)/oauth/consent': typeof oauthOauthConsentRoute
'/(dashboard)/dashboard/': typeof dashboardDashboardIndexRoute
'/(marketing)/blog/': typeof marketingBlogIndexRoute
'/(dashboard)/dashboard/settings/api-tokens': typeof dashboardDashboardSettingsApiTokensRoute
'/(dashboard)/dashboard/settings/change-password': typeof dashboardDashboardSettingsChangePasswordRoute
'/(dashboard)/dashboard/settings/passkeys': typeof dashboardDashboardSettingsPasskeysRoute
'/(oauth)/dashboard/connect/apple': typeof oauthDashboardConnectAppleRoute
'/(oauth)/dashboard/connect/caldav': typeof oauthDashboardConnectCaldavRoute
'/(oauth)/dashboard/connect/fastmail': typeof oauthDashboardConnectFastmailRoute
'/(oauth)/dashboard/connect/google': typeof oauthDashboardConnectGoogleRoute
'/(oauth)/dashboard/connect/ical-link': typeof oauthDashboardConnectIcalLinkRoute
'/(oauth)/dashboard/connect/ics-file': typeof oauthDashboardConnectIcsFileRoute
'/(oauth)/dashboard/connect/microsoft': typeof oauthDashboardConnectMicrosoftRoute
'/(oauth)/dashboard/connect/outlook': typeof oauthDashboardConnectOutlookRoute
'/(dashboard)/dashboard/connect/': typeof dashboardDashboardConnectIndexRoute
'/(dashboard)/dashboard/events/': typeof dashboardDashboardEventsIndexRoute
'/(dashboard)/dashboard/integrations/': typeof dashboardDashboardIntegrationsIndexRoute
'/(dashboard)/dashboard/settings/': typeof dashboardDashboardSettingsIndexRoute
'/(dashboard)/dashboard/upgrade/': typeof dashboardDashboardUpgradeIndexRoute
'/(dashboard)/dashboard/accounts/$accountId/$calendarId': typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
'/(dashboard)/dashboard/accounts/$accountId/setup': typeof dashboardDashboardAccountsAccountIdSetupRoute
'/(dashboard)/dashboard/accounts/$accountId/': typeof dashboardDashboardAccountsAccountIdIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/blog'
| '/auth'
| '/dashboard'
| '/forgot-password'
| '/login'
| '/register'
| '/reset-password'
| '/verify-authentication'
| '/verify-email'
| '/privacy'
| '/terms'
| '/'
| '/dashboard/accounts'
| '/dashboard/connect'
| '/dashboard/settings'
| '/dashboard/feedback'
| '/dashboard/ical'
| '/dashboard/report'
| '/blog/$slug'
| '/auth/google'
| '/auth/outlook'
| '/oauth/consent'
| '/dashboard/'
| '/blog/'
| '/dashboard/settings/api-tokens'
| '/dashboard/settings/change-password'
| '/dashboard/settings/passkeys'
| '/dashboard/connect/apple'
| '/dashboard/connect/caldav'
| '/dashboard/connect/fastmail'
| '/dashboard/connect/google'
| '/dashboard/connect/ical-link'
| '/dashboard/connect/ics-file'
| '/dashboard/connect/microsoft'
| '/dashboard/connect/outlook'
| '/dashboard/connect/'
| '/dashboard/events/'
| '/dashboard/integrations/'
| '/dashboard/settings/'
| '/dashboard/upgrade/'
| '/dashboard/accounts/$accountId/$calendarId'
| '/dashboard/accounts/$accountId/setup'
| '/dashboard/accounts/$accountId/'
fileRoutesByTo: FileRoutesByTo
to:
| '/auth'
| '/dashboard'
| '/forgot-password'
| '/login'
| '/register'
| '/reset-password'
| '/verify-authentication'
| '/verify-email'
| '/privacy'
| '/terms'
| '/'
| '/dashboard/accounts'
| '/dashboard/connect'
| '/dashboard/feedback'
| '/dashboard/ical'
| '/dashboard/report'
| '/blog/$slug'
| '/auth/google'
| '/auth/outlook'
| '/oauth/consent'
| '/blog'
| '/dashboard/settings/api-tokens'
| '/dashboard/settings/change-password'
| '/dashboard/settings/passkeys'
| '/dashboard/connect/apple'
| '/dashboard/connect/caldav'
| '/dashboard/connect/fastmail'
| '/dashboard/connect/google'
| '/dashboard/connect/ical-link'
| '/dashboard/connect/ics-file'
| '/dashboard/connect/microsoft'
| '/dashboard/connect/outlook'
| '/dashboard/events'
| '/dashboard/integrations'
| '/dashboard/settings'
| '/dashboard/upgrade'
| '/dashboard/accounts/$accountId/$calendarId'
| '/dashboard/accounts/$accountId/setup'
| '/dashboard/accounts/$accountId'
id:
| '__root__'
| '/(auth)'
| '/(dashboard)'
| '/(marketing)'
| '/(oauth)'
| '/(marketing)/blog'
| '/(oauth)/auth'
| '/(oauth)/dashboard'
| '/(auth)/forgot-password'
| '/(auth)/login'
| '/(auth)/register'
| '/(auth)/reset-password'
| '/(auth)/verify-authentication'
| '/(auth)/verify-email'
| '/(marketing)/privacy'
| '/(marketing)/terms'
| '/(marketing)/'
| '/(dashboard)/dashboard/accounts'
| '/(dashboard)/dashboard/connect'
| '/(dashboard)/dashboard/settings'
| '/(oauth)/dashboard/connect'
| '/(dashboard)/dashboard/feedback'
| '/(dashboard)/dashboard/ical'
| '/(dashboard)/dashboard/report'
| '/(marketing)/blog/$slug'
| '/(oauth)/auth/google'
| '/(oauth)/auth/outlook'
| '/(oauth)/oauth/consent'
| '/(dashboard)/dashboard/'
| '/(marketing)/blog/'
| '/(dashboard)/dashboard/settings/api-tokens'
| '/(dashboard)/dashboard/settings/change-password'
| '/(dashboard)/dashboard/settings/passkeys'
| '/(oauth)/dashboard/connect/apple'
| '/(oauth)/dashboard/connect/caldav'
| '/(oauth)/dashboard/connect/fastmail'
| '/(oauth)/dashboard/connect/google'
| '/(oauth)/dashboard/connect/ical-link'
| '/(oauth)/dashboard/connect/ics-file'
| '/(oauth)/dashboard/connect/microsoft'
| '/(oauth)/dashboard/connect/outlook'
| '/(dashboard)/dashboard/connect/'
| '/(dashboard)/dashboard/events/'
| '/(dashboard)/dashboard/integrations/'
| '/(dashboard)/dashboard/settings/'
| '/(dashboard)/dashboard/upgrade/'
| '/(dashboard)/dashboard/accounts/$accountId/$calendarId'
| '/(dashboard)/dashboard/accounts/$accountId/setup'
| '/(dashboard)/dashboard/accounts/$accountId/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
authRouteRoute: typeof authRouteRouteWithChildren
dashboardRouteRoute: typeof dashboardRouteRouteWithChildren
marketingRouteRoute: typeof marketingRouteRouteWithChildren
oauthRouteRoute: typeof oauthRouteRouteWithChildren
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/(oauth)': {
id: '/(oauth)'
path: ''
fullPath: ''
preLoaderRoute: typeof oauthRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(marketing)': {
id: '/(marketing)'
path: ''
fullPath: ''
preLoaderRoute: typeof marketingRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(dashboard)': {
id: '/(dashboard)'
path: ''
fullPath: ''
preLoaderRoute: typeof dashboardRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(auth)': {
id: '/(auth)'
path: ''
fullPath: ''
preLoaderRoute: typeof authRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(marketing)/': {
id: '/(marketing)/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof marketingIndexRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(marketing)/terms': {
id: '/(marketing)/terms'
path: '/terms'
fullPath: '/terms'
preLoaderRoute: typeof marketingTermsRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(marketing)/privacy': {
id: '/(marketing)/privacy'
path: '/privacy'
fullPath: '/privacy'
preLoaderRoute: typeof marketingPrivacyRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(auth)/verify-email': {
id: '/(auth)/verify-email'
path: '/verify-email'
fullPath: '/verify-email'
preLoaderRoute: typeof authVerifyEmailRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/verify-authentication': {
id: '/(auth)/verify-authentication'
path: '/verify-authentication'
fullPath: '/verify-authentication'
preLoaderRoute: typeof authVerifyAuthenticationRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/reset-password': {
id: '/(auth)/reset-password'
path: '/reset-password'
fullPath: '/reset-password'
preLoaderRoute: typeof authResetPasswordRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/register': {
id: '/(auth)/register'
path: '/register'
fullPath: '/register'
preLoaderRoute: typeof authRegisterRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/login': {
id: '/(auth)/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof authLoginRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/forgot-password': {
id: '/(auth)/forgot-password'
path: '/forgot-password'
fullPath: '/forgot-password'
preLoaderRoute: typeof authForgotPasswordRouteImport
parentRoute: typeof authRouteRoute
}
'/(oauth)/dashboard': {
id: '/(oauth)/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof oauthDashboardRouteRouteImport
parentRoute: typeof oauthRouteRoute
}
'/(oauth)/auth': {
id: '/(oauth)/auth'
path: '/auth'
fullPath: '/auth'
preLoaderRoute: typeof oauthAuthRouteRouteImport
parentRoute: typeof oauthRouteRoute
}
'/(marketing)/blog': {
id: '/(marketing)/blog'
path: '/blog'
fullPath: '/blog'
preLoaderRoute: typeof marketingBlogRouteRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(marketing)/blog/': {
id: '/(marketing)/blog/'
path: '/'
fullPath: '/blog/'
preLoaderRoute: typeof marketingBlogIndexRouteImport
parentRoute: typeof marketingBlogRouteRoute
}
'/(dashboard)/dashboard/': {
id: '/(dashboard)/dashboard/'
path: '/dashboard'
fullPath: '/dashboard/'
preLoaderRoute: typeof dashboardDashboardIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(oauth)/oauth/consent': {
id: '/(oauth)/oauth/consent'
path: '/oauth/consent'
fullPath: '/oauth/consent'
preLoaderRoute: typeof oauthOauthConsentRouteImport
parentRoute: typeof oauthRouteRoute
}
'/(oauth)/auth/outlook': {
id: '/(oauth)/auth/outlook'
path: '/outlook'
fullPath: '/auth/outlook'
preLoaderRoute: typeof oauthAuthOutlookRouteImport
parentRoute: typeof oauthAuthRouteRoute
}
'/(oauth)/auth/google': {
id: '/(oauth)/auth/google'
path: '/google'
fullPath: '/auth/google'
preLoaderRoute: typeof oauthAuthGoogleRouteImport
parentRoute: typeof oauthAuthRouteRoute
}
'/(marketing)/blog/$slug': {
id: '/(marketing)/blog/$slug'
path: '/$slug'
fullPath: '/blog/$slug'
preLoaderRoute: typeof marketingBlogSlugRouteImport
parentRoute: typeof marketingBlogRouteRoute
}
'/(dashboard)/dashboard/report': {
id: '/(dashboard)/dashboard/report'
path: '/dashboard/report'
fullPath: '/dashboard/report'
preLoaderRoute: typeof dashboardDashboardReportRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/ical': {
id: '/(dashboard)/dashboard/ical'
path: '/dashboard/ical'
fullPath: '/dashboard/ical'
preLoaderRoute: typeof dashboardDashboardIcalRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/feedback': {
id: '/(dashboard)/dashboard/feedback'
path: '/dashboard/feedback'
fullPath: '/dashboard/feedback'
preLoaderRoute: typeof dashboardDashboardFeedbackRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(oauth)/dashboard/connect': {
id: '/(oauth)/dashboard/connect'
path: '/connect'
fullPath: '/dashboard/connect'
preLoaderRoute: typeof oauthDashboardConnectRouteRouteImport
parentRoute: typeof oauthDashboardRouteRoute
}
'/(dashboard)/dashboard/settings': {
id: '/(dashboard)/dashboard/settings'
path: '/dashboard/settings'
fullPath: '/dashboard/settings'
preLoaderRoute: typeof dashboardDashboardSettingsRouteRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/connect': {
id: '/(dashboard)/dashboard/connect'
path: '/dashboard/connect'
fullPath: '/dashboard/connect'
preLoaderRoute: typeof dashboardDashboardConnectRouteRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/accounts': {
id: '/(dashboard)/dashboard/accounts'
path: '/dashboard/accounts'
fullPath: '/dashboard/accounts'
preLoaderRoute: typeof dashboardDashboardAccountsRouteRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/upgrade/': {
id: '/(dashboard)/dashboard/upgrade/'
path: '/dashboard/upgrade'
fullPath: '/dashboard/upgrade/'
preLoaderRoute: typeof dashboardDashboardUpgradeIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/settings/': {
id: '/(dashboard)/dashboard/settings/'
path: '/'
fullPath: '/dashboard/settings/'
preLoaderRoute: typeof dashboardDashboardSettingsIndexRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/integrations/': {
id: '/(dashboard)/dashboard/integrations/'
path: '/dashboard/integrations'
fullPath: '/dashboard/integrations/'
preLoaderRoute: typeof dashboardDashboardIntegrationsIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/events/': {
id: '/(dashboard)/dashboard/events/'
path: '/dashboard/events'
fullPath: '/dashboard/events/'
preLoaderRoute: typeof dashboardDashboardEventsIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/connect/': {
id: '/(dashboard)/dashboard/connect/'
path: '/'
fullPath: '/dashboard/connect/'
preLoaderRoute: typeof dashboardDashboardConnectIndexRouteImport
parentRoute: typeof dashboardDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/outlook': {
id: '/(oauth)/dashboard/connect/outlook'
path: '/outlook'
fullPath: '/dashboard/connect/outlook'
preLoaderRoute: typeof oauthDashboardConnectOutlookRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/microsoft': {
id: '/(oauth)/dashboard/connect/microsoft'
path: '/microsoft'
fullPath: '/dashboard/connect/microsoft'
preLoaderRoute: typeof oauthDashboardConnectMicrosoftRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/ics-file': {
id: '/(oauth)/dashboard/connect/ics-file'
path: '/ics-file'
fullPath: '/dashboard/connect/ics-file'
preLoaderRoute: typeof oauthDashboardConnectIcsFileRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/ical-link': {
id: '/(oauth)/dashboard/connect/ical-link'
path: '/ical-link'
fullPath: '/dashboard/connect/ical-link'
preLoaderRoute: typeof oauthDashboardConnectIcalLinkRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/google': {
id: '/(oauth)/dashboard/connect/google'
path: '/google'
fullPath: '/dashboard/connect/google'
preLoaderRoute: typeof oauthDashboardConnectGoogleRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/fastmail': {
id: '/(oauth)/dashboard/connect/fastmail'
path: '/fastmail'
fullPath: '/dashboard/connect/fastmail'
preLoaderRoute: typeof oauthDashboardConnectFastmailRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/caldav': {
id: '/(oauth)/dashboard/connect/caldav'
path: '/caldav'
fullPath: '/dashboard/connect/caldav'
preLoaderRoute: typeof oauthDashboardConnectCaldavRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/apple': {
id: '/(oauth)/dashboard/connect/apple'
path: '/apple'
fullPath: '/dashboard/connect/apple'
preLoaderRoute: typeof oauthDashboardConnectAppleRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(dashboard)/dashboard/settings/passkeys': {
id: '/(dashboard)/dashboard/settings/passkeys'
path: '/passkeys'
fullPath: '/dashboard/settings/passkeys'
preLoaderRoute: typeof dashboardDashboardSettingsPasskeysRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/settings/change-password': {
id: '/(dashboard)/dashboard/settings/change-password'
path: '/change-password'
fullPath: '/dashboard/settings/change-password'
preLoaderRoute: typeof dashboardDashboardSettingsChangePasswordRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/settings/api-tokens': {
id: '/(dashboard)/dashboard/settings/api-tokens'
path: '/api-tokens'
fullPath: '/dashboard/settings/api-tokens'
preLoaderRoute: typeof dashboardDashboardSettingsApiTokensRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/accounts/$accountId/': {
id: '/(dashboard)/dashboard/accounts/$accountId/'
path: '/$accountId'
fullPath: '/dashboard/accounts/$accountId/'
preLoaderRoute: typeof dashboardDashboardAccountsAccountIdIndexRouteImport
parentRoute: typeof dashboardDashboardAccountsRouteRoute
}
'/(dashboard)/dashboard/accounts/$accountId/setup': {
id: '/(dashboard)/dashboard/accounts/$accountId/setup'
path: '/$accountId/setup'
fullPath: '/dashboard/accounts/$accountId/setup'
preLoaderRoute: typeof dashboardDashboardAccountsAccountIdSetupRouteImport
parentRoute: typeof dashboardDashboardAccountsRouteRoute
}
'/(dashboard)/dashboard/accounts/$accountId/$calendarId': {
id: '/(dashboard)/dashboard/accounts/$accountId/$calendarId'
path: '/$accountId/$calendarId'
fullPath: '/dashboard/accounts/$accountId/$calendarId'
preLoaderRoute: typeof dashboardDashboardAccountsAccountIdCalendarIdRouteImport
parentRoute: typeof dashboardDashboardAccountsRouteRoute
}
}
}
interface authRouteRouteChildren {
authForgotPasswordRoute: typeof authForgotPasswordRoute
authLoginRoute: typeof authLoginRoute
authRegisterRoute: typeof authRegisterRoute
authResetPasswordRoute: typeof authResetPasswordRoute
authVerifyAuthenticationRoute: typeof authVerifyAuthenticationRoute
authVerifyEmailRoute: typeof authVerifyEmailRoute
}
const authRouteRouteChildren: authRouteRouteChildren = {
authForgotPasswordRoute: authForgotPasswordRoute,
authLoginRoute: authLoginRoute,
authRegisterRoute: authRegisterRoute,
authResetPasswordRoute: authResetPasswordRoute,
authVerifyAuthenticationRoute: authVerifyAuthenticationRoute,
authVerifyEmailRoute: authVerifyEmailRoute,
}
const authRouteRouteWithChildren = authRouteRoute._addFileChildren(
authRouteRouteChildren,
)
interface dashboardDashboardAccountsRouteRouteChildren {
dashboardDashboardAccountsAccountIdCalendarIdRoute: typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
dashboardDashboardAccountsAccountIdSetupRoute: typeof dashboardDashboardAccountsAccountIdSetupRoute
dashboardDashboardAccountsAccountIdIndexRoute: typeof dashboardDashboardAccountsAccountIdIndexRoute
}
const dashboardDashboardAccountsRouteRouteChildren: dashboardDashboardAccountsRouteRouteChildren =
{
dashboardDashboardAccountsAccountIdCalendarIdRoute:
dashboardDashboardAccountsAccountIdCalendarIdRoute,
dashboardDashboardAccountsAccountIdSetupRoute:
dashboardDashboardAccountsAccountIdSetupRoute,
dashboardDashboardAccountsAccountIdIndexRoute:
dashboardDashboardAccountsAccountIdIndexRoute,
}
const dashboardDashboardAccountsRouteRouteWithChildren =
dashboardDashboardAccountsRouteRoute._addFileChildren(
dashboardDashboardAccountsRouteRouteChildren,
)
interface dashboardDashboardConnectRouteRouteChildren {
dashboardDashboardConnectIndexRoute: typeof dashboardDashboardConnectIndexRoute
}
const dashboardDashboardConnectRouteRouteChildren: dashboardDashboardConnectRouteRouteChildren =
{
dashboardDashboardConnectIndexRoute: dashboardDashboardConnectIndexRoute,
}
const dashboardDashboardConnectRouteRouteWithChildren =
dashboardDashboardConnectRouteRoute._addFileChildren(
dashboardDashboardConnectRouteRouteChildren,
)
interface dashboardDashboardSettingsRouteRouteChildren {
dashboardDashboardSettingsApiTokensRoute: typeof dashboardDashboardSettingsApiTokensRoute
dashboardDashboardSettingsChangePasswordRoute: typeof dashboardDashboardSettingsChangePasswordRoute
dashboardDashboardSettingsPasskeysRoute: typeof dashboardDashboardSettingsPasskeysRoute
dashboardDashboardSettingsIndexRoute: typeof dashboardDashboardSettingsIndexRoute
}
const dashboardDashboardSettingsRouteRouteChildren: dashboardDashboardSettingsRouteRouteChildren =
{
dashboardDashboardSettingsApiTokensRoute:
dashboardDashboardSettingsApiTokensRoute,
dashboardDashboardSettingsChangePasswordRoute:
dashboardDashboardSettingsChangePasswordRoute,
dashboardDashboardSettingsPasskeysRoute:
dashboardDashboardSettingsPasskeysRoute,
dashboardDashboardSettingsIndexRoute: dashboardDashboardSettingsIndexRoute,
}
const dashboardDashboardSettingsRouteRouteWithChildren =
dashboardDashboardSettingsRouteRoute._addFileChildren(
dashboardDashboardSettingsRouteRouteChildren,
)
interface dashboardRouteRouteChildren {
dashboardDashboardAccountsRouteRoute: typeof dashboardDashboardAccountsRouteRouteWithChildren
dashboardDashboardConnectRouteRoute: typeof dashboardDashboardConnectRouteRouteWithChildren
dashboardDashboardSettingsRouteRoute: typeof dashboardDashboardSettingsRouteRouteWithChildren
dashboardDashboardFeedbackRoute: typeof dashboardDashboardFeedbackRoute
dashboardDashboardIcalRoute: typeof dashboardDashboardIcalRoute
dashboardDashboardReportRoute: typeof dashboardDashboardReportRoute
dashboardDashboardIndexRoute: typeof dashboardDashboardIndexRoute
dashboardDashboardEventsIndexRoute: typeof dashboardDashboardEventsIndexRoute
dashboardDashboardIntegrationsIndexRoute: typeof dashboardDashboardIntegrationsIndexRoute
dashboardDashboardUpgradeIndexRoute: typeof dashboardDashboardUpgradeIndexRoute
}
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardDashboardAccountsRouteRoute:
dashboardDashboardAccountsRouteRouteWithChildren,
dashboardDashboardConnectRouteRoute:
dashboardDashboardConnectRouteRouteWithChildren,
dashboardDashboardSettingsRouteRoute:
dashboardDashboardSettingsRouteRouteWithChildren,
dashboardDashboardFeedbackRoute: dashboardDashboardFeedbackRoute,
dashboardDashboardIcalRoute: dashboardDashboardIcalRoute,
dashboardDashboardReportRoute: dashboardDashboardReportRoute,
dashboardDashboardIndexRoute: dashboardDashboardIndexRoute,
dashboardDashboardEventsIndexRoute: dashboardDashboardEventsIndexRoute,
dashboardDashboardIntegrationsIndexRoute:
dashboardDashboardIntegrationsIndexRoute,
dashboardDashboardUpgradeIndexRoute: dashboardDashboardUpgradeIndexRoute,
}
const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren(
dashboardRouteRouteChildren,
)
interface marketingBlogRouteRouteChildren {
marketingBlogSlugRoute: typeof marketingBlogSlugRoute
marketingBlogIndexRoute: typeof marketingBlogIndexRoute
}
const marketingBlogRouteRouteChildren: marketingBlogRouteRouteChildren = {
marketingBlogSlugRoute: marketingBlogSlugRoute,
marketingBlogIndexRoute: marketingBlogIndexRoute,
}
const marketingBlogRouteRouteWithChildren =
marketingBlogRouteRoute._addFileChildren(marketingBlogRouteRouteChildren)
interface marketingRouteRouteChildren {
marketingBlogRouteRoute: typeof marketingBlogRouteRouteWithChildren
marketingPrivacyRoute: typeof marketingPrivacyRoute
marketingTermsRoute: typeof marketingTermsRoute
marketingIndexRoute: typeof marketingIndexRoute
}
const marketingRouteRouteChildren: marketingRouteRouteChildren = {
marketingBlogRouteRoute: marketingBlogRouteRouteWithChildren,
marketingPrivacyRoute: marketingPrivacyRoute,
marketingTermsRoute: marketingTermsRoute,
marketingIndexRoute: marketingIndexRoute,
}
const marketingRouteRouteWithChildren = marketingRouteRoute._addFileChildren(
marketingRouteRouteChildren,
)
interface oauthAuthRouteRouteChildren {
oauthAuthGoogleRoute: typeof oauthAuthGoogleRoute
oauthAuthOutlookRoute: typeof oauthAuthOutlookRoute
}
const oauthAuthRouteRouteChildren: oauthAuthRouteRouteChildren = {
oauthAuthGoogleRoute: oauthAuthGoogleRoute,
oauthAuthOutlookRoute: oauthAuthOutlookRoute,
}
const oauthAuthRouteRouteWithChildren = oauthAuthRouteRoute._addFileChildren(
oauthAuthRouteRouteChildren,
)
interface oauthDashboardConnectRouteRouteChildren {
oauthDashboardConnectAppleRoute: typeof oauthDashboardConnectAppleRoute
oauthDashboardConnectCaldavRoute: typeof oauthDashboardConnectCaldavRoute
oauthDashboardConnectFastmailRoute: typeof oauthDashboardConnectFastmailRoute
oauthDashboardConnectGoogleRoute: typeof oauthDashboardConnectGoogleRoute
oauthDashboardConnectIcalLinkRoute: typeof oauthDashboardConnectIcalLinkRoute
oauthDashboardConnectIcsFileRoute: typeof oauthDashboardConnectIcsFileRoute
oauthDashboardConnectMicrosoftRoute: typeof oauthDashboardConnectMicrosoftRoute
oauthDashboardConnectOutlookRoute: typeof oauthDashboardConnectOutlookRoute
}
const oauthDashboardConnectRouteRouteChildren: oauthDashboardConnectRouteRouteChildren =
{
oauthDashboardConnectAppleRoute: oauthDashboardConnectAppleRoute,
oauthDashboardConnectCaldavRoute: oauthDashboardConnectCaldavRoute,
oauthDashboardConnectFastmailRoute: oauthDashboardConnectFastmailRoute,
oauthDashboardConnectGoogleRoute: oauthDashboardConnectGoogleRoute,
oauthDashboardConnectIcalLinkRoute: oauthDashboardConnectIcalLinkRoute,
oauthDashboardConnectIcsFileRoute: oauthDashboardConnectIcsFileRoute,
oauthDashboardConnectMicrosoftRoute: oauthDashboardConnectMicrosoftRoute,
oauthDashboardConnectOutlookRoute: oauthDashboardConnectOutlookRoute,
}
const oauthDashboardConnectRouteRouteWithChildren =
oauthDashboardConnectRouteRoute._addFileChildren(
oauthDashboardConnectRouteRouteChildren,
)
interface oauthDashboardRouteRouteChildren {
oauthDashboardConnectRouteRoute: typeof oauthDashboardConnectRouteRouteWithChildren
}
const oauthDashboardRouteRouteChildren: oauthDashboardRouteRouteChildren = {
oauthDashboardConnectRouteRoute: oauthDashboardConnectRouteRouteWithChildren,
}
const oauthDashboardRouteRouteWithChildren =
oauthDashboardRouteRoute._addFileChildren(oauthDashboardRouteRouteChildren)
interface oauthRouteRouteChildren {
oauthAuthRouteRoute: typeof oauthAuthRouteRouteWithChildren
oauthDashboardRouteRoute: typeof oauthDashboardRouteRouteWithChildren
oauthOauthConsentRoute: typeof oauthOauthConsentRoute
}
const oauthRouteRouteChildren: oauthRouteRouteChildren = {
oauthAuthRouteRoute: oauthAuthRouteRouteWithChildren,
oauthDashboardRouteRoute: oauthDashboardRouteRouteWithChildren,
oauthOauthConsentRoute: oauthOauthConsentRoute,
}
const oauthRouteRouteWithChildren = oauthRouteRoute._addFileChildren(
oauthRouteRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
authRouteRoute: authRouteRouteWithChildren,
dashboardRouteRoute: dashboardRouteRouteWithChildren,
marketingRouteRoute: marketingRouteRouteWithChildren,
oauthRouteRoute: oauthRouteRouteWithChildren,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes()
================================================
FILE: applications/web/src/hooks/use-animated-swr.ts
================================================
import { useState } from "react";
import useSWR from "swr";
import type { SWRConfiguration, SWRResponse } from "swr";
interface AnimatedSWRResponse extends SWRResponse {
shouldAnimate: boolean;
}
function useAnimatedSWR(key: string, config?: SWRConfiguration): AnimatedSWRResponse {
const response = useSWR(key, config);
const [shouldAnimate] = useState(response.isLoading);
return { ...response, shouldAnimate };
}
export { useAnimatedSWR };
================================================
FILE: applications/web/src/hooks/use-api-tokens.ts
================================================
import useSWR from "swr";
import { fetcher, apiFetch } from "@/lib/fetcher";
export interface ApiToken {
id: string;
name: string;
tokenPrefix: string;
lastUsedAt: string | null;
expiresAt: string | null;
createdAt: string;
}
export interface CreatedApiToken {
id: string;
name: string;
token: string;
tokenPrefix: string;
createdAt: string;
}
export const useApiTokens = () => {
return useSWR("/api/tokens", fetcher);
};
export const createApiToken = async (name: string): Promise => {
const response = await apiFetch("/api/tokens", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
return response.json();
};
export const deleteApiToken = async (id: string): Promise => {
await apiFetch(`/api/tokens/${id}`, { method: "DELETE" });
};
================================================
FILE: applications/web/src/hooks/use-entitlements.ts
================================================
import { useMemo, useCallback } from "react";
import useSWR, { useSWRConfig } from "swr";
import { fetcher } from "@/lib/fetcher";
import { useSubscription } from "./use-subscription";
interface EntitlementLimit {
current: number;
limit: number | null;
}
interface Entitlements {
plan: "free" | "pro";
accounts: EntitlementLimit;
mappings: EntitlementLimit;
canCustomizeIcalFeed: boolean;
canUseEventFilters: boolean;
}
const USAGE_CACHE_KEY = "/api/entitlements";
function useEntitlements() {
const { data: subscription } = useSubscription();
const {
data: serverEntitlements,
mutate,
isLoading,
error,
} = useSWR(USAGE_CACHE_KEY, fetcher);
const data = useMemo(() => {
if (serverEntitlements) {
return serverEntitlements;
}
if (subscription?.plan !== "pro") {
return undefined;
}
return {
plan: "pro",
accounts: { current: 0, limit: null },
mappings: { current: 0, limit: null },
canCustomizeIcalFeed: true,
canUseEventFilters: true,
};
}, [serverEntitlements, subscription?.plan]);
return { data, mutate, isLoading, error };
}
function useMutateEntitlements() {
const { mutate } = useSWRConfig();
const adjustMappingCount = useCallback(
(delta: number) => {
mutate(
USAGE_CACHE_KEY,
(current) => {
if (!current) return current;
const nextCount = Math.max(0, current.mappings.current + delta);
return {
...current,
mappings: {
...current.mappings,
current: nextCount,
},
};
},
{ revalidate: false },
);
},
[mutate],
);
const revalidateEntitlements = useCallback(() => {
return mutate(USAGE_CACHE_KEY);
}, [mutate]);
return { adjustMappingCount, revalidateEntitlements };
}
function canAddMore(entitlement: EntitlementLimit | undefined): boolean {
if (!entitlement) return true;
if (entitlement.limit === null) return true;
return entitlement.current < entitlement.limit;
}
export { useEntitlements, useMutateEntitlements, canAddMore, USAGE_CACHE_KEY };
export type { Entitlements, EntitlementLimit };
================================================
FILE: applications/web/src/hooks/use-events.ts
================================================
import useSWRInfinite from "swr/infinite";
import { fetcher } from "@/lib/fetcher";
import { useStartOfToday } from "./use-start-of-today";
import type { ApiEvent } from "@/types/api";
export interface CalendarEvent {
id: string;
startTime: Date;
endTime: Date;
calendarId: string;
calendarName: string;
calendarProvider: string;
calendarUrl: string;
}
const DAYS_PER_PAGE = 7;
const buildEventsUrl = (from: Date, to: Date): string => {
const url = new URL("/api/events", globalThis.location.origin);
url.searchParams.set("from", from.toISOString());
url.searchParams.set("to", to.toISOString());
return url.toString();
};
const fetchEvents = async (url: string): Promise => {
const data = await fetcher(url);
return data.map((event) => ({
id: event.id,
startTime: new Date(event.startTime),
endTime: new Date(event.endTime),
calendarId: event.calendarId,
calendarName: event.calendarName,
calendarProvider: event.calendarProvider,
calendarUrl: event.calendarUrl,
}));
};
export function useEvents() {
const todayStart = useStartOfToday();
const getKey = (pageIndex: number): string => {
const from = new Date(todayStart);
from.setDate(from.getDate() + pageIndex * DAYS_PER_PAGE);
const to = new Date(from);
to.setDate(to.getDate() + DAYS_PER_PAGE - 1);
to.setHours(23, 59, 59, 999);
return buildEventsUrl(from, to);
};
const { data, error, setSize, isLoading, isValidating } = useSWRInfinite(
getKey,
fetchEvents,
{ revalidateFirstPage: false, keepPreviousData: true },
);
const events = resolveEvents(data);
const hasMore = !data || (data[data.length - 1]?.length ?? 0) > 0;
const loadMore = () => {
void setSize((prev) => prev + 1);
};
return { events, error, isLoading, isValidating, hasMore, loadMore };
}
const deduplicateEvents = (events: CalendarEvent[]): CalendarEvent[] => [
...new Map(events.map((event) => [event.id, event])).values(),
];
function resolveEvents(data: CalendarEvent[][] | undefined): CalendarEvent[] {
if (data) return deduplicateEvents(data.flat());
return [];
}
================================================
FILE: applications/web/src/hooks/use-has-password.ts
================================================
import useSWR from "swr";
import { authClient } from "@/lib/auth-client";
const fetchHasPassword = async (): Promise => {
const { data } = await authClient.listAccounts();
return data?.some((account) => account.providerId === "credential") ?? false;
};
export const useHasPassword = () => useSWR("auth/has-password", fetchHasPassword);
================================================
FILE: applications/web/src/hooks/use-passkeys.ts
================================================
import useSWR from "swr";
import { authClient } from "@/lib/auth-client";
export interface Passkey {
id: string;
name?: string | null;
createdAt: Date;
}
const fetchPasskeys = async (): Promise => {
const { data } = await authClient.passkey.listUserPasskeys();
return data ?? [];
};
export const addPasskey = async () => {
await authClient.passkey.addPasskey();
};
export const deletePasskey = async (id: string) => {
await authClient.passkey.deletePasskey({ id });
};
export const usePasskeys = (enabled = true) => {
return useSWR(enabled ? "auth/passkeys" : null, fetchPasskeys);
};
================================================
FILE: applications/web/src/hooks/use-session.ts
================================================
import useSWR from "swr";
import { authClient } from "@/lib/auth-client";
export interface SessionUser {
id: string;
email?: string;
name?: string;
username?: string;
}
const fetchSession = async (): Promise => {
const { data } = await authClient.getSession();
if (!data?.user) return null;
const username =
"username" in data.user && typeof data.user.username === "string"
? data.user.username
: undefined;
const { id, email, name } = data.user;
return { id, email, name, username };
};
export function useSession() {
const { data: user, error, isLoading } = useSWR("auth/session", fetchSession, {
revalidateOnFocus: false,
revalidateOnReconnect: true,
});
return { user: user ?? null, error, isLoading };
}
================================================
FILE: applications/web/src/hooks/use-start-of-today.ts
================================================
import { useEffect, useState } from "react";
function resolveStartOfToday(): Date {
const date = new Date();
date.setHours(0, 0, 0, 0);
return date;
}
function resolveMillisecondsUntilTomorrow(): number {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setHours(24, 0, 0, 0);
return tomorrow.getTime() - now.getTime();
}
export function useStartOfToday(): Date {
const [todayStart, setTodayStart] = useState(resolveStartOfToday);
useEffect(() => {
const timeoutId = globalThis.setTimeout(() => {
setTodayStart(resolveStartOfToday());
}, resolveMillisecondsUntilTomorrow());
return () => {
globalThis.clearTimeout(timeoutId);
};
}, [todayStart]);
return todayStart;
}
================================================
FILE: applications/web/src/hooks/use-subscription.ts
================================================
import useSWR from "swr";
import { fetcher } from "@/lib/fetcher";
import { getCommercialMode } from "@/config/commercial";
export interface SubscriptionState {
plan: "free" | "pro";
interval: "month" | "year" | null;
}
interface ActiveSubscription {
recurringInterval?: "month" | "year" | null;
}
interface CustomerStateResponse {
activeSubscriptions?: ActiveSubscription[] | null;
}
const SUBSCRIPTION_STATE_CACHE_KEY = "customer-state";
export const resolveSubscriptionState = (
customerState: CustomerStateResponse,
): SubscriptionState => {
const [active] = customerState.activeSubscriptions ?? [];
if (!active) {
return { plan: "free", interval: null };
}
return {
plan: "pro",
interval: active.recurringInterval === "year" ? "year" : "month",
};
};
const fetchSubscriptionState = async (): Promise => {
const data = await fetcher("/api/auth/customer/state");
return resolveSubscriptionState(data);
};
interface UseSubscriptionOptions {
enabled?: boolean;
fallbackData?: SubscriptionState;
}
const resolveSubscriptionCacheKey = (enabled: boolean): string | null => {
if (!enabled) {
return null;
}
return SUBSCRIPTION_STATE_CACHE_KEY;
};
export function useSubscription(options: UseSubscriptionOptions = {}) {
const { enabled = getCommercialMode(), fallbackData } = options;
const cacheKey = resolveSubscriptionCacheKey(enabled);
const { data, error, isLoading, mutate } = useSWR(
cacheKey,
fetchSubscriptionState,
{ fallbackData },
);
return { data, error, isLoading, mutate };
}
export async function fetchSubscriptionStateWithApi(
fetchApi: (path: string, init?: RequestInit) => Promise,
): Promise {
const data = await fetchApi("/api/auth/customer/state");
return resolveSubscriptionState(data);
}
export { fetchSubscriptionState };
================================================
FILE: applications/web/src/illustrations/how-it-works-configure.tsx
================================================
function MiniToggle({ checked }: { checked: boolean }) {
return (
);
}
const SETTINGS = [
{ label: "Sync Event Name", checked: true },
{ label: "Sync Event Description", checked: true },
{ label: "Sync Event Location", checked: false },
{ label: "Sync Event Status", checked: true },
{ label: "Sync Attendees", checked: false },
];
function MiniSettingsRow({ label, checked }: { label: string; checked: boolean }) {
return (
);
}
export function HowItWorksConfigure() {
return (
{SETTINGS.map(({ label, checked }) => (
))}
);
}
================================================
FILE: applications/web/src/illustrations/how-it-works-connect.tsx
================================================
import ArrowRightIcon from "lucide-react/dist/esm/icons/arrow-right";
const PROVIDERS = [
{ icon: "/integrations/icon-google-calendar.svg", label: "Connect Google Calendar" },
{ icon: "/integrations/icon-outlook.svg", label: "Connect Outlook" },
{ icon: "/integrations/icon-icloud.svg", label: "Connect iCloud" },
{ icon: "/integrations/icon-fastmail.svg", label: "Connect Fastmail" },
{ icon: "/integrations/icon-microsoft-365.svg", label: "Connect Microsoft 365" },
];
function MiniMenuRow({ icon, label }: { icon: string; label: string }) {
return (
{label}
);
}
export function HowItWorksConnect() {
return (
{PROVIDERS.map(({ icon, label }) => (
))}
);
}
================================================
FILE: applications/web/src/illustrations/how-it-works-sync.tsx
================================================
import { useEffect, useRef, useState } from "react";
import { LazyMotion } from "motion/react";
import * as m from "motion/react-m";
import { loadMotionFeatures } from "@/lib/motion-features";
import { tv } from "tailwind-variants/lite";
import { Text } from "@/components/ui/primitives/text";
const PLACEHOLDER_COUNTS = [3, 5, 2, 7, 4, 6, 1, 8, 3, 5, 4, 6, 2, 7, 5];
const GRAPH_HEIGHT = 128;
const MIN_BAR_HEIGHT = 16;
const GROWTH_SPACE = GRAPH_HEIGHT - MIN_BAR_HEIGHT;
const MAX_COUNT = Math.max(...PLACEHOLDER_COUNTS);
const TOTAL_EVENTS = PLACEHOLDER_COUNTS.reduce((sum, count) => sum + count, 0);
const TODAY_INDEX = 7;
type Period = "past" | "today" | "future";
const barStyle = tv({
base: "flex-1 rounded-[0.625rem]",
variants: {
period: {
past: "bg-background-hover border border-border-elevated",
today: "bg-emerald-400 border-transparent",
future:
"bg-emerald-400 border-emerald-500 bg-[repeating-linear-gradient(-45deg,transparent_0_4px,var(--color-illustration-stripe)_4px_8px)]",
},
},
});
function resolvePeriod(index: number): Period {
if (index < TODAY_INDEX) return "past";
if (index === TODAY_INDEX) return "today";
return "future";
}
function resolveBarHeight(count: number): number {
return MIN_BAR_HEIGHT + (count / MAX_COUNT) * GROWTH_SPACE;
}
interface BarData {
height: number;
period: Period;
index: number;
}
const BARS: BarData[] = PLACEHOLDER_COUNTS.map((count, index) => ({
height: resolveBarHeight(count),
period: resolvePeriod(index),
index,
}));
const BAR_TRANSITION_EASE = [0.4, 0, 0.2, 1] as const;
export function HowItWorksSync() {
const containerRef = useRef(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const element = containerRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ rootMargin: "-64px" },
);
observer.observe(element);
return () => observer.disconnect();
}, []);
return (
{TOTAL_EVENTS} events
This Week
);
}
================================================
FILE: applications/web/src/illustrations/marketing-illustration-contributors.tsx
================================================
import { AnimatePresence, LazyMotion } from "motion/react";
import * as m from "motion/react-m";
import { loadMotionFeatures } from "@/lib/motion-features";
import { memo, useEffect, useLayoutEffect, useRef, useState } from "react";
import { Text } from "@/components/ui/primitives/text";
import CONTRIBUTORS from "@/features/marketing/contributors.json";
const VISIBLE_COUNT = 3;
const ROTATE_INTERVAL_MS = 1800;
const FALLBACK_ROW_HEIGHT = 36;
const SLOT_STYLES = [
{ scale: 0.92, opacity: 0.35, filter: "blur(1px)" },
{ scale: 1, opacity: 1, filter: "blur(0px)" },
{ scale: 0.92, opacity: 0.35, filter: "blur(1px)" },
];
const TRANSITION = {
type: "tween" as const,
duration: 0.5,
ease: [0.4, 0, 0.2, 1] as const,
};
type Contributor = (typeof CONTRIBUTORS)[number];
const ContributorRow = memo(function ContributorRow({
contributor,
}: {
contributor: Contributor;
}) {
return (
{contributor.username}
{contributor.name}
);
});
export function MarketingIllustrationContributors() {
const [offset, setOffset] = useState(0);
const [rowHeight, setRowHeight] = useState(FALLBACK_ROW_HEIGHT);
const measureRef = useRef(null);
useLayoutEffect(() => {
if (measureRef.current) {
setRowHeight(measureRef.current.getBoundingClientRect().height);
}
}, []);
useEffect(() => {
const interval = setInterval(() => {
setOffset((previous) => (previous + 1) % CONTRIBUTORS.length);
}, ROTATE_INTERVAL_MS);
return () => clearInterval(interval);
}, []);
const visibleContributors = Array.from({ length: VISIBLE_COUNT }, (_, slot) => {
const contributorIndex = (offset + slot) % CONTRIBUTORS.length;
return { contributor: CONTRIBUTORS[contributorIndex], slot, contributorIndex };
});
return (
{visibleContributors.map(({ contributor, slot, contributorIndex }) => (
))}
);
}
================================================
FILE: applications/web/src/illustrations/marketing-illustration-providers.tsx
================================================
import { providerIcons } from "@/lib/providers";
const ORBIT_ITEMS = Object.entries(providerIcons).reverse();
const ORBIT_DURATION = 12;
const RADIUS = 110;
const ICON_SIZE = 32;
export function MarketingIllustrationProviders() {
return (
{ORBIT_ITEMS.map(([provider, iconPath], index) => {
const angle = (360 / ORBIT_ITEMS.length) * index;
const radians = (angle * Math.PI) / 180;
const posX = Math.cos(radians) * RADIUS;
const posY = Math.sin(radians) * RADIUS;
return (
);
})}
);
}
================================================
FILE: applications/web/src/illustrations/marketing-illustration-setup.tsx
================================================
import { AnimatePresence, LazyMotion } from "motion/react";
import * as m from "motion/react-m";
import { loadMotionFeatures } from "@/lib/motion-features";
import { useEffect, useRef, useState } from "react";
type Phase = "button" | "cursor" | "click" | "syncing" | "done";
const PHASE_DURATIONS: Record = {
button: 600,
cursor: 800,
click: 400,
syncing: 1800,
done: 800,
};
const PHASE_ORDER: Phase[] = ["button", "cursor", "click", "syncing", "done"];
const TRANSITION_ENTER = {
type: "tween" as const,
duration: 0.35,
ease: [0.4, 0, 0.2, 1] as const,
};
const INITIAL = { opacity: 0, scale: 0.9, filter: "blur(4px)" };
const ANIMATE = { opacity: 1, scale: 1, filter: "blur(0px)" };
const EXIT = { opacity: 0, scale: 0.9, filter: "blur(4px)" };
const CIRCLE_RADIUS = 16;
const CIRCLE_CIRCUMFERENCE = 2 * Math.PI * CIRCLE_RADIUS;
function SyncButton({ pressed }: { pressed: boolean }) {
return (
Sync Calendars
);
}
function SyncCircle() {
const circleRef = useRef(null);
useEffect(() => {
const frame = requestAnimationFrame(() => {
if (circleRef.current) {
circleRef.current.style.strokeDashoffset = "0";
}
});
return () => cancelAnimationFrame(frame);
}, []);
return (
);
}
export function MarketingIllustrationSetup() {
const [phaseIndex, setPhaseIndex] = useState(0);
const phase = PHASE_ORDER[phaseIndex];
useEffect(() => {
const timeout = setTimeout(() => {
setPhaseIndex((previous) => (previous + 1) % PHASE_ORDER.length);
}, PHASE_DURATIONS[phase]);
return () => clearTimeout(timeout);
}, [phase, phaseIndex]);
const showButton = phase === "button" || phase === "cursor" || phase === "click";
const showCursor = phase === "cursor" || phase === "click";
const showCircle = phase === "syncing" || phase === "done";
const pressed = phase === "click";
return (
{showButton && (
{showCursor && (
)}
)}
{showCircle && (
)}
);
}
================================================
FILE: applications/web/src/illustrations/marketing-illustration-sync.tsx
================================================
const R = 12;
const PATH_UPPER = `M -10,20 L ${210 - R},20 Q 210,20 210,${20 + R} L 210,${50 - R} Q 210,50 ${210 + R},50 L 310,50`;
const PATH_LOWER = `M -10,80 L ${210 - R},80 Q 210,80 210,${80 - R} L 210,${50 + R} Q 210,50 ${210 + R},50 L 310,50`;
const ICON_SIZE = 24;
const PROVIDERS = [
{ icon: "/integrations/icon-google-calendar.svg", x: 100, y: 20 },
{ icon: "/integrations/icon-outlook.svg", x: 100, y: 80 },
{ icon: "/integrations/icon-icloud.svg", x: 260, y: 50 },
];
export function MarketingIllustrationSync() {
return (
{PROVIDERS.map(({ icon, x, y }) => (
))}
);
}
================================================
FILE: applications/web/src/index.css
================================================
@import "tailwindcss";
@custom-variant pointer-hover (@media (hover: hover));
html {
@apply w-full min-h-full bg-background overflow-x-hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@apply w-full;
height: 100dvh;
overflow-y: scroll;
overflow-x: hidden;
}
#root {
@apply w-full;
}
html {
color-scheme: light;
}
:root {
--ui-foreground: var(--color-neutral-950);
--ui-background: var(--color-neutral-50);
--ui-background-inverse: var(--ui-foreground);
--ui-foreground-inverse: var(--ui-background);
--ui-interactive-border: var(--color-neutral-300);
--ui-foreground-muted: var(--color-neutral-500);
--ui-foreground-disabled: var(--color-neutral-300);
--ui-foreground-hover: color-mix(
in srgb,
var(--ui-foreground),
transparent 20%
);
--ui-illustration-stripe: color-mix(
in srgb,
var(--ui-foreground),
transparent 92%
);
--ui-foreground-inverse-muted: color-mix(
in srgb,
var(--ui-foreground-inverse),
transparent 40%
);
--ui-background-elevated: var(--color-white);
--ui-border-elevated: var(--color-neutral-200);
--ui-background-inverse-hover: var(--color-neutral-800);
--ui-background-hover: color-mix(
in srgb,
var(--ui-foreground),
var(--ui-background) 96.125%
);
--ui-destructive: var(--color-red-700);
--ui-destructive-background: var(--color-red-50);
--ui-destructive-border: var(--color-red-200);
--ui-destructive-background-hover: var(--color-red-100);
--ui-ring: var(--color-blue-500);
--ui-template: var(--color-purple-600);
--ui-template-muted: color-mix(in srgb, var(--ui-template), transparent 50%);
}
@font-face {
font-family: "Geist Sans";
src: url("/assets/fonts/Geist-variable.woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: "Geist Mono";
src: url("/assets/fonts/GeistMono-variable.woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: "Lora";
src: url("/assets/fonts/Lora-variable.woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
}
@theme {
--color-foreground: var(--ui-foreground);
--color-background: var(--ui-background);
--color-background-inverse: var(--ui-background-inverse);
--color-foreground-inverse: var(--ui-foreground-inverse);
--color-interactive-border: var(--ui-interactive-border);
--color-foreground-muted: var(--ui-foreground-muted);
--color-foreground-disabled: var(--ui-foreground-disabled);
--color-foreground-hover: var(--ui-foreground-hover);
--color-illustration-stripe: var(--ui-illustration-stripe);
--color-foreground-inverse-muted: var(--ui-foreground-inverse-muted);
--color-background-elevated: var(--ui-background-elevated);
--color-border-elevated: var(--ui-border-elevated);
--color-background-inverse-hover: var(--ui-background-inverse-hover);
--color-background-hover: var(--ui-background-hover);
--color-destructive: var(--ui-destructive);
--color-destructive-background: var(--ui-destructive-background);
--color-destructive-border: var(--ui-destructive-border);
--color-destructive-background-hover: var(--ui-destructive-background-hover);
--color-ring: var(--ui-ring);
--color-template: var(--ui-template);
--color-template-muted: var(--ui-template-muted);
--font-sans: "Geist Sans", system-ui, sans-serif;
--font-lora: "Lora", Georgia, serif;
--font-mono: "Geist Mono", ui-monospace, monospace;
--breakpoint-xs: 28rem;
--animate-shimmer: shimmer 3s linear infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -100% 0;
}
}
@keyframes spin {
to {
rotate: 360deg;
}
}
@keyframes counter-spin {
to {
rotate: -360deg;
}
}
@keyframes sync-progress {
0% {
stroke-dashoffset: 87.96;
}
50% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: 87.96;
}
}
@media (prefers-color-scheme: dark) {
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
-webkit-text-fill-color: var(--ui-foreground);
-webkit-background-clip: text;
caret-color: var(--ui-foreground);
}
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
:root {
--ui-foreground: var(--color-neutral-100);
--ui-background: var(--color-neutral-950);
--ui-background-inverse: var(--ui-foreground);
--ui-foreground-inverse: var(--ui-background);
--ui-interactive-border: var(--color-neutral-800);
--ui-foreground-muted: var(--color-neutral-400);
--ui-foreground-disabled: var(--color-neutral-600);
--ui-foreground-hover: color-mix(
in srgb,
var(--ui-foreground),
transparent 16%
);
--ui-illustration-stripe: color-mix(
in srgb,
var(--ui-foreground),
transparent 88%
);
--ui-foreground-inverse-muted: color-mix(
in srgb,
var(--ui-foreground-inverse),
transparent 40%
);
--ui-background-elevated: var(--color-neutral-900);
--ui-border-elevated: var(--color-neutral-800);
--ui-background-inverse-hover: var(--color-neutral-300);
--ui-background-hover: color-mix(
in srgb,
var(--ui-foreground),
var(--ui-background) 90%
);
--ui-destructive: var(--color-red-300);
--ui-destructive-background: color-mix(
in srgb,
var(--color-red-500),
transparent 90%
);
--ui-destructive-border: color-mix(
in srgb,
var(--color-red-500),
transparent 80%
);
--ui-destructive-background-hover: color-mix(
in srgb,
var(--color-red-500),
transparent 80%
);
--ui-ring: var(--color-blue-400);
--ui-template: var(--color-purple-400);
--ui-template-muted: color-mix(
in srgb,
var(--ui-template),
transparent 40%
);
}
}
================================================
FILE: applications/web/src/index.d.ts
================================================
declare module "lucide-react/dist/esm/icons/*" {
import type { LucideIcon } from "lucide-react";
const icon: LucideIcon;
export default icon;
}
================================================
FILE: applications/web/src/lib/analytics.ts
================================================
import type { PublicRuntimeConfig } from "./runtime-config";
const CONSENT_COOKIE = "keeper.analytics_consent";
const CONSENT_MAX_AGE = 60 * 60 * 24 * 182;
function resolveConsentState(granted: boolean): "granted" | "denied" {
if (granted) return "granted";
return "denied";
}
function readCookieSource(cookieHeader?: string): string {
if (cookieHeader !== undefined) return cookieHeader;
if (typeof document === "undefined") return "";
return document.cookie;
}
function readConsentValue(cookieHeader?: string): string | null {
const source = readCookieSource(cookieHeader);
const prefix = `${CONSENT_COOKIE}=`;
const match = source
.split(";")
.map((cookie) => cookie.trim())
.find((cookie) => cookie.startsWith(prefix));
if (!match) return null;
return match.slice(prefix.length);
}
const updateGoogleConsent = (granted: boolean): void => {
const state = resolveConsentState(granted);
globalThis.gtag?.("consent", "update", {
ad_personalization: state,
ad_storage: state,
ad_user_data: state,
analytics_storage: state,
});
};
const hasAnalyticsConsent = (cookieHeader?: string): boolean =>
readConsentValue(cookieHeader) === "granted";
const hasConsentChoice = (cookieHeader?: string): boolean => {
const value = readConsentValue(cookieHeader);
return value === "granted" || value === "denied";
};
function resolveEffectiveConsent(gdprApplies: boolean, cookieHeader?: string): boolean {
const value = readConsentValue(cookieHeader);
if (value === "granted") return true;
if (value === "denied") return false;
return !gdprApplies;
}
const setAnalyticsConsent = (granted: boolean): void => {
const state = resolveConsentState(granted);
document.cookie = `${CONSENT_COOKIE}=${state}; path=/; max-age=${CONSENT_MAX_AGE}; samesite=lax`;
updateGoogleConsent(granted);
globalThis.dispatchEvent(new StorageEvent("storage", { key: CONSENT_COOKIE }));
};
export const ANALYTICS_EVENTS = {
signup_completed: "signup_completed",
login_completed: "login_completed",
password_reset_requested: "password_reset_requested",
password_reset_completed: "password_reset_completed",
logout: "logout",
calendar_connect_started: "calendar_connect_started",
calendar_renamed: "calendar_renamed",
destination_toggled: "destination_toggled",
calendar_setting_toggled: "calendar_setting_toggled",
calendar_account_deleted: "calendar_account_deleted",
setup_step_completed: "setup_step_completed",
setup_skipped: "setup_skipped",
setup_completed: "setup_completed",
password_changed: "password_changed",
passkey_created: "passkey_created",
passkey_deleted: "passkey_deleted",
api_token_created: "api_token_created",
api_token_deleted: "api_token_deleted",
analytics_consent_changed: "analytics_consent_changed",
account_deleted: "account_deleted",
upgrade_billing_toggled: "upgrade_billing_toggled",
upgrade_started: "upgrade_started",
plan_managed: "plan_managed",
feedback_submitted: "feedback_submitted",
report_submitted: "report_submitted",
ical_link_copied: "ical_link_copied",
ical_setting_toggled: "ical_setting_toggled",
ical_source_toggled: "ical_source_toggled",
oauth_consent_granted: "oauth_consent_granted",
oauth_consent_denied: "oauth_consent_denied",
marketing_cta_clicked: "marketing_cta_clicked",
} satisfies Record;
type EventProperties = Record;
const track = (event: string, properties?: EventProperties): void => {
globalThis.visitors?.track(event, properties);
};
interface IdentifyProps {
id: string;
email?: string;
name?: string;
}
const identify = (
user: IdentifyProps,
options: { gdprApplies: boolean },
): void => {
if (options.gdprApplies && !hasAnalyticsConsent()) return;
globalThis.visitors?.identify(user);
};
interface ConversionOptions {
value: number | null;
currency: string | null;
transactionId: string | null;
}
const reportPurchaseConversion = (
runtimeConfig: PublicRuntimeConfig,
options?: ConversionOptions,
): void => {
const { googleAdsConversionLabel, googleAdsId } = runtimeConfig;
if (!googleAdsId || !googleAdsConversionLabel) return;
globalThis.gtag?.("event", "conversion", {
send_to: `${googleAdsId}/${googleAdsConversionLabel}`,
...options,
});
};
declare global {
var visitors:
| {
identify: (props: IdentifyProps) => void;
track: (event: string, properties?: EventProperties) => void;
}
| undefined;
var gtag: ((...args: unknown[]) => void) | undefined;
}
export {
hasAnalyticsConsent,
hasConsentChoice,
identify,
reportPurchaseConversion,
resolveEffectiveConsent,
setAnalyticsConsent,
track,
};
================================================
FILE: applications/web/src/lib/auth-capabilities.ts
================================================
import { authCapabilitiesSchema } from "@keeper.sh/data-schemas";
import type { AuthCapabilities } from "@keeper.sh/data-schemas";
import type { AppJsonFetcher } from "./router-context";
type SocialProviderId = keyof AuthCapabilities["socialProviders"];
interface CredentialField {
autoComplete: string;
id: string;
label: string;
name: string;
placeholder: string;
type: "email" | "text";
}
const resolveCredentialField = (
capabilities: AuthCapabilities,
): CredentialField => {
if (capabilities.credentialMode === "username") {
return {
autoComplete: "username",
id: "username",
label: "Username",
name: "username",
placeholder: "johndoe",
type: "text",
};
}
return {
autoComplete: "email",
id: "email",
label: "Email",
name: "email",
placeholder: "johndoe+keeper@example.com",
type: "email",
};
};
const getEnabledSocialProviders = (
capabilities: AuthCapabilities,
): SocialProviderId[] => {
/**
* TODO: Move this to providers, and have it based off
* the defined metadata.
* @param providerName
*/
const isValidProvider = (providerName: string): providerName is "google" | "microsoft" => {
if (providerName === "google") return true;
if (providerName === "microsoft") return true;
return false;
}
const providers: SocialProviderId[] = [];
for (const [provider, enabled] of Object.entries(capabilities.socialProviders)) {
if (!isValidProvider(provider) || !enabled) continue;
providers.push(provider)
}
return providers;
}
const supportsPasskeys = (capabilities: AuthCapabilities): boolean =>
capabilities.supportsPasskeys;
const fetchAuthCapabilitiesWithApi = async (
fetchApi: AppJsonFetcher,
): Promise => {
const data = await fetchApi("/api/auth/capabilities");
return authCapabilitiesSchema.assert(data);
};
export {
fetchAuthCapabilitiesWithApi,
getEnabledSocialProviders,
resolveCredentialField,
supportsPasskeys,
};
export type { AuthCapabilities, CredentialField, SocialProviderId };
================================================
FILE: applications/web/src/lib/auth-client.ts
================================================
import { createAuthClient } from "better-auth/react";
import { passkeyClient } from "@better-auth/passkey/client";
export const authClient = createAuthClient({
fetchOptions: { credentials: "include" },
plugins: [passkeyClient()],
});
================================================
FILE: applications/web/src/lib/auth.ts
================================================
import { authClient } from "./auth-client";
import type { AuthCapabilities } from "./auth-capabilities";
async function authPost(url: string, body: Record = {}): Promise {
const response = await fetch(url, {
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
method: "POST",
credentials: "include",
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.message ?? "Request failed");
}
}
async function authJsonPost(url: string, body: Record): Promise {
const response = await fetch(url, {
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
method: "POST",
credentials: "include",
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
const message =
typeof data === "object" && data !== null && "message" in data && typeof data.message === "string"
? data.message
: typeof data === "object" && data !== null && "error" in data && typeof data.error === "string"
? data.error
: "Request failed";
throw new Error(message);
}
}
export const signInWithCredential = async (
credential: string,
password: string,
capabilities: AuthCapabilities,
): Promise => {
if (capabilities.credentialMode === "username") {
await authJsonPost("/api/auth/username-only/sign-in", {
password,
username: credential,
});
return;
}
const { error } = await authClient.signIn.email({ email: credential, password });
if (error) throw new Error(error.message ?? "Sign in failed");
};
export const signUpWithCredential = async (
credential: string,
password: string,
capabilities: AuthCapabilities,
callbackURL = "/dashboard",
): Promise => {
if (capabilities.credentialMode === "username") {
await authJsonPost("/api/auth/username-only/sign-up", {
password,
username: credential,
});
return;
}
const { error } = await authClient.signUp.email({
callbackURL,
email: credential,
name: credential.split("@")[0] ?? credential,
password,
});
if (!error) return;
if (error.code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") {
throw new Error("This email is already registered. Please sign in instead.");
}
throw new Error(error.message ?? "Sign up failed");
};
export const signOut = () => authPost("/api/auth/sign-out");
export const forgotPassword = async (email: string): Promise => {
const { error } = await authClient.requestPasswordReset({
email,
redirectTo: "/reset-password",
});
if (error) throw new Error(error.message ?? "Failed to send reset email");
};
export const resetPassword = async (token: string, newPassword: string): Promise => {
const { error } = await authClient.resetPassword({ newPassword, token });
if (error) throw new Error(error.message ?? "Failed to reset password");
};
export const changePassword = (currentPassword: string, newPassword: string) =>
authPost("/api/auth/change-password", { currentPassword, newPassword });
export const deleteAccount = (password?: string) =>
authPost("/api/auth/delete-user", { ...(password && { password }) });
================================================
FILE: applications/web/src/lib/blog-posts.ts
================================================
// @ts-expect-error - virtual module provided by plugins/blog.ts
import { blogPosts as processedPosts } from "virtual:blog-posts";
export interface BlogPostMetadata {
blurb: string;
createdAt: string;
description: string;
slug?: string;
tags: string[];
title: string;
updatedAt: string;
}
export interface BlogPost {
content: string;
metadata: BlogPostMetadata;
slug: string;
}
export const blogPosts: BlogPost[] = processedPosts;
export function findBlogPostBySlug(slug: string): BlogPost | undefined {
return blogPosts.find((blogPost) => blogPost.slug === slug);
}
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
] as const;
export function formatIsoDate(isoDate: string): string {
const [yearPart, monthPart, dayPart] = isoDate.split("-");
const monthName = monthNames[Number(monthPart) - 1];
return `${monthName} ${Number(dayPart)}, ${yearPart}`;
}
================================================
FILE: applications/web/src/lib/fetcher.ts
================================================
export class HttpError extends Error {
constructor(
public readonly status: number,
public readonly url: string,
) {
super(`${status} ${url}`);
this.name = "HttpError";
}
}
export async function fetcher(url: string): Promise {
const response = await fetch(url, { credentials: "include" });
if (!response.ok) throw new HttpError(response.status, url);
return response.json();
}
export async function apiFetch(
url: string,
options: RequestInit,
): Promise {
const response = await fetch(url, { credentials: "include", ...options });
if (!response.ok) throw new HttpError(response.status, url);
return response;
}
================================================
FILE: applications/web/src/lib/mcp-auth-flow.ts
================================================
import { type } from "arktype";
type SearchParams = Record;
type StringSearchParams = Record;
const DEFAULT_POST_AUTH_PATH = "/dashboard";
const mcpAuthorizationSearchSchema = type({
client_id: "string > 0",
code_challenge: "string > 0",
code_challenge_method: "string > 0",
redirect_uri: "string > 0",
response_type: "string > 0",
scope: "string > 0",
state: "string > 0",
"+": "delete",
});
const resolvePathWithSearch = (
pathname: string,
search?: StringSearchParams,
): string => {
if (!search || Object.keys(search).length === 0) {
return pathname;
}
const url = new URL(pathname, "http://placeholder");
url.search = new URLSearchParams(search).toString();
return `${url.pathname}${url.search}`;
};
// SearchParams values may be non-string (e.g. from route search parsing),
// so we filter to only string entries before forwarding as query params.
const toStringSearchParams = (search: SearchParams): StringSearchParams =>
Object.fromEntries(
Object.entries(search).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
const getMcpAuthorizationSearch = (
search: SearchParams,
): StringSearchParams | null => {
const stringParams = toStringSearchParams(search);
const result = mcpAuthorizationSearchSchema(stringParams);
if (result instanceof type.errors) {
return null;
}
// Return the full string params (including optional OAuth keys like
// prompt, resource) rather than just the validated required subset.
return stringParams;
};
const resolveClientApiOrigin = (): string => {
const configuredApiOrigin = import.meta.env.VITE_API_URL;
if (typeof configuredApiOrigin === "string" && configuredApiOrigin.length > 0) {
return configuredApiOrigin;
}
if (typeof window !== "undefined") {
return window.location.origin;
}
throw new Error(
"Unable to resolve API origin: VITE_API_URL is not configured and window is undefined",
);
};
const resolvePostAuthRedirect = ({
apiOrigin,
defaultPath = DEFAULT_POST_AUTH_PATH,
search,
}: {
apiOrigin: string;
defaultPath?: string;
search: SearchParams;
}): string => {
const mcpAuthorizationSearch = getMcpAuthorizationSearch(search);
if (!mcpAuthorizationSearch) {
return defaultPath;
}
const authorizeUrl = new URL("/api/auth/oauth2/authorize", apiOrigin);
authorizeUrl.search = new URLSearchParams(mcpAuthorizationSearch).toString();
return authorizeUrl.toString();
};
const resolveClientPostAuthRedirect = (
search?: SearchParams | null,
defaultPath = DEFAULT_POST_AUTH_PATH,
): string => {
if (!search) {
return defaultPath;
}
const apiOrigin = resolveClientApiOrigin();
return resolvePostAuthRedirect({
apiOrigin,
defaultPath,
search,
});
};
export {
getMcpAuthorizationSearch,
resolvePathWithSearch,
resolveClientPostAuthRedirect,
resolvePostAuthRedirect,
toStringSearchParams,
};
export type { SearchParams, StringSearchParams };
================================================
FILE: applications/web/src/lib/motion-features.ts
================================================
export const loadMotionFeatures = () =>
import("motion/react").then((mod) => mod.domMin);
================================================
FILE: applications/web/src/lib/page-metadata.ts
================================================
const monthYearFormatter = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
timeZone: "UTC",
});
export function formatMonthYear(isoDate: string): string {
return monthYearFormatter.format(new Date(isoDate));
}
export interface PageMetadata {
path: string;
updatedAt: string;
}
export const privacyPageMetadata: PageMetadata = {
path: "/privacy",
updatedAt: "2025-12-01",
};
export const termsPageMetadata: PageMetadata = {
path: "/terms",
updatedAt: "2025-12-01",
};
================================================
FILE: applications/web/src/lib/pluralize.ts
================================================
export function pluralize(count: number, singular: string, plural: string = `${singular}s`): string {
if (count === 1) return `${count} ${singular}`;
return `${count.toLocaleString()} ${plural}`;
}
================================================
FILE: applications/web/src/lib/providers.ts
================================================
export const providerIcons: Record = {
google: "/integrations/icon-google-calendar.svg",
outlook: "/integrations/icon-outlook.svg",
icloud: "/integrations/icon-icloud.svg",
fastmail: "/integrations/icon-fastmail.svg",
"microsoft-365": "/integrations/icon-microsoft-365.svg",
nextcloud: "/integrations/icon-nextcloud.svg",
radicale: "/integrations/icon-radicale.svg",
};
================================================
FILE: applications/web/src/lib/route-access-guards.ts
================================================
export type RedirectTarget = "/dashboard" | "/login";
export type SubscriptionPlan = "free" | "pro";
export const resolveDashboardRedirect = (
hasSession: boolean,
): RedirectTarget | null => {
if (!hasSession) {
return "/login";
}
return null;
};
export const resolveAuthRedirect = (
hasSession: boolean,
): RedirectTarget | null => {
if (hasSession) {
return "/dashboard";
}
return null;
};
export const resolveUpgradeRedirect = (
hasSession: boolean,
plan: SubscriptionPlan | null,
): RedirectTarget | null => {
if (!hasSession) {
return "/login";
}
if (plan === "pro") {
return "/dashboard";
}
return null;
};
================================================
FILE: applications/web/src/lib/router-context.ts
================================================
import type { PublicRuntimeConfig } from "./runtime-config";
export interface AppAuthContext {
hasSession: () => boolean;
}
export type AppJsonFetcher = (path: string, init?: RequestInit) => Promise;
export interface ViteScript {
src?: string;
content?: string;
}
export interface ViteAssets {
stylesheets: string[];
inlineStyles: string[];
modulePreloads: string[];
headScripts: ViteScript[];
bodyScripts: ViteScript[];
}
export interface AppRouterContext {
auth: AppAuthContext;
fetchApi: AppJsonFetcher;
fetchWeb: AppJsonFetcher;
runtimeConfig: PublicRuntimeConfig;
viteAssets: ViteAssets | null;
}
================================================
FILE: applications/web/src/lib/runtime-config.ts
================================================
import { GDPR_COUNTRIES } from "@/config/gdpr";
interface PublicRuntimeConfig {
commercialMode: boolean;
gdprApplies: boolean;
googleAdsConversionLabel: string | null;
googleAdsId: string | null;
polarProMonthlyProductId: string | null;
polarProYearlyProductId: string | null;
visitorsNowToken: string | null;
}
interface RuntimeConfigSource {
commercialMode?: boolean | null;
gdprApplies?: boolean | null;
googleAdsConversionLabel?: string | null;
googleAdsId?: string | null;
polarProMonthlyProductId?: string | null;
polarProYearlyProductId?: string | null;
visitorsNowToken?: string | null;
}
const normalizeOptionalValue = (value: string | null | undefined): string | null => {
if (typeof value !== "string") {
return null;
}
return value.length > 0 ? value : null;
};
interface ServerRuntimeConfigOptions {
environment: Record;
countryCode?: string | null;
}
const getServerPublicRuntimeConfig = (
options: ServerRuntimeConfigOptions,
): PublicRuntimeConfig => {
const { environment, countryCode } = options;
return {
commercialMode: environment.COMMERCIAL_MODE === "true",
gdprApplies: environment.ENV === "development" || (typeof countryCode === "string" && GDPR_COUNTRIES.has(countryCode)),
googleAdsConversionLabel: normalizeOptionalValue(
environment.VITE_GOOGLE_ADS_CONVERSION_LABEL,
),
googleAdsId: normalizeOptionalValue(environment.VITE_GOOGLE_ADS_ID),
polarProMonthlyProductId: normalizeOptionalValue(environment.POLAR_PRO_MONTHLY_PRODUCT_ID),
polarProYearlyProductId: normalizeOptionalValue(environment.POLAR_PRO_YEARLY_PRODUCT_ID),
visitorsNowToken: normalizeOptionalValue(environment.VITE_VISITORS_NOW_TOKEN),
};
};
const getWindowPublicRuntimeConfig = (): RuntimeConfigSource => {
if (typeof window === "undefined") {
return {};
}
return window.__KEEPER_RUNTIME_CONFIG__ ?? {};
};
const resolvePublicRuntimeConfig = (source: RuntimeConfigSource): PublicRuntimeConfig => ({
commercialMode: source.commercialMode === true,
gdprApplies: source.gdprApplies === true,
googleAdsConversionLabel: normalizeOptionalValue(source.googleAdsConversionLabel),
googleAdsId: normalizeOptionalValue(source.googleAdsId),
polarProMonthlyProductId: normalizeOptionalValue(source.polarProMonthlyProductId),
polarProYearlyProductId: normalizeOptionalValue(source.polarProYearlyProductId),
visitorsNowToken: normalizeOptionalValue(source.visitorsNowToken),
});
const getPublicRuntimeConfig = (): PublicRuntimeConfig => resolvePublicRuntimeConfig(
getWindowPublicRuntimeConfig(),
);
const serializePublicRuntimeConfig = (config: PublicRuntimeConfig): string =>
JSON.stringify(config)
.replace(//g, "\\u003E")
.replace(/&/g, "\\u0026");
declare global {
interface Window {
__KEEPER_RUNTIME_CONFIG__?: RuntimeConfigSource;
}
}
export {
getPublicRuntimeConfig,
getServerPublicRuntimeConfig,
resolvePublicRuntimeConfig,
serializePublicRuntimeConfig,
};
export type { PublicRuntimeConfig, RuntimeConfigSource };
================================================
FILE: applications/web/src/lib/seo.ts
================================================
const SITE_URL = "https://keeper.sh";
const SITE_NAME = "Keeper.sh";
export function canonicalUrl(path: string): string {
return `${SITE_URL}${path}`;
}
export function jsonLdScript(data: Record) {
return { type: "application/ld+json", children: JSON.stringify(data) };
}
export function seoMeta({
title,
description,
path,
type = "website",
brandPosition = "after",
}: {
title: string;
description: string;
path: string;
type?: string;
brandPosition?: "before" | "after";
}) {
const fullTitle = brandPosition === "before"
? `${SITE_NAME} — ${title}`
: `${title} · ${SITE_NAME}`;
return [
{ title: fullTitle },
{ content: description, name: "description" },
{ content: type, property: "og:type" },
{ content: title, property: "og:title" },
{ content: description, property: "og:description" },
{ content: canonicalUrl(path), property: "og:url" },
{ content: SITE_NAME, property: "og:site_name" },
{ content: `${SITE_URL}/open-graph.png`, property: "og:image" },
{ content: "1200", property: "og:image:width" },
{ content: "630", property: "og:image:height" },
{ content: "summary_large_image", name: "twitter:card" },
{ content: title, name: "twitter:title" },
{ content: description, name: "twitter:description" },
{ content: `${SITE_URL}/open-graph.png`, name: "twitter:image" },
];
}
export const organizationSchema = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": `${SITE_URL}/#organization`,
name: SITE_NAME,
url: SITE_URL,
logo: {
"@type": "ImageObject",
url: `${SITE_URL}/512x512-on-light.png`,
width: 512,
height: 512,
},
sameAs: ["https://github.com/ridafkih/keeper.sh"],
},
{
"@type": "WebSite",
"@id": `${SITE_URL}/#website`,
name: SITE_NAME,
url: SITE_URL,
publisher: { "@id": `${SITE_URL}/#organization` },
},
],
};
export function breadcrumbSchema(
items: Array<{ name: string; path: string }>,
) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: canonicalUrl(item.path),
})),
};
}
export function webPageSchema(name: string, description: string, path: string) {
return {
"@context": "https://schema.org",
"@type": "WebPage",
"@id": `${canonicalUrl(path)}/#webpage`,
name,
description,
url: canonicalUrl(path),
isPartOf: { "@id": `${SITE_URL}/#website` },
publisher: { "@id": `${SITE_URL}/#organization` },
};
}
export function softwareApplicationSchema() {
return {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"@id": `${SITE_URL}/#software`,
name: SITE_NAME,
description:
"Open-source calendar event syncing tool. Synchronize events between your personal, work, business and school calendars.",
url: SITE_URL,
image: `${SITE_URL}/open-graph.png`,
applicationCategory: "BusinessApplication",
operatingSystem: "Web",
offers: [
{
"@type": "Offer",
name: "Free",
price: "0",
priceCurrency: "USD",
description:
"For users that just want to get basic calendar syncing up and running.",
},
{
"@type": "Offer",
name: "Pro",
price: "5.00",
priceCurrency: "USD",
priceSpecification: {
"@type": "UnitPriceSpecification",
price: "5.00",
priceCurrency: "USD",
billingDuration: "P1M",
},
description:
"For power users who want minutely syncs and unlimited calendars.",
},
],
provider: { "@id": `${SITE_URL}/#organization` },
};
}
export function collectionPageSchema(posts: Array<{ slug: string; metadata: { title: string } }>) {
return {
"@context": "https://schema.org",
"@type": "CollectionPage",
"@id": `${SITE_URL}/blog/#collectionpage`,
name: "Blog",
url: canonicalUrl("/blog"),
isPartOf: { "@id": `${SITE_URL}/#website` },
mainEntity: {
"@type": "ItemList",
itemListElement: posts.map((post, index) => ({
"@type": "ListItem",
position: index + 1,
url: canonicalUrl(`/blog/${post.slug}`),
name: post.metadata.title,
})),
},
};
}
export const authorPersonSchema = {
"@type": "Person",
"@id": `${SITE_URL}/#author`,
name: "Rida F'kih",
url: "https://rida.dev",
sameAs: ["https://github.com/ridafkih"],
};
export function blogPostingSchema(post: {
title: string;
description: string;
slug: string;
createdAt: string;
updatedAt: string;
tags: string[];
}) {
const url = canonicalUrl(`/blog/${post.slug}`);
return {
"@context": "https://schema.org",
"@type": "BlogPosting",
"@id": `${url}/#blogposting`,
headline: post.title,
description: post.description,
image: `${SITE_URL}/open-graph.png`,
url,
datePublished: post.createdAt,
dateModified: post.updatedAt,
keywords: post.tags,
author: authorPersonSchema,
publisher: { "@id": `${SITE_URL}/#organization` },
isPartOf: { "@id": `${SITE_URL}/#website` },
mainEntityOfPage: { "@type": "WebPage", "@id": url },
};
}
================================================
FILE: applications/web/src/lib/serialized-mutate.ts
================================================
type ErrorHandler = (error: unknown) => void;
interface QueuedPatch {
patch: Record;
flush: (mergedPatch: Record) => Promise;
onError: ErrorHandler;
}
const activePatchMutations = new Set();
const patchQueue = new Map();
function runFlush(
promise: Promise,
onError: ErrorHandler,
cleanup: () => void,
) {
promise
.catch(onError)
.finally(cleanup);
}
function drainPatchQueue(key: string) {
const pending = patchQueue.get(key);
if (!pending) return;
patchQueue.delete(key);
activePatchMutations.add(key);
runFlush(
pending.flush({ ...pending.patch }),
pending.onError,
() => {
activePatchMutations.delete(key);
drainPatchQueue(key);
},
);
}
export function serializedPatch(
key: string,
patch: Record,
flush: (mergedPatch: Record) => Promise,
onError?: ErrorHandler,
) {
const handleError = onError ?? defaultOnError;
if (activePatchMutations.has(key)) {
const existing = patchQueue.get(key);
if (existing) {
Object.assign(existing.patch, patch);
existing.flush = flush;
existing.onError = handleError;
return;
}
patchQueue.set(key, { patch: { ...patch }, flush, onError: handleError });
return;
}
activePatchMutations.add(key);
runFlush(
flush(patch),
handleError,
() => {
activePatchMutations.delete(key);
drainPatchQueue(key);
},
);
}
interface QueuedCall {
callback: () => Promise;
onError: ErrorHandler;
}
const activeCallMutations = new Set();
const callQueue = new Map();
function drainCallQueue(key: string) {
const pending = callQueue.get(key);
if (!pending) return;
callQueue.delete(key);
activeCallMutations.add(key);
runFlush(
pending.callback(),
pending.onError,
() => {
activeCallMutations.delete(key);
drainCallQueue(key);
},
);
}
export function serializedCall(
key: string,
callback: () => Promise,
onError?: ErrorHandler,
) {
const handleError = onError ?? defaultOnError;
if (activeCallMutations.has(key)) {
callQueue.set(key, { callback, onError: handleError });
return;
}
activeCallMutations.add(key);
runFlush(
callback(),
handleError,
() => {
activeCallMutations.delete(key);
drainCallQueue(key);
},
);
}
function defaultOnError() {}
================================================
FILE: applications/web/src/lib/session-cookie.ts
================================================
const SESSION_COOKIE = "keeper.has_session=1";
export function hasSessionCookie(cookieHeader?: string): boolean {
const cookieSource = cookieHeader
?? (typeof document === "undefined" ? "" : document.cookie);
return cookieSource
.split(";")
.map((cookie) => cookie.trim())
.some((cookie) => cookie.startsWith(SESSION_COOKIE));
}
================================================
FILE: applications/web/src/lib/swr.ts
================================================
import type { ScopedMutator } from "swr";
/**
* Revalidate all account and source caches.
* Use after creating, updating, or deleting accounts/sources.
*/
export function invalidateAccountsAndSources(
globalMutate: ScopedMutator,
...additionalKeys: string[]
) {
return Promise.all([
globalMutate("/api/accounts"),
globalMutate("/api/sources"),
...additionalKeys.map((key) => globalMutate(key)),
]);
}
================================================
FILE: applications/web/src/lib/time.ts
================================================
const MINS_PER_HOUR = 60;
const HOURS_PER_DAY = 24;
const MINS_PER_DAY = MINS_PER_HOUR * HOURS_PER_DAY;
const getSuffixAndPrefix = (
diffMins: number,
): { suffix: string; prefix: string } => {
if (diffMins < 0) return { suffix: " ago", prefix: "" };
if (diffMins > 0) return { suffix: "", prefix: "in " };
return { suffix: "", prefix: "" };
};
export const formatTimeUntil = (date: Date): string => {
const now = new Date();
const diffMs = date.getTime() - now.getTime();
const diffMins = Math.round(diffMs / (1000 * 60));
if (diffMins === 0) return "now";
const absMins = Math.abs(diffMins);
const { suffix, prefix } = getSuffixAndPrefix(diffMins);
if (absMins < MINS_PER_HOUR) return `${prefix}${absMins}m${suffix}`;
const hours = Math.floor(absMins / MINS_PER_HOUR);
if (hours < 48) return `${prefix}${hours}h${suffix}`;
const days = Math.floor(absMins / MINS_PER_DAY);
return `${prefix}${days}d${suffix}`;
};
export const isEventPast = (endTime: Date): boolean =>
endTime.getTime() < Date.now();
const resolvePeriod = (hours: number): string => {
if (hours >= 12) return "PM";
return "AM";
};
export const formatTime = (date: Date): string => {
const hours = date.getHours();
const minutes = date.getMinutes();
const period = resolvePeriod(hours);
const displayHours = hours % 12 || 12;
return `${displayHours}:${minutes.toString().padStart(2, "0")} ${period}`;
};
export const formatDate = (date: string | Date): string =>
new Date(date).toLocaleDateString(undefined, {
month: "long",
day: "numeric",
year: "numeric",
});
export const formatDateShort = (date: string | Date): string =>
new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
export const formatDayHeader = (date: Date): string => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const diffDays = Math.round(
(target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Tomorrow";
if (diffDays === -1) return "Yesterday";
return date.toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
};
================================================
FILE: applications/web/src/main.tsx
================================================
import { StrictMode } from "react";
import { createRoot, hydrateRoot } from "react-dom/client";
import { RouterProvider } from "@tanstack/react-router";
import { RouterClient } from "@tanstack/react-router/ssr/client";
import { createAppRouter } from "./router";
const rootElement = document.getElementById("root");
const router = createAppRouter();
if (rootElement && rootElement.innerHTML.length > 0) {
hydrateRoot(
document,
,
);
} else if (rootElement) {
const root = createRoot(rootElement);
root.render(
,
);
}
================================================
FILE: applications/web/src/providers/sync-provider-logic.ts
================================================
import { isSocketMessage, isSyncAggregate } from "@keeper.sh/data-schemas/client";
import type { CompositeSyncState, SyncAggregateData } from "@/state/sync";
type IncomingSocketAction =
| { kind: "ignore" }
| { kind: "pong" }
| { kind: "reconnect" }
| { kind: "aggregate"; data: SyncAggregateData };
interface AggregateDecision {
accepted: boolean;
nextSeq: number;
}
const parseTimestampMs = (value: string | null | undefined): number | null => {
if (!value) {
return null;
}
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : null;
};
const isForwardProgress = (
current: CompositeSyncState,
next: SyncAggregateData,
): boolean => {
const currentLastSyncedAtMs = parseTimestampMs(current.lastSyncedAt);
const nextLastSyncedAtMs = parseTimestampMs(next.lastSyncedAt);
if (
nextLastSyncedAtMs !== null &&
(currentLastSyncedAtMs === null || nextLastSyncedAtMs > currentLastSyncedAtMs)
) {
return true;
}
if (next.syncEventsProcessed > current.syncEventsProcessed) {
return true;
}
if (next.syncEventsRemaining < current.syncEventsRemaining) {
return true;
}
if (next.progressPercent > current.progressPercent) {
return true;
}
if (current.state === "idle" && next.syncing) {
return true;
}
if (current.state === "syncing" && !next.syncing && next.syncEventsRemaining === 0) {
return true;
}
return false;
};
const parseIncomingSocketAction = (
raw: string,
): IncomingSocketAction => {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return { kind: "reconnect" };
}
if (!isSocketMessage(parsed)) {
return { kind: "reconnect" };
}
if (parsed.event === "ping") {
return { kind: "pong" };
}
if (parsed.event !== "sync:aggregate") {
return { kind: "ignore" };
}
if (!isSyncAggregate(parsed.data)) {
return { kind: "reconnect" };
}
return {
data: parsed.data,
kind: "aggregate",
};
};
const shouldAcceptAggregatePayload = (
currentState: CompositeSyncState,
lastSeq: number,
nextAggregate: SyncAggregateData,
): AggregateDecision => {
const isNewerSequence = nextAggregate.seq > lastSeq;
if (!isNewerSequence && !isForwardProgress(currentState, nextAggregate)) {
return {
accepted: false,
nextSeq: lastSeq,
};
}
return {
accepted: true,
nextSeq: Math.max(lastSeq, nextAggregate.seq),
};
};
export { parseIncomingSocketAction, shouldAcceptAggregatePayload };
export type { IncomingSocketAction, AggregateDecision };
================================================
FILE: applications/web/src/providers/sync-provider.tsx
================================================
import { useEffect } from "react";
import { useSetAtom } from "jotai";
import { syncStateAtom, type CompositeSyncState } from "@/state/sync";
import {
parseIncomingSocketAction,
shouldAcceptAggregatePayload,
} from "./sync-provider-logic";
interface ConnectionState {
socket: WebSocket | null;
abortController: AbortController;
reconnectTimer: ReturnType | null;
initialAggregateTimer: ReturnType | null;
attempts: number;
disposed: boolean;
hasReceivedSocketAggregate: boolean;
lastSeq: number;
currentState: CompositeSyncState;
}
const MAX_RECONNECT_DELAY_MS = 30_000;
const BASE_RECONNECT_DELAY_MS = 2_000;
const INITIAL_AGGREGATE_TIMEOUT_MS = 10_000;
const INITIAL_SYNC_STATE: CompositeSyncState = {
connected: false,
lastSyncedAt: null,
progressPercent: 100,
seq: 0,
syncEventsProcessed: 0,
syncEventsRemaining: 0,
syncEventsTotal: 0,
state: "idle",
hasReceivedAggregate: false,
};
const getWebSocketProtocol = (): string =>
globalThis.location.protocol === "https:" ? "wss:" : "ws:";
const buildWebSocketUrl = (socketPath: string): string => {
const url = new URL(socketPath, globalThis.location.origin);
url.protocol = getWebSocketProtocol();
return url.toString();
};
const fetchSocketUrl = async (signal: AbortSignal): Promise => {
const response = await fetch("/api/socket/url", { credentials: "include", signal });
if (!response.ok) {
throw new Error(`Failed to fetch socket URL: ${response.status}`);
}
const { socketUrl, socketPath } = await response.json();
return socketUrl ?? buildWebSocketUrl(socketPath);
};
const getReconnectDelay = (attempts: number): number =>
Math.min(BASE_RECONNECT_DELAY_MS * 2 ** attempts, MAX_RECONNECT_DELAY_MS);
const applyState = (
connectionState: ConnectionState,
setSyncState: (state: CompositeSyncState) => void,
state: CompositeSyncState,
): void => {
connectionState.currentState = state;
setSyncState(state);
};
const setConnected = (
connectionState: ConnectionState,
setSyncState: (state: CompositeSyncState) => void,
connected: boolean,
): void => {
applyState(connectionState, setSyncState, {
...connectionState.currentState,
connected,
});
};
const clearInitialAggregateTimer = (connectionState: ConnectionState): void => {
if (connectionState.initialAggregateTimer) {
clearTimeout(connectionState.initialAggregateTimer);
connectionState.initialAggregateTimer = null;
}
};
const startInitialAggregateTimer = (
connectionState: ConnectionState,
socket: WebSocket,
): void => {
clearInitialAggregateTimer(connectionState);
connectionState.initialAggregateTimer = setTimeout(() => {
if (connectionState.disposed || connectionState.socket !== socket) {
return;
}
if (connectionState.hasReceivedSocketAggregate) {
return;
}
socket.close();
}, INITIAL_AGGREGATE_TIMEOUT_MS);
};
const resolveProgressPercent = (data: { syncEventsRemaining: number; progressPercent: number }): number => {
if (data.syncEventsRemaining === 0) {
return 100;
}
return data.progressPercent;
};
const handleMessage = (
connectionState: ConnectionState,
setSyncState: (state: CompositeSyncState) => void,
raw: string,
socket: WebSocket,
): void => {
const action = parseIncomingSocketAction(raw);
if (action.kind === "ignore") {
return;
}
if (action.kind === "pong") {
socket.send(JSON.stringify({ event: "pong" }));
return;
}
if (action.kind === "reconnect") {
if (connectionState.socket === socket && socket.readyState === WebSocket.OPEN) {
socket.close();
}
return;
}
const decision = shouldAcceptAggregatePayload(
connectionState.currentState,
connectionState.lastSeq,
action.data,
);
if (!decision.accepted) {
return;
}
connectionState.hasReceivedSocketAggregate = true;
clearInitialAggregateTimer(connectionState);
connectionState.lastSeq = decision.nextSeq;
applyState(connectionState, setSyncState, {
connected: true,
hasReceivedAggregate: true,
lastSyncedAt: action.data.lastSyncedAt ?? null,
progressPercent: resolveProgressPercent(action.data),
seq: action.data.seq,
syncEventsProcessed: action.data.syncEventsProcessed,
syncEventsRemaining: action.data.syncEventsRemaining,
syncEventsTotal: action.data.syncEventsTotal,
state: action.data.syncing ? "syncing" : "idle",
});
};
const scheduleReconnect = (connectionState: ConnectionState, connectFn: () => void): void => {
if (connectionState.disposed) {
return;
}
const delay = getReconnectDelay(connectionState.attempts);
connectionState.attempts += 1;
connectionState.reconnectTimer = setTimeout(connectFn, delay);
};
const connect = async (
connectionState: ConnectionState,
setSyncState: (state: CompositeSyncState) => void,
): Promise => {
if (connectionState.disposed) {
return;
}
connectionState.abortController = new AbortController();
try {
const socketUrl = await fetchSocketUrl(connectionState.abortController.signal);
if (connectionState.disposed) {
return;
}
const socket = new WebSocket(socketUrl);
connectionState.socket = socket;
socket.addEventListener("open", () => {
connectionState.attempts = 0;
connectionState.lastSeq = -1;
connectionState.hasReceivedSocketAggregate = false;
setConnected(connectionState, setSyncState, false);
startInitialAggregateTimer(connectionState, socket);
});
socket.addEventListener("message", (event) => {
handleMessage(connectionState, setSyncState, String(event.data), socket);
});
socket.addEventListener("close", () => {
clearInitialAggregateTimer(connectionState);
connectionState.hasReceivedSocketAggregate = false;
setConnected(connectionState, setSyncState, false);
scheduleReconnect(connectionState, () => {
void connect(connectionState, setSyncState);
});
});
socket.addEventListener("error", () => {
socket.close();
});
} catch {
setConnected(connectionState, setSyncState, false);
scheduleReconnect(connectionState, () => {
void connect(connectionState, setSyncState);
});
}
};
const dispose = (connectionState: ConnectionState): void => {
connectionState.disposed = true;
connectionState.abortController.abort();
clearInitialAggregateTimer(connectionState);
if (connectionState.reconnectTimer) {
clearTimeout(connectionState.reconnectTimer);
}
connectionState.socket?.close();
};
export function SyncProvider() {
const setSyncState = useSetAtom(syncStateAtom);
useEffect(() => {
const connectionState: ConnectionState = {
abortController: new AbortController(),
attempts: 0,
currentState: INITIAL_SYNC_STATE,
disposed: false,
hasReceivedSocketAggregate: false,
initialAggregateTimer: null,
lastSeq: -1,
reconnectTimer: null,
socket: null,
};
void connect(connectionState, setSyncState);
return () => {
dispose(connectionState);
};
}, [setSyncState]);
return null;
}
================================================
FILE: applications/web/src/routeTree.gen.ts
================================================
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as oauthRouteRouteImport } from './routes/(oauth)/route'
import { Route as marketingRouteRouteImport } from './routes/(marketing)/route'
import { Route as dashboardRouteRouteImport } from './routes/(dashboard)/route'
import { Route as authRouteRouteImport } from './routes/(auth)/route'
import { Route as marketingIndexRouteImport } from './routes/(marketing)/index'
import { Route as marketingTermsRouteImport } from './routes/(marketing)/terms'
import { Route as marketingPrivacyRouteImport } from './routes/(marketing)/privacy'
import { Route as authVerifyEmailRouteImport } from './routes/(auth)/verify-email'
import { Route as authVerifyAuthenticationRouteImport } from './routes/(auth)/verify-authentication'
import { Route as authResetPasswordRouteImport } from './routes/(auth)/reset-password'
import { Route as authRegisterRouteImport } from './routes/(auth)/register'
import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as authForgotPasswordRouteImport } from './routes/(auth)/forgot-password'
import { Route as oauthDashboardRouteRouteImport } from './routes/(oauth)/dashboard/route'
import { Route as oauthAuthRouteRouteImport } from './routes/(oauth)/auth/route'
import { Route as marketingBlogRouteRouteImport } from './routes/(marketing)/blog/route'
import { Route as marketingBlogIndexRouteImport } from './routes/(marketing)/blog/index'
import { Route as dashboardDashboardIndexRouteImport } from './routes/(dashboard)/dashboard/index'
import { Route as oauthOauthConsentRouteImport } from './routes/(oauth)/oauth/consent'
import { Route as oauthAuthOutlookRouteImport } from './routes/(oauth)/auth/outlook'
import { Route as oauthAuthGoogleRouteImport } from './routes/(oauth)/auth/google'
import { Route as marketingBlogSlugRouteImport } from './routes/(marketing)/blog/$slug'
import { Route as dashboardDashboardReportRouteImport } from './routes/(dashboard)/dashboard/report'
import { Route as dashboardDashboardIcalRouteImport } from './routes/(dashboard)/dashboard/ical'
import { Route as dashboardDashboardFeedbackRouteImport } from './routes/(dashboard)/dashboard/feedback'
import { Route as oauthDashboardConnectRouteRouteImport } from './routes/(oauth)/dashboard/connect/route'
import { Route as dashboardDashboardSettingsRouteRouteImport } from './routes/(dashboard)/dashboard/settings/route'
import { Route as dashboardDashboardConnectRouteRouteImport } from './routes/(dashboard)/dashboard/connect/route'
import { Route as dashboardDashboardAccountsRouteRouteImport } from './routes/(dashboard)/dashboard/accounts/route'
import { Route as dashboardDashboardUpgradeIndexRouteImport } from './routes/(dashboard)/dashboard/upgrade/index'
import { Route as dashboardDashboardSettingsIndexRouteImport } from './routes/(dashboard)/dashboard/settings/index'
import { Route as dashboardDashboardIntegrationsIndexRouteImport } from './routes/(dashboard)/dashboard/integrations/index'
import { Route as dashboardDashboardEventsIndexRouteImport } from './routes/(dashboard)/dashboard/events/index'
import { Route as dashboardDashboardConnectIndexRouteImport } from './routes/(dashboard)/dashboard/connect/index'
import { Route as oauthDashboardConnectOutlookRouteImport } from './routes/(oauth)/dashboard/connect/outlook'
import { Route as oauthDashboardConnectMicrosoftRouteImport } from './routes/(oauth)/dashboard/connect/microsoft'
import { Route as oauthDashboardConnectIcsFileRouteImport } from './routes/(oauth)/dashboard/connect/ics-file'
import { Route as oauthDashboardConnectIcalLinkRouteImport } from './routes/(oauth)/dashboard/connect/ical-link'
import { Route as oauthDashboardConnectGoogleRouteImport } from './routes/(oauth)/dashboard/connect/google'
import { Route as oauthDashboardConnectFastmailRouteImport } from './routes/(oauth)/dashboard/connect/fastmail'
import { Route as oauthDashboardConnectCaldavRouteImport } from './routes/(oauth)/dashboard/connect/caldav'
import { Route as oauthDashboardConnectAppleRouteImport } from './routes/(oauth)/dashboard/connect/apple'
import { Route as dashboardDashboardSettingsPasskeysRouteImport } from './routes/(dashboard)/dashboard/settings/passkeys'
import { Route as dashboardDashboardSettingsChangePasswordRouteImport } from './routes/(dashboard)/dashboard/settings/change-password'
import { Route as dashboardDashboardSettingsApiTokensRouteImport } from './routes/(dashboard)/dashboard/settings/api-tokens'
import { Route as dashboardDashboardAccountsAccountIdIndexRouteImport } from './routes/(dashboard)/dashboard/accounts/$accountId.index'
import { Route as dashboardDashboardAccountsAccountIdSetupRouteImport } from './routes/(dashboard)/dashboard/accounts/$accountId.setup'
import { Route as dashboardDashboardAccountsAccountIdCalendarIdRouteImport } from './routes/(dashboard)/dashboard/accounts/$accountId.$calendarId'
const oauthRouteRoute = oauthRouteRouteImport.update({
id: '/(oauth)',
getParentRoute: () => rootRouteImport,
} as any)
const marketingRouteRoute = marketingRouteRouteImport.update({
id: '/(marketing)',
getParentRoute: () => rootRouteImport,
} as any)
const dashboardRouteRoute = dashboardRouteRouteImport.update({
id: '/(dashboard)',
getParentRoute: () => rootRouteImport,
} as any)
const authRouteRoute = authRouteRouteImport.update({
id: '/(auth)',
getParentRoute: () => rootRouteImport,
} as any)
const marketingIndexRoute = marketingIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => marketingRouteRoute,
} as any)
const marketingTermsRoute = marketingTermsRouteImport.update({
id: '/terms',
path: '/terms',
getParentRoute: () => marketingRouteRoute,
} as any)
const marketingPrivacyRoute = marketingPrivacyRouteImport.update({
id: '/privacy',
path: '/privacy',
getParentRoute: () => marketingRouteRoute,
} as any)
const authVerifyEmailRoute = authVerifyEmailRouteImport.update({
id: '/verify-email',
path: '/verify-email',
getParentRoute: () => authRouteRoute,
} as any)
const authVerifyAuthenticationRoute =
authVerifyAuthenticationRouteImport.update({
id: '/verify-authentication',
path: '/verify-authentication',
getParentRoute: () => authRouteRoute,
} as any)
const authResetPasswordRoute = authResetPasswordRouteImport.update({
id: '/reset-password',
path: '/reset-password',
getParentRoute: () => authRouteRoute,
} as any)
const authRegisterRoute = authRegisterRouteImport.update({
id: '/register',
path: '/register',
getParentRoute: () => authRouteRoute,
} as any)
const authLoginRoute = authLoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => authRouteRoute,
} as any)
const authForgotPasswordRoute = authForgotPasswordRouteImport.update({
id: '/forgot-password',
path: '/forgot-password',
getParentRoute: () => authRouteRoute,
} as any)
const oauthDashboardRouteRoute = oauthDashboardRouteRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => oauthRouteRoute,
} as any)
const oauthAuthRouteRoute = oauthAuthRouteRouteImport.update({
id: '/auth',
path: '/auth',
getParentRoute: () => oauthRouteRoute,
} as any)
const marketingBlogRouteRoute = marketingBlogRouteRouteImport.update({
id: '/blog',
path: '/blog',
getParentRoute: () => marketingRouteRoute,
} as any)
const marketingBlogIndexRoute = marketingBlogIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => marketingBlogRouteRoute,
} as any)
const dashboardDashboardIndexRoute = dashboardDashboardIndexRouteImport.update({
id: '/dashboard/',
path: '/dashboard/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const oauthOauthConsentRoute = oauthOauthConsentRouteImport.update({
id: '/oauth/consent',
path: '/oauth/consent',
getParentRoute: () => oauthRouteRoute,
} as any)
const oauthAuthOutlookRoute = oauthAuthOutlookRouteImport.update({
id: '/outlook',
path: '/outlook',
getParentRoute: () => oauthAuthRouteRoute,
} as any)
const oauthAuthGoogleRoute = oauthAuthGoogleRouteImport.update({
id: '/google',
path: '/google',
getParentRoute: () => oauthAuthRouteRoute,
} as any)
const marketingBlogSlugRoute = marketingBlogSlugRouteImport.update({
id: '/$slug',
path: '/$slug',
getParentRoute: () => marketingBlogRouteRoute,
} as any)
const dashboardDashboardReportRoute =
dashboardDashboardReportRouteImport.update({
id: '/dashboard/report',
path: '/dashboard/report',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardIcalRoute = dashboardDashboardIcalRouteImport.update({
id: '/dashboard/ical',
path: '/dashboard/ical',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardFeedbackRoute =
dashboardDashboardFeedbackRouteImport.update({
id: '/dashboard/feedback',
path: '/dashboard/feedback',
getParentRoute: () => dashboardRouteRoute,
} as any)
const oauthDashboardConnectRouteRoute =
oauthDashboardConnectRouteRouteImport.update({
id: '/connect',
path: '/connect',
getParentRoute: () => oauthDashboardRouteRoute,
} as any)
const dashboardDashboardSettingsRouteRoute =
dashboardDashboardSettingsRouteRouteImport.update({
id: '/dashboard/settings',
path: '/dashboard/settings',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardConnectRouteRoute =
dashboardDashboardConnectRouteRouteImport.update({
id: '/dashboard/connect',
path: '/dashboard/connect',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardAccountsRouteRoute =
dashboardDashboardAccountsRouteRouteImport.update({
id: '/dashboard/accounts',
path: '/dashboard/accounts',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardUpgradeIndexRoute =
dashboardDashboardUpgradeIndexRouteImport.update({
id: '/dashboard/upgrade/',
path: '/dashboard/upgrade/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardSettingsIndexRoute =
dashboardDashboardSettingsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardIntegrationsIndexRoute =
dashboardDashboardIntegrationsIndexRouteImport.update({
id: '/dashboard/integrations/',
path: '/dashboard/integrations/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardEventsIndexRoute =
dashboardDashboardEventsIndexRouteImport.update({
id: '/dashboard/events/',
path: '/dashboard/events/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardDashboardConnectIndexRoute =
dashboardDashboardConnectIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => dashboardDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectOutlookRoute =
oauthDashboardConnectOutlookRouteImport.update({
id: '/outlook',
path: '/outlook',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectMicrosoftRoute =
oauthDashboardConnectMicrosoftRouteImport.update({
id: '/microsoft',
path: '/microsoft',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectIcsFileRoute =
oauthDashboardConnectIcsFileRouteImport.update({
id: '/ics-file',
path: '/ics-file',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectIcalLinkRoute =
oauthDashboardConnectIcalLinkRouteImport.update({
id: '/ical-link',
path: '/ical-link',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectGoogleRoute =
oauthDashboardConnectGoogleRouteImport.update({
id: '/google',
path: '/google',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectFastmailRoute =
oauthDashboardConnectFastmailRouteImport.update({
id: '/fastmail',
path: '/fastmail',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectCaldavRoute =
oauthDashboardConnectCaldavRouteImport.update({
id: '/caldav',
path: '/caldav',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const oauthDashboardConnectAppleRoute =
oauthDashboardConnectAppleRouteImport.update({
id: '/apple',
path: '/apple',
getParentRoute: () => oauthDashboardConnectRouteRoute,
} as any)
const dashboardDashboardSettingsPasskeysRoute =
dashboardDashboardSettingsPasskeysRouteImport.update({
id: '/passkeys',
path: '/passkeys',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardSettingsChangePasswordRoute =
dashboardDashboardSettingsChangePasswordRouteImport.update({
id: '/change-password',
path: '/change-password',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardSettingsApiTokensRoute =
dashboardDashboardSettingsApiTokensRouteImport.update({
id: '/api-tokens',
path: '/api-tokens',
getParentRoute: () => dashboardDashboardSettingsRouteRoute,
} as any)
const dashboardDashboardAccountsAccountIdIndexRoute =
dashboardDashboardAccountsAccountIdIndexRouteImport.update({
id: '/$accountId/',
path: '/$accountId/',
getParentRoute: () => dashboardDashboardAccountsRouteRoute,
} as any)
const dashboardDashboardAccountsAccountIdSetupRoute =
dashboardDashboardAccountsAccountIdSetupRouteImport.update({
id: '/$accountId/setup',
path: '/$accountId/setup',
getParentRoute: () => dashboardDashboardAccountsRouteRoute,
} as any)
const dashboardDashboardAccountsAccountIdCalendarIdRoute =
dashboardDashboardAccountsAccountIdCalendarIdRouteImport.update({
id: '/$accountId/$calendarId',
path: '/$accountId/$calendarId',
getParentRoute: () => dashboardDashboardAccountsRouteRoute,
} as any)
export interface FileRoutesByFullPath {
'/blog': typeof marketingBlogRouteRouteWithChildren
'/auth': typeof oauthAuthRouteRouteWithChildren
'/dashboard': typeof oauthDashboardRouteRouteWithChildren
'/forgot-password': typeof authForgotPasswordRoute
'/login': typeof authLoginRoute
'/register': typeof authRegisterRoute
'/reset-password': typeof authResetPasswordRoute
'/verify-authentication': typeof authVerifyAuthenticationRoute
'/verify-email': typeof authVerifyEmailRoute
'/privacy': typeof marketingPrivacyRoute
'/terms': typeof marketingTermsRoute
'/': typeof marketingIndexRoute
'/dashboard/accounts': typeof dashboardDashboardAccountsRouteRouteWithChildren
'/dashboard/connect': typeof oauthDashboardConnectRouteRouteWithChildren
'/dashboard/settings': typeof dashboardDashboardSettingsRouteRouteWithChildren
'/dashboard/feedback': typeof dashboardDashboardFeedbackRoute
'/dashboard/ical': typeof dashboardDashboardIcalRoute
'/dashboard/report': typeof dashboardDashboardReportRoute
'/blog/$slug': typeof marketingBlogSlugRoute
'/auth/google': typeof oauthAuthGoogleRoute
'/auth/outlook': typeof oauthAuthOutlookRoute
'/oauth/consent': typeof oauthOauthConsentRoute
'/dashboard/': typeof dashboardDashboardIndexRoute
'/blog/': typeof marketingBlogIndexRoute
'/dashboard/settings/api-tokens': typeof dashboardDashboardSettingsApiTokensRoute
'/dashboard/settings/change-password': typeof dashboardDashboardSettingsChangePasswordRoute
'/dashboard/settings/passkeys': typeof dashboardDashboardSettingsPasskeysRoute
'/dashboard/connect/apple': typeof oauthDashboardConnectAppleRoute
'/dashboard/connect/caldav': typeof oauthDashboardConnectCaldavRoute
'/dashboard/connect/fastmail': typeof oauthDashboardConnectFastmailRoute
'/dashboard/connect/google': typeof oauthDashboardConnectGoogleRoute
'/dashboard/connect/ical-link': typeof oauthDashboardConnectIcalLinkRoute
'/dashboard/connect/ics-file': typeof oauthDashboardConnectIcsFileRoute
'/dashboard/connect/microsoft': typeof oauthDashboardConnectMicrosoftRoute
'/dashboard/connect/outlook': typeof oauthDashboardConnectOutlookRoute
'/dashboard/connect/': typeof dashboardDashboardConnectIndexRoute
'/dashboard/events/': typeof dashboardDashboardEventsIndexRoute
'/dashboard/integrations/': typeof dashboardDashboardIntegrationsIndexRoute
'/dashboard/settings/': typeof dashboardDashboardSettingsIndexRoute
'/dashboard/upgrade/': typeof dashboardDashboardUpgradeIndexRoute
'/dashboard/accounts/$accountId/$calendarId': typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
'/dashboard/accounts/$accountId/setup': typeof dashboardDashboardAccountsAccountIdSetupRoute
'/dashboard/accounts/$accountId/': typeof dashboardDashboardAccountsAccountIdIndexRoute
}
export interface FileRoutesByTo {
'/auth': typeof oauthAuthRouteRouteWithChildren
'/dashboard': typeof dashboardDashboardIndexRoute
'/forgot-password': typeof authForgotPasswordRoute
'/login': typeof authLoginRoute
'/register': typeof authRegisterRoute
'/reset-password': typeof authResetPasswordRoute
'/verify-authentication': typeof authVerifyAuthenticationRoute
'/verify-email': typeof authVerifyEmailRoute
'/privacy': typeof marketingPrivacyRoute
'/terms': typeof marketingTermsRoute
'/': typeof marketingIndexRoute
'/dashboard/accounts': typeof dashboardDashboardAccountsRouteRouteWithChildren
'/dashboard/connect': typeof dashboardDashboardConnectIndexRoute
'/dashboard/feedback': typeof dashboardDashboardFeedbackRoute
'/dashboard/ical': typeof dashboardDashboardIcalRoute
'/dashboard/report': typeof dashboardDashboardReportRoute
'/blog/$slug': typeof marketingBlogSlugRoute
'/auth/google': typeof oauthAuthGoogleRoute
'/auth/outlook': typeof oauthAuthOutlookRoute
'/oauth/consent': typeof oauthOauthConsentRoute
'/blog': typeof marketingBlogIndexRoute
'/dashboard/settings/api-tokens': typeof dashboardDashboardSettingsApiTokensRoute
'/dashboard/settings/change-password': typeof dashboardDashboardSettingsChangePasswordRoute
'/dashboard/settings/passkeys': typeof dashboardDashboardSettingsPasskeysRoute
'/dashboard/connect/apple': typeof oauthDashboardConnectAppleRoute
'/dashboard/connect/caldav': typeof oauthDashboardConnectCaldavRoute
'/dashboard/connect/fastmail': typeof oauthDashboardConnectFastmailRoute
'/dashboard/connect/google': typeof oauthDashboardConnectGoogleRoute
'/dashboard/connect/ical-link': typeof oauthDashboardConnectIcalLinkRoute
'/dashboard/connect/ics-file': typeof oauthDashboardConnectIcsFileRoute
'/dashboard/connect/microsoft': typeof oauthDashboardConnectMicrosoftRoute
'/dashboard/connect/outlook': typeof oauthDashboardConnectOutlookRoute
'/dashboard/events': typeof dashboardDashboardEventsIndexRoute
'/dashboard/integrations': typeof dashboardDashboardIntegrationsIndexRoute
'/dashboard/settings': typeof dashboardDashboardSettingsIndexRoute
'/dashboard/upgrade': typeof dashboardDashboardUpgradeIndexRoute
'/dashboard/accounts/$accountId/$calendarId': typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
'/dashboard/accounts/$accountId/setup': typeof dashboardDashboardAccountsAccountIdSetupRoute
'/dashboard/accounts/$accountId': typeof dashboardDashboardAccountsAccountIdIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/(auth)': typeof authRouteRouteWithChildren
'/(dashboard)': typeof dashboardRouteRouteWithChildren
'/(marketing)': typeof marketingRouteRouteWithChildren
'/(oauth)': typeof oauthRouteRouteWithChildren
'/(marketing)/blog': typeof marketingBlogRouteRouteWithChildren
'/(oauth)/auth': typeof oauthAuthRouteRouteWithChildren
'/(oauth)/dashboard': typeof oauthDashboardRouteRouteWithChildren
'/(auth)/forgot-password': typeof authForgotPasswordRoute
'/(auth)/login': typeof authLoginRoute
'/(auth)/register': typeof authRegisterRoute
'/(auth)/reset-password': typeof authResetPasswordRoute
'/(auth)/verify-authentication': typeof authVerifyAuthenticationRoute
'/(auth)/verify-email': typeof authVerifyEmailRoute
'/(marketing)/privacy': typeof marketingPrivacyRoute
'/(marketing)/terms': typeof marketingTermsRoute
'/(marketing)/': typeof marketingIndexRoute
'/(dashboard)/dashboard/accounts': typeof dashboardDashboardAccountsRouteRouteWithChildren
'/(dashboard)/dashboard/connect': typeof dashboardDashboardConnectRouteRouteWithChildren
'/(dashboard)/dashboard/settings': typeof dashboardDashboardSettingsRouteRouteWithChildren
'/(oauth)/dashboard/connect': typeof oauthDashboardConnectRouteRouteWithChildren
'/(dashboard)/dashboard/feedback': typeof dashboardDashboardFeedbackRoute
'/(dashboard)/dashboard/ical': typeof dashboardDashboardIcalRoute
'/(dashboard)/dashboard/report': typeof dashboardDashboardReportRoute
'/(marketing)/blog/$slug': typeof marketingBlogSlugRoute
'/(oauth)/auth/google': typeof oauthAuthGoogleRoute
'/(oauth)/auth/outlook': typeof oauthAuthOutlookRoute
'/(oauth)/oauth/consent': typeof oauthOauthConsentRoute
'/(dashboard)/dashboard/': typeof dashboardDashboardIndexRoute
'/(marketing)/blog/': typeof marketingBlogIndexRoute
'/(dashboard)/dashboard/settings/api-tokens': typeof dashboardDashboardSettingsApiTokensRoute
'/(dashboard)/dashboard/settings/change-password': typeof dashboardDashboardSettingsChangePasswordRoute
'/(dashboard)/dashboard/settings/passkeys': typeof dashboardDashboardSettingsPasskeysRoute
'/(oauth)/dashboard/connect/apple': typeof oauthDashboardConnectAppleRoute
'/(oauth)/dashboard/connect/caldav': typeof oauthDashboardConnectCaldavRoute
'/(oauth)/dashboard/connect/fastmail': typeof oauthDashboardConnectFastmailRoute
'/(oauth)/dashboard/connect/google': typeof oauthDashboardConnectGoogleRoute
'/(oauth)/dashboard/connect/ical-link': typeof oauthDashboardConnectIcalLinkRoute
'/(oauth)/dashboard/connect/ics-file': typeof oauthDashboardConnectIcsFileRoute
'/(oauth)/dashboard/connect/microsoft': typeof oauthDashboardConnectMicrosoftRoute
'/(oauth)/dashboard/connect/outlook': typeof oauthDashboardConnectOutlookRoute
'/(dashboard)/dashboard/connect/': typeof dashboardDashboardConnectIndexRoute
'/(dashboard)/dashboard/events/': typeof dashboardDashboardEventsIndexRoute
'/(dashboard)/dashboard/integrations/': typeof dashboardDashboardIntegrationsIndexRoute
'/(dashboard)/dashboard/settings/': typeof dashboardDashboardSettingsIndexRoute
'/(dashboard)/dashboard/upgrade/': typeof dashboardDashboardUpgradeIndexRoute
'/(dashboard)/dashboard/accounts/$accountId/$calendarId': typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
'/(dashboard)/dashboard/accounts/$accountId/setup': typeof dashboardDashboardAccountsAccountIdSetupRoute
'/(dashboard)/dashboard/accounts/$accountId/': typeof dashboardDashboardAccountsAccountIdIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/blog'
| '/auth'
| '/dashboard'
| '/forgot-password'
| '/login'
| '/register'
| '/reset-password'
| '/verify-authentication'
| '/verify-email'
| '/privacy'
| '/terms'
| '/'
| '/dashboard/accounts'
| '/dashboard/connect'
| '/dashboard/settings'
| '/dashboard/feedback'
| '/dashboard/ical'
| '/dashboard/report'
| '/blog/$slug'
| '/auth/google'
| '/auth/outlook'
| '/oauth/consent'
| '/dashboard/'
| '/blog/'
| '/dashboard/settings/api-tokens'
| '/dashboard/settings/change-password'
| '/dashboard/settings/passkeys'
| '/dashboard/connect/apple'
| '/dashboard/connect/caldav'
| '/dashboard/connect/fastmail'
| '/dashboard/connect/google'
| '/dashboard/connect/ical-link'
| '/dashboard/connect/ics-file'
| '/dashboard/connect/microsoft'
| '/dashboard/connect/outlook'
| '/dashboard/connect/'
| '/dashboard/events/'
| '/dashboard/integrations/'
| '/dashboard/settings/'
| '/dashboard/upgrade/'
| '/dashboard/accounts/$accountId/$calendarId'
| '/dashboard/accounts/$accountId/setup'
| '/dashboard/accounts/$accountId/'
fileRoutesByTo: FileRoutesByTo
to:
| '/auth'
| '/dashboard'
| '/forgot-password'
| '/login'
| '/register'
| '/reset-password'
| '/verify-authentication'
| '/verify-email'
| '/privacy'
| '/terms'
| '/'
| '/dashboard/accounts'
| '/dashboard/connect'
| '/dashboard/feedback'
| '/dashboard/ical'
| '/dashboard/report'
| '/blog/$slug'
| '/auth/google'
| '/auth/outlook'
| '/oauth/consent'
| '/blog'
| '/dashboard/settings/api-tokens'
| '/dashboard/settings/change-password'
| '/dashboard/settings/passkeys'
| '/dashboard/connect/apple'
| '/dashboard/connect/caldav'
| '/dashboard/connect/fastmail'
| '/dashboard/connect/google'
| '/dashboard/connect/ical-link'
| '/dashboard/connect/ics-file'
| '/dashboard/connect/microsoft'
| '/dashboard/connect/outlook'
| '/dashboard/events'
| '/dashboard/integrations'
| '/dashboard/settings'
| '/dashboard/upgrade'
| '/dashboard/accounts/$accountId/$calendarId'
| '/dashboard/accounts/$accountId/setup'
| '/dashboard/accounts/$accountId'
id:
| '__root__'
| '/(auth)'
| '/(dashboard)'
| '/(marketing)'
| '/(oauth)'
| '/(marketing)/blog'
| '/(oauth)/auth'
| '/(oauth)/dashboard'
| '/(auth)/forgot-password'
| '/(auth)/login'
| '/(auth)/register'
| '/(auth)/reset-password'
| '/(auth)/verify-authentication'
| '/(auth)/verify-email'
| '/(marketing)/privacy'
| '/(marketing)/terms'
| '/(marketing)/'
| '/(dashboard)/dashboard/accounts'
| '/(dashboard)/dashboard/connect'
| '/(dashboard)/dashboard/settings'
| '/(oauth)/dashboard/connect'
| '/(dashboard)/dashboard/feedback'
| '/(dashboard)/dashboard/ical'
| '/(dashboard)/dashboard/report'
| '/(marketing)/blog/$slug'
| '/(oauth)/auth/google'
| '/(oauth)/auth/outlook'
| '/(oauth)/oauth/consent'
| '/(dashboard)/dashboard/'
| '/(marketing)/blog/'
| '/(dashboard)/dashboard/settings/api-tokens'
| '/(dashboard)/dashboard/settings/change-password'
| '/(dashboard)/dashboard/settings/passkeys'
| '/(oauth)/dashboard/connect/apple'
| '/(oauth)/dashboard/connect/caldav'
| '/(oauth)/dashboard/connect/fastmail'
| '/(oauth)/dashboard/connect/google'
| '/(oauth)/dashboard/connect/ical-link'
| '/(oauth)/dashboard/connect/ics-file'
| '/(oauth)/dashboard/connect/microsoft'
| '/(oauth)/dashboard/connect/outlook'
| '/(dashboard)/dashboard/connect/'
| '/(dashboard)/dashboard/events/'
| '/(dashboard)/dashboard/integrations/'
| '/(dashboard)/dashboard/settings/'
| '/(dashboard)/dashboard/upgrade/'
| '/(dashboard)/dashboard/accounts/$accountId/$calendarId'
| '/(dashboard)/dashboard/accounts/$accountId/setup'
| '/(dashboard)/dashboard/accounts/$accountId/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
authRouteRoute: typeof authRouteRouteWithChildren
dashboardRouteRoute: typeof dashboardRouteRouteWithChildren
marketingRouteRoute: typeof marketingRouteRouteWithChildren
oauthRouteRoute: typeof oauthRouteRouteWithChildren
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/(oauth)': {
id: '/(oauth)'
path: ''
fullPath: ''
preLoaderRoute: typeof oauthRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(marketing)': {
id: '/(marketing)'
path: ''
fullPath: ''
preLoaderRoute: typeof marketingRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(dashboard)': {
id: '/(dashboard)'
path: ''
fullPath: ''
preLoaderRoute: typeof dashboardRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(auth)': {
id: '/(auth)'
path: ''
fullPath: ''
preLoaderRoute: typeof authRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/(marketing)/': {
id: '/(marketing)/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof marketingIndexRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(marketing)/terms': {
id: '/(marketing)/terms'
path: '/terms'
fullPath: '/terms'
preLoaderRoute: typeof marketingTermsRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(marketing)/privacy': {
id: '/(marketing)/privacy'
path: '/privacy'
fullPath: '/privacy'
preLoaderRoute: typeof marketingPrivacyRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(auth)/verify-email': {
id: '/(auth)/verify-email'
path: '/verify-email'
fullPath: '/verify-email'
preLoaderRoute: typeof authVerifyEmailRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/verify-authentication': {
id: '/(auth)/verify-authentication'
path: '/verify-authentication'
fullPath: '/verify-authentication'
preLoaderRoute: typeof authVerifyAuthenticationRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/reset-password': {
id: '/(auth)/reset-password'
path: '/reset-password'
fullPath: '/reset-password'
preLoaderRoute: typeof authResetPasswordRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/register': {
id: '/(auth)/register'
path: '/register'
fullPath: '/register'
preLoaderRoute: typeof authRegisterRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/login': {
id: '/(auth)/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof authLoginRouteImport
parentRoute: typeof authRouteRoute
}
'/(auth)/forgot-password': {
id: '/(auth)/forgot-password'
path: '/forgot-password'
fullPath: '/forgot-password'
preLoaderRoute: typeof authForgotPasswordRouteImport
parentRoute: typeof authRouteRoute
}
'/(oauth)/dashboard': {
id: '/(oauth)/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof oauthDashboardRouteRouteImport
parentRoute: typeof oauthRouteRoute
}
'/(oauth)/auth': {
id: '/(oauth)/auth'
path: '/auth'
fullPath: '/auth'
preLoaderRoute: typeof oauthAuthRouteRouteImport
parentRoute: typeof oauthRouteRoute
}
'/(marketing)/blog': {
id: '/(marketing)/blog'
path: '/blog'
fullPath: '/blog'
preLoaderRoute: typeof marketingBlogRouteRouteImport
parentRoute: typeof marketingRouteRoute
}
'/(marketing)/blog/': {
id: '/(marketing)/blog/'
path: '/'
fullPath: '/blog/'
preLoaderRoute: typeof marketingBlogIndexRouteImport
parentRoute: typeof marketingBlogRouteRoute
}
'/(dashboard)/dashboard/': {
id: '/(dashboard)/dashboard/'
path: '/dashboard'
fullPath: '/dashboard/'
preLoaderRoute: typeof dashboardDashboardIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(oauth)/oauth/consent': {
id: '/(oauth)/oauth/consent'
path: '/oauth/consent'
fullPath: '/oauth/consent'
preLoaderRoute: typeof oauthOauthConsentRouteImport
parentRoute: typeof oauthRouteRoute
}
'/(oauth)/auth/outlook': {
id: '/(oauth)/auth/outlook'
path: '/outlook'
fullPath: '/auth/outlook'
preLoaderRoute: typeof oauthAuthOutlookRouteImport
parentRoute: typeof oauthAuthRouteRoute
}
'/(oauth)/auth/google': {
id: '/(oauth)/auth/google'
path: '/google'
fullPath: '/auth/google'
preLoaderRoute: typeof oauthAuthGoogleRouteImport
parentRoute: typeof oauthAuthRouteRoute
}
'/(marketing)/blog/$slug': {
id: '/(marketing)/blog/$slug'
path: '/$slug'
fullPath: '/blog/$slug'
preLoaderRoute: typeof marketingBlogSlugRouteImport
parentRoute: typeof marketingBlogRouteRoute
}
'/(dashboard)/dashboard/report': {
id: '/(dashboard)/dashboard/report'
path: '/dashboard/report'
fullPath: '/dashboard/report'
preLoaderRoute: typeof dashboardDashboardReportRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/ical': {
id: '/(dashboard)/dashboard/ical'
path: '/dashboard/ical'
fullPath: '/dashboard/ical'
preLoaderRoute: typeof dashboardDashboardIcalRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/feedback': {
id: '/(dashboard)/dashboard/feedback'
path: '/dashboard/feedback'
fullPath: '/dashboard/feedback'
preLoaderRoute: typeof dashboardDashboardFeedbackRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(oauth)/dashboard/connect': {
id: '/(oauth)/dashboard/connect'
path: '/connect'
fullPath: '/dashboard/connect'
preLoaderRoute: typeof oauthDashboardConnectRouteRouteImport
parentRoute: typeof oauthDashboardRouteRoute
}
'/(dashboard)/dashboard/settings': {
id: '/(dashboard)/dashboard/settings'
path: '/dashboard/settings'
fullPath: '/dashboard/settings'
preLoaderRoute: typeof dashboardDashboardSettingsRouteRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/connect': {
id: '/(dashboard)/dashboard/connect'
path: '/dashboard/connect'
fullPath: '/dashboard/connect'
preLoaderRoute: typeof dashboardDashboardConnectRouteRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/accounts': {
id: '/(dashboard)/dashboard/accounts'
path: '/dashboard/accounts'
fullPath: '/dashboard/accounts'
preLoaderRoute: typeof dashboardDashboardAccountsRouteRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/upgrade/': {
id: '/(dashboard)/dashboard/upgrade/'
path: '/dashboard/upgrade'
fullPath: '/dashboard/upgrade/'
preLoaderRoute: typeof dashboardDashboardUpgradeIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/settings/': {
id: '/(dashboard)/dashboard/settings/'
path: '/'
fullPath: '/dashboard/settings/'
preLoaderRoute: typeof dashboardDashboardSettingsIndexRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/integrations/': {
id: '/(dashboard)/dashboard/integrations/'
path: '/dashboard/integrations'
fullPath: '/dashboard/integrations/'
preLoaderRoute: typeof dashboardDashboardIntegrationsIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/events/': {
id: '/(dashboard)/dashboard/events/'
path: '/dashboard/events'
fullPath: '/dashboard/events/'
preLoaderRoute: typeof dashboardDashboardEventsIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/dashboard/connect/': {
id: '/(dashboard)/dashboard/connect/'
path: '/'
fullPath: '/dashboard/connect/'
preLoaderRoute: typeof dashboardDashboardConnectIndexRouteImport
parentRoute: typeof dashboardDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/outlook': {
id: '/(oauth)/dashboard/connect/outlook'
path: '/outlook'
fullPath: '/dashboard/connect/outlook'
preLoaderRoute: typeof oauthDashboardConnectOutlookRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/microsoft': {
id: '/(oauth)/dashboard/connect/microsoft'
path: '/microsoft'
fullPath: '/dashboard/connect/microsoft'
preLoaderRoute: typeof oauthDashboardConnectMicrosoftRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/ics-file': {
id: '/(oauth)/dashboard/connect/ics-file'
path: '/ics-file'
fullPath: '/dashboard/connect/ics-file'
preLoaderRoute: typeof oauthDashboardConnectIcsFileRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/ical-link': {
id: '/(oauth)/dashboard/connect/ical-link'
path: '/ical-link'
fullPath: '/dashboard/connect/ical-link'
preLoaderRoute: typeof oauthDashboardConnectIcalLinkRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/google': {
id: '/(oauth)/dashboard/connect/google'
path: '/google'
fullPath: '/dashboard/connect/google'
preLoaderRoute: typeof oauthDashboardConnectGoogleRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/fastmail': {
id: '/(oauth)/dashboard/connect/fastmail'
path: '/fastmail'
fullPath: '/dashboard/connect/fastmail'
preLoaderRoute: typeof oauthDashboardConnectFastmailRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/caldav': {
id: '/(oauth)/dashboard/connect/caldav'
path: '/caldav'
fullPath: '/dashboard/connect/caldav'
preLoaderRoute: typeof oauthDashboardConnectCaldavRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(oauth)/dashboard/connect/apple': {
id: '/(oauth)/dashboard/connect/apple'
path: '/apple'
fullPath: '/dashboard/connect/apple'
preLoaderRoute: typeof oauthDashboardConnectAppleRouteImport
parentRoute: typeof oauthDashboardConnectRouteRoute
}
'/(dashboard)/dashboard/settings/passkeys': {
id: '/(dashboard)/dashboard/settings/passkeys'
path: '/passkeys'
fullPath: '/dashboard/settings/passkeys'
preLoaderRoute: typeof dashboardDashboardSettingsPasskeysRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/settings/change-password': {
id: '/(dashboard)/dashboard/settings/change-password'
path: '/change-password'
fullPath: '/dashboard/settings/change-password'
preLoaderRoute: typeof dashboardDashboardSettingsChangePasswordRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/settings/api-tokens': {
id: '/(dashboard)/dashboard/settings/api-tokens'
path: '/api-tokens'
fullPath: '/dashboard/settings/api-tokens'
preLoaderRoute: typeof dashboardDashboardSettingsApiTokensRouteImport
parentRoute: typeof dashboardDashboardSettingsRouteRoute
}
'/(dashboard)/dashboard/accounts/$accountId/': {
id: '/(dashboard)/dashboard/accounts/$accountId/'
path: '/$accountId'
fullPath: '/dashboard/accounts/$accountId/'
preLoaderRoute: typeof dashboardDashboardAccountsAccountIdIndexRouteImport
parentRoute: typeof dashboardDashboardAccountsRouteRoute
}
'/(dashboard)/dashboard/accounts/$accountId/setup': {
id: '/(dashboard)/dashboard/accounts/$accountId/setup'
path: '/$accountId/setup'
fullPath: '/dashboard/accounts/$accountId/setup'
preLoaderRoute: typeof dashboardDashboardAccountsAccountIdSetupRouteImport
parentRoute: typeof dashboardDashboardAccountsRouteRoute
}
'/(dashboard)/dashboard/accounts/$accountId/$calendarId': {
id: '/(dashboard)/dashboard/accounts/$accountId/$calendarId'
path: '/$accountId/$calendarId'
fullPath: '/dashboard/accounts/$accountId/$calendarId'
preLoaderRoute: typeof dashboardDashboardAccountsAccountIdCalendarIdRouteImport
parentRoute: typeof dashboardDashboardAccountsRouteRoute
}
}
}
interface authRouteRouteChildren {
authForgotPasswordRoute: typeof authForgotPasswordRoute
authLoginRoute: typeof authLoginRoute
authRegisterRoute: typeof authRegisterRoute
authResetPasswordRoute: typeof authResetPasswordRoute
authVerifyAuthenticationRoute: typeof authVerifyAuthenticationRoute
authVerifyEmailRoute: typeof authVerifyEmailRoute
}
const authRouteRouteChildren: authRouteRouteChildren = {
authForgotPasswordRoute: authForgotPasswordRoute,
authLoginRoute: authLoginRoute,
authRegisterRoute: authRegisterRoute,
authResetPasswordRoute: authResetPasswordRoute,
authVerifyAuthenticationRoute: authVerifyAuthenticationRoute,
authVerifyEmailRoute: authVerifyEmailRoute,
}
const authRouteRouteWithChildren = authRouteRoute._addFileChildren(
authRouteRouteChildren,
)
interface dashboardDashboardAccountsRouteRouteChildren {
dashboardDashboardAccountsAccountIdCalendarIdRoute: typeof dashboardDashboardAccountsAccountIdCalendarIdRoute
dashboardDashboardAccountsAccountIdSetupRoute: typeof dashboardDashboardAccountsAccountIdSetupRoute
dashboardDashboardAccountsAccountIdIndexRoute: typeof dashboardDashboardAccountsAccountIdIndexRoute
}
const dashboardDashboardAccountsRouteRouteChildren: dashboardDashboardAccountsRouteRouteChildren =
{
dashboardDashboardAccountsAccountIdCalendarIdRoute:
dashboardDashboardAccountsAccountIdCalendarIdRoute,
dashboardDashboardAccountsAccountIdSetupRoute:
dashboardDashboardAccountsAccountIdSetupRoute,
dashboardDashboardAccountsAccountIdIndexRoute:
dashboardDashboardAccountsAccountIdIndexRoute,
}
const dashboardDashboardAccountsRouteRouteWithChildren =
dashboardDashboardAccountsRouteRoute._addFileChildren(
dashboardDashboardAccountsRouteRouteChildren,
)
interface dashboardDashboardConnectRouteRouteChildren {
dashboardDashboardConnectIndexRoute: typeof dashboardDashboardConnectIndexRoute
}
const dashboardDashboardConnectRouteRouteChildren: dashboardDashboardConnectRouteRouteChildren =
{
dashboardDashboardConnectIndexRoute: dashboardDashboardConnectIndexRoute,
}
const dashboardDashboardConnectRouteRouteWithChildren =
dashboardDashboardConnectRouteRoute._addFileChildren(
dashboardDashboardConnectRouteRouteChildren,
)
interface dashboardDashboardSettingsRouteRouteChildren {
dashboardDashboardSettingsApiTokensRoute: typeof dashboardDashboardSettingsApiTokensRoute
dashboardDashboardSettingsChangePasswordRoute: typeof dashboardDashboardSettingsChangePasswordRoute
dashboardDashboardSettingsPasskeysRoute: typeof dashboardDashboardSettingsPasskeysRoute
dashboardDashboardSettingsIndexRoute: typeof dashboardDashboardSettingsIndexRoute
}
const dashboardDashboardSettingsRouteRouteChildren: dashboardDashboardSettingsRouteRouteChildren =
{
dashboardDashboardSettingsApiTokensRoute:
dashboardDashboardSettingsApiTokensRoute,
dashboardDashboardSettingsChangePasswordRoute:
dashboardDashboardSettingsChangePasswordRoute,
dashboardDashboardSettingsPasskeysRoute:
dashboardDashboardSettingsPasskeysRoute,
dashboardDashboardSettingsIndexRoute: dashboardDashboardSettingsIndexRoute,
}
const dashboardDashboardSettingsRouteRouteWithChildren =
dashboardDashboardSettingsRouteRoute._addFileChildren(
dashboardDashboardSettingsRouteRouteChildren,
)
interface dashboardRouteRouteChildren {
dashboardDashboardAccountsRouteRoute: typeof dashboardDashboardAccountsRouteRouteWithChildren
dashboardDashboardConnectRouteRoute: typeof dashboardDashboardConnectRouteRouteWithChildren
dashboardDashboardSettingsRouteRoute: typeof dashboardDashboardSettingsRouteRouteWithChildren
dashboardDashboardFeedbackRoute: typeof dashboardDashboardFeedbackRoute
dashboardDashboardIcalRoute: typeof dashboardDashboardIcalRoute
dashboardDashboardReportRoute: typeof dashboardDashboardReportRoute
dashboardDashboardIndexRoute: typeof dashboardDashboardIndexRoute
dashboardDashboardEventsIndexRoute: typeof dashboardDashboardEventsIndexRoute
dashboardDashboardIntegrationsIndexRoute: typeof dashboardDashboardIntegrationsIndexRoute
dashboardDashboardUpgradeIndexRoute: typeof dashboardDashboardUpgradeIndexRoute
}
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardDashboardAccountsRouteRoute:
dashboardDashboardAccountsRouteRouteWithChildren,
dashboardDashboardConnectRouteRoute:
dashboardDashboardConnectRouteRouteWithChildren,
dashboardDashboardSettingsRouteRoute:
dashboardDashboardSettingsRouteRouteWithChildren,
dashboardDashboardFeedbackRoute: dashboardDashboardFeedbackRoute,
dashboardDashboardIcalRoute: dashboardDashboardIcalRoute,
dashboardDashboardReportRoute: dashboardDashboardReportRoute,
dashboardDashboardIndexRoute: dashboardDashboardIndexRoute,
dashboardDashboardEventsIndexRoute: dashboardDashboardEventsIndexRoute,
dashboardDashboardIntegrationsIndexRoute:
dashboardDashboardIntegrationsIndexRoute,
dashboardDashboardUpgradeIndexRoute: dashboardDashboardUpgradeIndexRoute,
}
const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren(
dashboardRouteRouteChildren,
)
interface marketingBlogRouteRouteChildren {
marketingBlogSlugRoute: typeof marketingBlogSlugRoute
marketingBlogIndexRoute: typeof marketingBlogIndexRoute
}
const marketingBlogRouteRouteChildren: marketingBlogRouteRouteChildren = {
marketingBlogSlugRoute: marketingBlogSlugRoute,
marketingBlogIndexRoute: marketingBlogIndexRoute,
}
const marketingBlogRouteRouteWithChildren =
marketingBlogRouteRoute._addFileChildren(marketingBlogRouteRouteChildren)
interface marketingRouteRouteChildren {
marketingBlogRouteRoute: typeof marketingBlogRouteRouteWithChildren
marketingPrivacyRoute: typeof marketingPrivacyRoute
marketingTermsRoute: typeof marketingTermsRoute
marketingIndexRoute: typeof marketingIndexRoute
}
const marketingRouteRouteChildren: marketingRouteRouteChildren = {
marketingBlogRouteRoute: marketingBlogRouteRouteWithChildren,
marketingPrivacyRoute: marketingPrivacyRoute,
marketingTermsRoute: marketingTermsRoute,
marketingIndexRoute: marketingIndexRoute,
}
const marketingRouteRouteWithChildren = marketingRouteRoute._addFileChildren(
marketingRouteRouteChildren,
)
interface oauthAuthRouteRouteChildren {
oauthAuthGoogleRoute: typeof oauthAuthGoogleRoute
oauthAuthOutlookRoute: typeof oauthAuthOutlookRoute
}
const oauthAuthRouteRouteChildren: oauthAuthRouteRouteChildren = {
oauthAuthGoogleRoute: oauthAuthGoogleRoute,
oauthAuthOutlookRoute: oauthAuthOutlookRoute,
}
const oauthAuthRouteRouteWithChildren = oauthAuthRouteRoute._addFileChildren(
oauthAuthRouteRouteChildren,
)
interface oauthDashboardConnectRouteRouteChildren {
oauthDashboardConnectAppleRoute: typeof oauthDashboardConnectAppleRoute
oauthDashboardConnectCaldavRoute: typeof oauthDashboardConnectCaldavRoute
oauthDashboardConnectFastmailRoute: typeof oauthDashboardConnectFastmailRoute
oauthDashboardConnectGoogleRoute: typeof oauthDashboardConnectGoogleRoute
oauthDashboardConnectIcalLinkRoute: typeof oauthDashboardConnectIcalLinkRoute
oauthDashboardConnectIcsFileRoute: typeof oauthDashboardConnectIcsFileRoute
oauthDashboardConnectMicrosoftRoute: typeof oauthDashboardConnectMicrosoftRoute
oauthDashboardConnectOutlookRoute: typeof oauthDashboardConnectOutlookRoute
}
const oauthDashboardConnectRouteRouteChildren: oauthDashboardConnectRouteRouteChildren =
{
oauthDashboardConnectAppleRoute: oauthDashboardConnectAppleRoute,
oauthDashboardConnectCaldavRoute: oauthDashboardConnectCaldavRoute,
oauthDashboardConnectFastmailRoute: oauthDashboardConnectFastmailRoute,
oauthDashboardConnectGoogleRoute: oauthDashboardConnectGoogleRoute,
oauthDashboardConnectIcalLinkRoute: oauthDashboardConnectIcalLinkRoute,
oauthDashboardConnectIcsFileRoute: oauthDashboardConnectIcsFileRoute,
oauthDashboardConnectMicrosoftRoute: oauthDashboardConnectMicrosoftRoute,
oauthDashboardConnectOutlookRoute: oauthDashboardConnectOutlookRoute,
}
const oauthDashboardConnectRouteRouteWithChildren =
oauthDashboardConnectRouteRoute._addFileChildren(
oauthDashboardConnectRouteRouteChildren,
)
interface oauthDashboardRouteRouteChildren {
oauthDashboardConnectRouteRoute: typeof oauthDashboardConnectRouteRouteWithChildren
}
const oauthDashboardRouteRouteChildren: oauthDashboardRouteRouteChildren = {
oauthDashboardConnectRouteRoute: oauthDashboardConnectRouteRouteWithChildren,
}
const oauthDashboardRouteRouteWithChildren =
oauthDashboardRouteRoute._addFileChildren(oauthDashboardRouteRouteChildren)
interface oauthRouteRouteChildren {
oauthAuthRouteRoute: typeof oauthAuthRouteRouteWithChildren
oauthDashboardRouteRoute: typeof oauthDashboardRouteRouteWithChildren
oauthOauthConsentRoute: typeof oauthOauthConsentRoute
}
const oauthRouteRouteChildren: oauthRouteRouteChildren = {
oauthAuthRouteRoute: oauthAuthRouteRouteWithChildren,
oauthDashboardRouteRoute: oauthDashboardRouteRouteWithChildren,
oauthOauthConsentRoute: oauthOauthConsentRoute,
}
const oauthRouteRouteWithChildren = oauthRouteRoute._addFileChildren(
oauthRouteRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
authRouteRoute: authRouteRouteWithChildren,
dashboardRouteRoute: dashboardRouteRouteWithChildren,
marketingRouteRoute: marketingRouteRouteWithChildren,
oauthRouteRoute: oauthRouteRouteWithChildren,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes()
================================================
FILE: applications/web/src/router.ts
================================================
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./generated/tanstack/route-tree.generated";
import { HttpError } from "./lib/fetcher";
import { getPublicRuntimeConfig, getServerPublicRuntimeConfig } from "./lib/runtime-config";
import type { PublicRuntimeConfig } from "./lib/runtime-config";
import { hasSessionCookie } from "./lib/session-cookie";
import type { AppRouterContext } from "./lib/router-context";
import type { ViteAssets } from "./lib/router-context";
interface CreateAppRouterOptions {
request?: Request;
viteAssets?: ViteAssets;
}
function getConfiguredApiOrigin(): string | undefined {
if (typeof window === "undefined") {
return process.env.VITE_API_URL;
}
return import.meta.env.VITE_API_URL;
}
function resolveApiOrigin(request: Request | undefined): string {
const configuredApiOrigin = getConfiguredApiOrigin();
if (configuredApiOrigin && configuredApiOrigin.length > 0) {
return configuredApiOrigin;
}
if (request) {
return new URL(request.url).origin;
}
if (typeof window !== "undefined") {
return window.location.origin;
}
throw new Error("Unable to resolve API origin.");
}
function resolveWebOrigin(request: Request | undefined): string {
if (request) {
return new URL(request.url).origin;
}
if (typeof window !== "undefined") {
return window.location.origin;
}
throw new Error("Unable to resolve web origin.");
}
function createJsonFetcher(
requestCookie: string | null,
origin: string,
): AppRouterContext["fetchApi"] {
return async (path: string, init: RequestInit = {}): Promise => {
const requestHeaders = new Headers(init.headers);
if (requestCookie && !requestHeaders.has("cookie")) {
requestHeaders.set("cookie", requestCookie);
}
const absoluteUrl = new URL(path, origin).toString();
const response = await fetch(absoluteUrl, {
...init,
credentials: "include",
headers: requestHeaders,
});
if (!response.ok) {
throw new HttpError(response.status, path);
}
return response.json();
};
}
function createApiFetcher(
request: Request | undefined,
): AppRouterContext["fetchApi"] {
const requestCookie = request?.headers.get("cookie") ?? null;
const apiOrigin = resolveApiOrigin(request);
return createJsonFetcher(requestCookie, apiOrigin);
}
function createWebFetcher(
request: Request | undefined,
): AppRouterContext["fetchWeb"] {
const requestCookie = request?.headers.get("cookie") ?? null;
const webOrigin = resolveWebOrigin(request);
return createJsonFetcher(requestCookie, webOrigin);
}
function resolveRuntimeConfig(request: Request | undefined): PublicRuntimeConfig {
if (request) {
return getServerPublicRuntimeConfig({
environment: process.env,
countryCode: request.headers.get("cf-ipcountry"),
});
}
return getPublicRuntimeConfig();
}
function createSessionChecker(
request: Request | undefined,
): () => boolean {
if (request) {
const cookieHeader = request.headers.get("cookie") ?? undefined;
const serverHasSession = hasSessionCookie(cookieHeader);
return () => serverHasSession;
}
return () => hasSessionCookie();
}
function buildRouterContext(
request: Request | undefined,
viteAssets: ViteAssets | undefined,
): AppRouterContext {
return {
auth: {
hasSession: createSessionChecker(request),
},
fetchApi: createApiFetcher(request),
fetchWeb: createWebFetcher(request),
runtimeConfig: resolveRuntimeConfig(request),
viteAssets: viteAssets ?? null,
};
}
export function createAppRouter(options: CreateAppRouterOptions = {}) {
const router = createRouter({
context: buildRouterContext(options.request, options.viteAssets),
defaultPreload: "intent",
routeTree,
scrollRestoration: false,
});
return router;
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType;
}
}
================================================
FILE: applications/web/src/routes/(auth)/forgot-password.tsx
================================================
import { useState } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
import Mail from "lucide-react/dist/esm/icons/mail";
import { forgotPassword } from "@/lib/auth";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import { resolveErrorMessage } from "@/utils/errors";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Input } from "@/components/ui/primitives/input";
import { Text } from "@/components/ui/primitives/text";
import { TextLink } from "@/components/ui/primitives/text-link";
import { AuthSwitchPrompt } from "@/features/auth/components/auth-switch-prompt";
export const Route = createFileRoute("/(auth)/forgot-password")({
loader: async ({ context }) => {
const capabilities = await fetchAuthCapabilitiesWithApi(context.fetchApi);
if (!capabilities.supportsPasswordReset) {
throw redirect({ to: "/login" });
}
return capabilities;
},
component: ForgotPasswordPage,
});
function ForgotPasswordPage() {
const [status, setStatus] = useState<"idle" | "loading" | "sent">("idle");
const [error, setError] = useState(null);
if (status === "sent") return ;
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = String(formData.get("email") ?? "");
if (!email) return;
setStatus("loading");
setError(null);
try {
await forgotPassword(email);
track(ANALYTICS_EVENTS.password_reset_requested);
setStatus("sent");
} catch (err) {
setError(resolveErrorMessage(err, "Failed to send reset email"));
setStatus("idle");
}
};
return (
<>
Reset password
Enter your email and we'll send you a link to reset your password.
{error && {error} }
Remember your password? Sign in
>
);
}
function SuccessState() {
return (
<>
Check your email
If an account exists with that email, we sent you a password reset link.
Back to sign in
>
);
}
================================================
FILE: applications/web/src/routes/(auth)/login.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import { AuthForm, type AuthScreenCopy } from "@/features/auth/components/auth-form";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import {
getMcpAuthorizationSearch,
toStringSearchParams,
} from "@/lib/mcp-auth-flow";
export const Route = createFileRoute("/(auth)/login")({
loader: ({ context }) => fetchAuthCapabilitiesWithApi(context.fetchApi),
component: LoginPage,
validateSearch: toStringSearchParams,
});
const copy: AuthScreenCopy = {
heading: "Welcome back",
subtitle: "Sign in to your Keeper.sh account",
oauthActionLabel: "Sign in",
submitLabel: "Sign in",
switchPrompt: "Don't have an account yet?",
switchCta: "Register",
switchTo: "/register",
action: "signIn",
};
function LoginPage() {
const capabilities = Route.useLoaderData();
const search = Route.useSearch();
return (
);
}
================================================
FILE: applications/web/src/routes/(auth)/register.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import { AuthForm, type AuthScreenCopy } from "@/features/auth/components/auth-form";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import {
getMcpAuthorizationSearch,
toStringSearchParams,
} from "@/lib/mcp-auth-flow";
export const Route = createFileRoute("/(auth)/register")({
loader: ({ context }) => fetchAuthCapabilitiesWithApi(context.fetchApi),
component: RegisterPage,
validateSearch: toStringSearchParams,
});
const copy: AuthScreenCopy = {
heading: "Create your account",
subtitle: "Get started with Keeper.sh for free",
oauthActionLabel: "Sign up",
submitLabel: "Sign up",
switchPrompt: "Already have an account?",
switchCta: "Sign in",
switchTo: "/login",
action: "signUp",
};
function RegisterPage() {
const capabilities = Route.useLoaderData();
const search = Route.useSearch();
return (
);
}
================================================
FILE: applications/web/src/routes/(auth)/reset-password.tsx
================================================
import { useState } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
import CircleCheck from "lucide-react/dist/esm/icons/circle-check";
import { resetPassword } from "@/lib/auth";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import { resolveErrorMessage } from "@/utils/errors";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Input } from "@/components/ui/primitives/input";
import { Text } from "@/components/ui/primitives/text";
import { TextLink } from "@/components/ui/primitives/text-link";
import { AuthSwitchPrompt } from "@/features/auth/components/auth-switch-prompt";
type SearchParams = { token?: string };
export const Route = createFileRoute("/(auth)/reset-password")({
loader: async ({ context }) => {
const capabilities = await fetchAuthCapabilitiesWithApi(context.fetchApi);
if (!capabilities.supportsPasswordReset) {
throw redirect({ to: "/login" });
}
return capabilities;
},
component: ResetPasswordPage,
validateSearch: (search: Record): SearchParams => ({
token: typeof search.token === "string" ? search.token : undefined,
}),
});
function ResetPasswordPage() {
const { token } = Route.useSearch();
if (!token) return ;
return ;
}
function ResetPasswordForm({ token }: { token: string }) {
const [status, setStatus] = useState<"idle" | "loading" | "success">("idle");
const [error, setError] = useState(null);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const password = String(formData.get("password") ?? "");
const confirmPassword = String(formData.get("confirmPassword") ?? "");
if (!password || !confirmPassword) return;
if (password !== confirmPassword) {
setError("Passwords do not match.");
return;
}
setStatus("loading");
setError(null);
try {
await resetPassword(token, password);
track(ANALYTICS_EVENTS.password_reset_completed);
setStatus("success");
} catch (err) {
setError(resolveErrorMessage(err, "Failed to reset password"));
setStatus("idle");
}
};
if (status === "success") return ;
return (
<>
Set new password
Enter your new password below.
{error && {error} }
>
);
}
function SuccessState() {
return (
<>
Password reset
Your password has been successfully reset.
Back to sign in
>
);
}
function InvalidTokenState() {
return (
<>
Invalid link
This password reset link is invalid or has expired.
Request a new link
>
);
}
================================================
FILE: applications/web/src/routes/(auth)/route.tsx
================================================
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { getMcpAuthorizationSearch } from "@/lib/mcp-auth-flow";
import { resolveAuthRedirect } from "@/lib/route-access-guards";
export const Route = createFileRoute("/(auth)")({
beforeLoad: ({ context, search }) => {
if (getMcpAuthorizationSearch(search)) {
return;
}
const redirectTarget = resolveAuthRedirect(context.auth.hasSession());
if (redirectTarget) {
throw redirect({ to: redirectTarget });
}
},
component: AuthLayout,
head: () => ({
meta: [{ content: "noindex, nofollow", name: "robots" }],
}),
});
function AuthLayout() {
return (
);
}
================================================
FILE: applications/web/src/routes/(auth)/verify-authentication.tsx
================================================
import { useEffect } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useSession } from "@/hooks/use-session";
import { Text } from "@/components/ui/primitives/text";
export const Route = createFileRoute("/(auth)/verify-authentication")({
component: VerifyAuthenticationPage,
});
function VerifyAuthenticationPage() {
const navigate = useNavigate();
const { user, isLoading } = useSession();
useEffect(() => {
if (!isLoading && user) navigate({ to: "/dashboard" });
}, [user, isLoading, navigate]);
return (
Redirecting...
);
}
================================================
FILE: applications/web/src/routes/(auth)/verify-email.tsx
================================================
import { useState } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
import Mail from "lucide-react/dist/esm/icons/mail";
import { authClient } from "@/lib/auth-client";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
export const Route = createFileRoute("/(auth)/verify-email")({
loader: async ({ context }) => {
const capabilities = await fetchAuthCapabilitiesWithApi(context.fetchApi);
if (!capabilities.requiresEmailVerification) {
throw redirect({ to: "/login" });
}
return capabilities;
},
component: VerifyEmailPage,
});
function VerifyEmailPage() {
const [status, setStatus] = useState<"idle" | "loading" | "sent">("idle");
const [error, setError] = useState(null);
const [email] = useState(() => {
const stored = sessionStorage.getItem("pendingVerificationEmail");
if (stored) sessionStorage.removeItem("pendingVerificationEmail");
return stored;
});
const [callbackURL] = useState(() => {
const stored = sessionStorage.getItem("pendingVerificationCallbackUrl");
if (stored) sessionStorage.removeItem("pendingVerificationCallbackUrl");
return stored ?? "/dashboard";
});
const handleResend = async () => {
if (!email) return;
setStatus("loading");
setError(null);
const { error } = await authClient.sendVerificationEmail({
callbackURL,
email,
});
if (error) {
setError(error.message ?? "Failed to resend verification email");
setStatus("idle");
return;
}
setStatus("sent");
};
return (
<>
Check your email
We sent a verification link to your email. Click the link to verify your account. Please check your spam or junk folder if you don't see it in your inbox.
{error && (
{error}
)}
{status === "sent" && (
Verification email sent.
)}
Resend verification email
>
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.$calendarId.tsx
================================================
import { use, useEffect, useMemo } from "react";
import { createFileRoute } from "@tanstack/react-router";
import useSWR, { preload, useSWRConfig } from "swr";
import CheckIcon from "lucide-react/dist/esm/icons/check";
import { useAtomValue, useStore } from "jotai";
import { useEntitlements, useMutateEntitlements, canAddMore } from "@/hooks/use-entitlements";
import { BackButton } from "@/components/ui/primitives/back-button";
import { UpgradeHint, PremiumFeatureGate } from "@/components/ui/primitives/upgrade-hint";
import { Pagination, PaginationPrevious, PaginationNext } from "@/components/ui/primitives/pagination";
import { RouteShell } from "@/components/ui/shells/route-shell";
import { MetadataRow } from "@/features/dashboard/components/metadata-row";
import { ProviderIcon } from "@/components/ui/primitives/provider-icon";
import { DashboardHeading1, DashboardSection } from "@/components/ui/primitives/dashboard-heading";
import { apiFetch, fetcher } from "@/lib/fetcher";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { serializedPatch, serializedCall } from "@/lib/serialized-mutate";
import { formatDate } from "@/lib/time";
import { canPull, canPush } from "@/utils/calendars";
import type { CalendarAccount, CalendarDetail, CalendarSource } from "@/types/api";
import {
NavigationMenu,
NavigationMenuEmptyItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import {
NavigationMenuEditableItem,
NavigationMenuEditableTemplateItem,
} from "@/components/ui/composites/navigation-menu/navigation-menu-editable";
import { MenuVariantContext, ItemDisabledContext } from "@/components/ui/composites/navigation-menu/navigation-menu.contexts";
import {
DISABLED_LABEL_TONE,
LABEL_TONE,
navigationMenuItemStyle,
navigationMenuCheckbox,
navigationMenuCheckboxIcon,
navigationMenuToggleTrack,
navigationMenuToggleThumb,
} from "@/components/ui/composites/navigation-menu/navigation-menu.styles";
import { Text } from "@/components/ui/primitives/text";
import { TemplateText } from "@/components/ui/primitives/template-text";
import {
calendarDetailAtom,
calendarDetailLoadedAtom,
calendarDetailErrorAtom,
calendarNameAtom,
calendarProviderAtom,
calendarTypeAtom,
customEventNameAtom,
excludeEventNameAtom,
excludeFieldAtoms,
} from "@/state/calendar-detail";
import type { ExcludeField } from "@/state/calendar-detail";
import {
destinationIdsAtom,
selectDestinationInclusion,
} from "@/state/destination-ids";
export const Route = createFileRoute(
"/(dashboard)/dashboard/accounts/$accountId/$calendarId",
)({
component: CalendarDetailPage,
});
interface SyncSetting {
field: ExcludeField;
label: string;
matchesField: boolean;
}
const SYNC_SETTINGS: SyncSetting[] = [
{ field: "excludeEventDescription", label: "Sync Event Description", matchesField: false },
{ field: "excludeEventLocation", label: "Sync Event Location", matchesField: false },
];
const EXCLUSION_SETTINGS: SyncSetting[] = [
{ field: "excludeAllDayEvents", label: "Exclude All Day Events", matchesField: true },
];
const PROVIDER_EXCLUSION_SETTINGS: SyncSetting[] = [
{ field: "excludeFocusTime", label: "Exclude Focus Time Events", matchesField: true },
{ field: "excludeOutOfOffice", label: "Exclude Out of Office Events", matchesField: true },
];
const PROVIDERS_WITH_EXTRA_SETTINGS = new Set(["google"]);
function patchSource(
store: ReturnType,
calendarId: string,
patch: Record,
) {
const swrKey = `/api/sources/${calendarId}`;
serializedPatch(
swrKey,
patch,
(mergedPatch) => {
return apiFetch(swrKey, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(mergedPatch),
});
},
() => {
fetcher(swrKey).then((serverState) => {
store.set(calendarDetailAtom, serverState);
});
},
);
}
function useSeedCalendarDetail(calendarId: string, calendar: CalendarDetail | undefined) {
const store = useStore();
useEffect(() => {
if (!calendar) return;
if (store.get(calendarDetailLoadedAtom) === calendarId) return;
store.set(calendarDetailAtom, calendar);
store.set(calendarDetailLoadedAtom, calendarId);
store.set(calendarDetailErrorAtom, null);
}, [calendarId, calendar, store]);
}
function CalendarDetailPage() {
const { accountId, calendarId } = Route.useParams();
const { data: account, isLoading: accountLoading, error: accountError, mutate: mutateAccount } = useSWR(`/api/accounts/${accountId}`);
const { data: calendar, isLoading: calendarLoading, error: calendarError } = useSWR(`/api/sources/${calendarId}`);
const { mutate: mutateCalendar } = useSWRConfig();
useSeedCalendarDetail(calendarId, calendar);
const isLoading = accountLoading || calendarLoading;
const error = accountError || calendarError;
if (error || isLoading || !account || !calendar) {
if (error) return { await Promise.all([mutateAccount(), mutateCalendar(`/api/sources/${calendarId}`)]); }} />;
return ;
}
const isPullCapable = canPull(calendar);
return (
{isPullCapable && (
<>
>
)}
{isPullCapable &&
}
{isPullCapable &&
}
);
}
function CalendarPrevNext({ calendarId }: { calendarId: string }) {
const { data: allCalendars } = useSWR("/api/sources");
const calendars = allCalendars ?? [];
const currentIndex = calendars.findIndex((c) => c.id === calendarId);
const prev = currentIndex > 0 ? calendars[currentIndex - 1] : null;
const next = currentIndex < calendars.length - 1 ? calendars[currentIndex + 1] : null;
useEffect(() => {
if (prev) preload(`/api/sources/${prev.id}`, fetcher);
if (next) preload(`/api/sources/${next.id}`, fetcher);
}, [prev, next]);
const toCalendar = (c: CalendarSource) => `/dashboard/accounts/${c.accountId}/${c.id}`;
return (
);
}
function CalendarHeader({ account }: { account: CalendarAccount }) {
const provider = useAtomValue(calendarProviderAtom);
const calendarType = useAtomValue(calendarTypeAtom);
return (
);
}
function CalendarTitle() {
const name = useAtomValue(calendarNameAtom);
return {name} ;
}
function RenameSection({ calendarId }: { calendarId: string }) {
return (
<>
>
);
}
function RenameItem({ calendarId }: { calendarId: string }) {
const store = useStore();
const name = useAtomValue(calendarNameAtom);
return (
{
track(ANALYTICS_EVENTS.calendar_renamed);
store.set(calendarDetailAtom, (prev) => (prev ? { ...prev, name: newName } : prev));
patchSource(store, calendarId, { name: newName });
}}
>
);
}
function RenameItemValue() {
const name = useAtomValue(calendarNameAtom);
const variant = use(MenuVariantContext);
const disabled = use(ItemDisabledContext);
return (
{name}
);
}
function DestinationsSeed({ calendarId }: { calendarId: string }) {
const { data } = useSWR<{ destinationIds: string[] }>(
`/api/sources/${calendarId}/destinations`,
);
const store = useStore();
useEffect(() => {
store.set(destinationIdsAtom, new Set(data?.destinationIds));
}, [calendarId, data, store]);
return null;
}
function DestinationsSection({ calendarId }: { calendarId: string }) {
const { data: allCalendars } = useSWR("/api/sources");
const { data: entitlements } = useEntitlements();
const atLimit = !canAddMore(entitlements?.mappings);
const pushCalendars = useMemo(
() => (allCalendars ?? []).filter((calendar) => canPush(calendar) && calendar.id !== calendarId),
[allCalendars, calendarId],
);
return (
<>
{pushCalendars.length === 0 ? (
No destination calendars available
) : (
pushCalendars.map((calendar) => (
))
)}
{atLimit && Mapping limit reached. }
>
);
}
function DestinationCheckboxItem({
calendarId,
destinationId,
name,
provider,
calendarType,
}: {
calendarId: string;
destinationId: string;
name: string;
provider: string;
calendarType: string;
}) {
const store = useStore();
const variant = use(MenuVariantContext);
const { mutate } = useSWRConfig();
const { data: entitlements } = useEntitlements();
const { adjustMappingCount, revalidateEntitlements } = useMutateEntitlements();
const checkedAtom = useMemo(() => selectDestinationInclusion(destinationId), [destinationId]);
const checked = useAtomValue(checkedAtom);
const atLimit = !canAddMore(entitlements?.mappings);
const disabled = atLimit && !checked;
const handleClick = () => {
if (disabled) return;
const currentIds = store.get(destinationIdsAtom);
const willCheck = !currentIds.has(destinationId);
track(ANALYTICS_EVENTS.destination_toggled, { enabled: willCheck });
const updatedSet = new Set(currentIds);
if (willCheck) {
updatedSet.add(destinationId);
adjustMappingCount(1);
} else {
updatedSet.delete(destinationId);
adjustMappingCount(-1);
}
store.set(destinationIdsAtom, updatedSet);
const swrKey = `/api/sources/${calendarId}/destinations`;
serializedCall(swrKey, () => {
const latestIds = Array.from(store.get(destinationIdsAtom));
return mutate(
swrKey,
async () => {
await apiFetch(swrKey, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ calendarIds: latestIds }),
});
return { destinationIds: latestIds };
},
{
optimisticData: { destinationIds: latestIds },
rollbackOnError: true,
revalidate: false,
},
).catch(() => {
void mutate(swrKey);
}).finally(() => {
void revalidateEntitlements();
});
});
};
return (
{name}
);
}
function DestinationCheckboxIndicator({ destinationId }: { destinationId: string }) {
const checkedAtom = useMemo(() => selectDestinationInclusion(destinationId), [destinationId]);
const checked = useAtomValue(checkedAtom);
const variant = use(MenuVariantContext);
return (
{checked && }
);
}
function SyncSettingsSection({ calendarId }: { calendarId: string }) {
const { data: entitlements } = useEntitlements();
const locked = entitlements ? !entitlements.canUseEventFilters : false;
return (
<>
Choose which event details are synced to destination calendars. Use {"{{calendar_name}}"} or {"{{event_name}}"} in text fields for dynamic values.>}
/>
{SYNC_SETTINGS.map((setting) => (
))}
>
);
}
function SyncEventNameDisabledProvider({ locked, children }: { locked: boolean; children: React.ReactNode }) {
const excludeEventName = useAtomValue(excludeEventNameAtom);
return {children} ;
}
function SyncEventNameTemplateItem({ calendarId, locked }: { calendarId: string; locked: boolean }) {
const store = useStore();
const customEventName = useAtomValue(customEventNameAtom);
return (
(
)}
onCommit={(customEventName) => {
store.set(calendarDetailAtom, (prev) => (prev ? { ...prev, customEventName } : prev));
patchSource(store, calendarId, { customEventName });
}}
>
);
}
function SyncEventNameTemplateInput({ template }: { template: string }) {
return ;
}
const TEMPLATE_VARIABLES = { calendar_name: "Calendar Name", event_name: "Event Name" };
function SyncEventNameTemplateValue() {
const customEventName = useAtomValue(customEventNameAtom);
const excludeEventName = useAtomValue(excludeEventNameAtom);
const disabled = !excludeEventName;
const template = customEventName || "{{event_name}}";
return (
);
}
function SyncEventNameToggle({ calendarId, locked }: { calendarId: string; locked: boolean }) {
const store = useStore();
const variant = use(MenuVariantContext);
const handleClick = () => {
if (locked) return;
const current = store.get(calendarDetailAtom);
if (!current) return;
const patch = current.excludeEventName
? { excludeEventName: false, customEventName: "{{event_name}}" }
: { excludeEventName: true, customEventName: "{{calendar_name}}" };
track(ANALYTICS_EVENTS.calendar_setting_toggled, { field: "excludeEventName", enabled: !current.excludeEventName });
store.set(calendarDetailAtom, (prev) => (prev ? { ...prev, ...patch } : prev));
patchSource(store, calendarId, patch);
};
return (
Sync Event Name
);
}
function SyncEventNameToggleIndicator({ disabled }: { disabled: boolean }) {
const excludeEventName = useAtomValue(excludeEventNameAtom);
const variant = use(MenuVariantContext);
const checked = !excludeEventName;
return (
);
}
function ExclusionsSection({ calendarId, provider }: { calendarId: string; provider: string }) {
const { data: entitlements } = useEntitlements();
const locked = entitlements ? !entitlements.canUseEventFilters : false;
const hasExtraSettings = PROVIDERS_WITH_EXTRA_SETTINGS.has(provider);
const exclusionSettings = hasExtraSettings
? [...EXCLUSION_SETTINGS, ...PROVIDER_EXCLUSION_SETTINGS]
: EXCLUSION_SETTINGS;
return (
<>
{exclusionSettings.map((setting) => (
))}
>
);
}
function ExcludeFieldToggle({
calendarId,
field,
label,
matchesField,
locked = false,
}: {
calendarId: string;
field: ExcludeField;
label: string;
matchesField: boolean;
locked?: boolean;
}) {
const store = useStore();
const variant = use(MenuVariantContext);
const handleClick = () => {
if (locked) return;
const current = store.get(calendarDetailAtom);
if (!current) return;
const newValue = !current[field];
track(ANALYTICS_EVENTS.calendar_setting_toggled, { field, enabled: newValue });
store.set(calendarDetailAtom, (prev) => (prev ? { ...prev, [field]: newValue } : prev));
patchSource(store, calendarId, { [field]: newValue });
};
return (
{label}
);
}
function ExcludeFieldToggleIndicator({ field, matchesField, disabled }: { field: ExcludeField; matchesField: boolean; disabled: boolean }) {
const raw = useAtomValue(excludeFieldAtoms[field]);
const variant = use(MenuVariantContext);
const checked = matchesField ? raw : !raw;
return (
);
}
function CalendarInfoSection({ account, accountId }: { account: CalendarAccount; accountId: string }) {
const calendar = useAtomValue(calendarDetailAtom);
if (!calendar) return null;
return (
<>
{calendar.originalName && (
)}
{calendar.url && (
)}
{calendar.calendarUrl && (
)}
>
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.index.tsx
================================================
import { useEffect, useMemo, useState, useTransition } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import useSWR, { preload, useSWRConfig } from "swr";
import Calendar from "lucide-react/dist/esm/icons/calendar";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Pagination, PaginationPrevious, PaginationNext } from "@/components/ui/primitives/pagination";
import { RouteShell } from "@/components/ui/shells/route-shell";
import { Text } from "@/components/ui/primitives/text";
import { MetadataRow } from "@/features/dashboard/components/metadata-row";
import { fetcher, apiFetch } from "@/lib/fetcher";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { formatDate } from "@/lib/time";
import { invalidateAccountsAndSources } from "@/lib/swr";
import type { CalendarAccount, CalendarSource } from "@/types/api";
import {
NavigationMenu,
NavigationMenuEmptyItem,
NavigationMenuButtonItem,
NavigationMenuLinkItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
NavigationMenuItemTrailing,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import { DeleteConfirmation } from "@/components/ui/primitives/delete-confirmation";
import { DashboardSection } from "@/components/ui/primitives/dashboard-heading";
import { pluralize } from "@/lib/pluralize";
import { resolveErrorMessage } from "@/utils/errors";
export const Route = createFileRoute(
"/(dashboard)/dashboard/accounts/$accountId/",
)({
component: AccountDetailPage,
});
function CalendarList({ calendars, accountId }: { calendars: CalendarSource[]; accountId: string }) {
if (calendars.length === 0) {
return No calendars ;
}
return calendars.map((calendar) => (
preload(`/api/sources/${calendar.id}`, fetcher)}
>
{calendar.name}
));
}
function AccountDetailPage() {
const { accountId } = Route.useParams();
const navigate = useNavigate();
const { mutate: globalMutate } = useSWRConfig();
const { data: account, isLoading: accountLoading, error: accountError } = useSWR(
`/api/accounts/${accountId}`,
);
const { data: allCalendars, isLoading: calendarsLoading, error: calendarsError } = useSWR(
"/api/sources",
);
const [deleteOpen, setDeleteOpen] = useState(false);
const [isDeleting, startDeleteTransition] = useTransition();
const [deleteError, setDeleteError] = useState(null);
const isLoading = accountLoading || calendarsLoading;
const error = accountError || calendarsError;
const handleConfirmDelete = () => {
setDeleteError(null);
startDeleteTransition(async () => {
try {
await apiFetch(`/api/accounts/${accountId}`, { method: "DELETE" });
if (account) {
track(ANALYTICS_EVENTS.calendar_account_deleted, { provider: account.provider });
}
await invalidateAccountsAndSources(globalMutate, `/api/accounts/${accountId}`);
navigate({ to: "/dashboard" });
} catch (err) {
setDeleteError(resolveErrorMessage(err, "Failed to delete account."));
}
});
};
if (error || isLoading || !account) {
if (error) return { await invalidateAccountsAndSources(globalMutate, `/api/accounts/${accountId}`); }} />;
return ;
}
const calendars = (allCalendars ?? []).filter(
(calendar) => calendar.accountId === accountId,
);
return (
setDeleteOpen(true)}>
Delete Account
This account has {pluralize(calendars.length, "calendar")} attached to it, choose a calendar below to view more details and configure it.>}
/>
{deleteError && {deleteError} }
);
}
function AccountPrevNext({ accountId }: { accountId: string }) {
const { data: accounts } = useSWR("/api/accounts");
const currentIndex = useMemo(
() => (accounts ?? []).findIndex((a) => a.id === accountId),
[accounts, accountId],
);
const prev = accounts && currentIndex > 0 ? accounts[currentIndex - 1] : null;
const next = accounts && currentIndex < accounts.length - 1 ? accounts[currentIndex + 1] : null;
useEffect(() => {
if (prev) preload(`/api/accounts/${prev.id}`, fetcher);
if (next) preload(`/api/accounts/${next.id}`, fetcher);
}, [prev, next]);
if (!accounts || accounts.length <= 1) return null;
return (
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/$accountId.setup.tsx
================================================
import { useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import useSWR from "swr";
import { BackButton } from "@/components/ui/primitives/back-button";
import { UpgradeHint } from "@/components/ui/primitives/upgrade-hint";
import { DashboardSection } from "@/components/ui/primitives/dashboard-heading";
import { Button, LinkButton, ButtonText } from "@/components/ui/primitives/button";
import { apiFetch } from "@/lib/fetcher";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { useEntitlements, useMutateEntitlements, canAddMore } from "@/hooks/use-entitlements";
import type { CalendarSource } from "@/types/api";
import {
NavigationMenu,
NavigationMenuCheckboxItem,
NavigationMenuEmptyItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import { NavigationMenuEditableItem } from "@/components/ui/composites/navigation-menu/navigation-menu-editable";
import { ProviderIcon } from "@/components/ui/primitives/provider-icon";
import { RouteShell } from "@/components/ui/shells/route-shell";
import { canPull, canPush, getCalendarProvider } from "@/utils/calendars";
import { resolveUpdatedIds } from "@/utils/collections";
const VALID_STEPS = ["select", "rename", "destinations", "sources"] as const;
type SetupStep = (typeof VALID_STEPS)[number];
interface SetupSearch {
step?: SetupStep;
id?: string;
index?: number;
}
type MappingRoute = "destinations" | "sources";
type MappingResponseKey = "destinationIds" | "sourceIds";
type CalendarMappingData = Partial>;
function isValidStep(value: unknown): value is SetupStep {
const validSteps: readonly string[] = VALID_STEPS;
return typeof value === "string" && validSteps.includes(value);
}
function parseSearchIndex(value: unknown): number | undefined {
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
if (!Number.isInteger(parsed) || parsed < 0) return undefined;
return parsed;
}
export const Route = createFileRoute(
"/(dashboard)/dashboard/accounts/$accountId/setup",
)({
component: AccountSetupPage,
validateSearch: (search: Record): SetupSearch => {
return {
step: isValidStep(search.step) ? search.step : undefined,
id: typeof search.id === "string" ? search.id : undefined,
index: parseSearchIndex(search.index),
};
},
});
function parseSelectedIds(commaIds: string | undefined): Set {
if (!commaIds) return new Set();
return new Set(commaIds.split(",").filter(Boolean));
}
function resolveStepCalendarIndex(index: number, count: number): number {
if (count === 0) return 0;
if (index >= count) return count - 1;
return index;
}
interface SetupWorkflowData {
accountCalendars: CalendarSource[];
selectedCalendars: CalendarSource[];
destinationCalendars: CalendarSource[];
sourceCalendars: CalendarSource[];
destinationCalendarIndex: number;
sourceCalendarIndex: number;
}
function resolveSetupWorkflowData({
allCalendars,
accountId,
selectedIds,
requestedCalendarIndex,
}: {
allCalendars: CalendarSource[];
accountId: string;
selectedIds: Set;
requestedCalendarIndex: number;
}): SetupWorkflowData {
const accountCalendars = allCalendars.filter((calendar) => calendar.accountId === accountId);
const selectedCalendars = accountCalendars.filter((calendar) => selectedIds.has(calendar.id));
const destinationCalendars = selectedCalendars.filter(canPull);
const sourceCalendars = selectedCalendars.filter(canPush);
return {
accountCalendars,
selectedCalendars,
destinationCalendars,
sourceCalendars,
destinationCalendarIndex: resolveStepCalendarIndex(requestedCalendarIndex, destinationCalendars.length),
sourceCalendarIndex: resolveStepCalendarIndex(requestedCalendarIndex, sourceCalendars.length),
};
}
function resolveNextIndex(currentIndex: number, totalCount: number): number | undefined {
const nextIndex = currentIndex + 1;
if (nextIndex < totalCount) return nextIndex;
return undefined;
}
interface SetupStepActions {
advanceFromRename: () => void;
advanceFromDestinations: (currentIndex: number) => void;
advanceFromSources: (currentIndex: number) => void;
}
function createSetupStepActions({
destinationCount,
sourceCount,
navigateToStep,
navigateToDashboard,
}: {
destinationCount: number;
sourceCount: number;
navigateToStep: (step: SetupStep, index?: number) => void;
navigateToDashboard: () => void;
}): SetupStepActions {
const advanceToSources = () => {
if (sourceCount > 0) {
navigateToStep("sources", 0);
return;
}
navigateToDashboard();
};
return {
advanceFromRename: () => {
track(ANALYTICS_EVENTS.setup_step_completed, { step: "rename" });
if (destinationCount > 0) {
navigateToStep("destinations", 0);
return;
}
advanceToSources();
},
advanceFromDestinations: (currentIndex: number) => {
track(ANALYTICS_EVENTS.setup_step_completed, { step: "destinations" });
const nextIndex = resolveNextIndex(currentIndex, destinationCount);
if (nextIndex !== undefined) {
navigateToStep("destinations", nextIndex);
return;
}
advanceToSources();
},
advanceFromSources: (currentIndex: number) => {
track(ANALYTICS_EVENTS.setup_step_completed, { step: "sources" });
const nextIndex = resolveNextIndex(currentIndex, sourceCount);
if (nextIndex !== undefined) {
navigateToStep("sources", nextIndex);
return;
}
track(ANALYTICS_EVENTS.setup_completed);
navigateToDashboard();
},
};
}
function SetupStepContent({
step,
accountId,
allCalendars,
workflow,
mutateCalendars,
actions,
}: {
step: SetupStep;
accountId: string;
allCalendars: CalendarSource[];
workflow: SetupWorkflowData;
mutateCalendars: ReturnType>["mutate"];
actions: SetupStepActions;
}) {
if (step === "select") {
return (
);
}
if (step === "rename") {
return (
);
}
if (step === "destinations") {
const calendar = workflow.destinationCalendars[workflow.destinationCalendarIndex];
const onNext = () => actions.advanceFromDestinations(workflow.destinationCalendarIndex);
if (!calendar) return ;
return ;
}
const calendar = workflow.sourceCalendars[workflow.sourceCalendarIndex];
const onNext = () => actions.advanceFromSources(workflow.sourceCalendarIndex);
if (!calendar) return ;
return ;
}
function buildMappingData(responseKey: MappingResponseKey, ids: string[]): CalendarMappingData {
return { [responseKey]: ids };
}
function useCalendarMapping({
calendarId,
route,
responseKey,
}: {
calendarId?: string;
route: MappingRoute;
responseKey: MappingResponseKey;
}) {
const endpoint = calendarId ? `/api/sources/${calendarId}/${route}` : null;
const { data, mutate } = useSWR(endpoint);
const { adjustMappingCount, revalidateEntitlements } = useMutateEntitlements();
const selectedIds = new Set(data?.[responseKey] ?? []);
const handleToggle = async (targetCalendarId: string, checked: boolean) => {
if (!endpoint) return;
const currentIds = data?.[responseKey] ?? [];
const updatedIds = resolveUpdatedIds(currentIds, targetCalendarId, checked);
const mappingData = buildMappingData(responseKey, updatedIds);
const delta = checked ? 1 : -1;
adjustMappingCount(delta);
try {
await mutate(
async () => {
await apiFetch(endpoint, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ calendarIds: updatedIds }),
});
return mappingData;
},
{
optimisticData: mappingData,
rollbackOnError: true,
revalidate: false,
},
);
} catch {
adjustMappingCount(-delta);
} finally {
void revalidateEntitlements();
}
};
return { selectedIds, handleToggle };
}
function AccountSetupPage() {
const { accountId } = Route.useParams();
const search = Route.useSearch();
const navigate = useNavigate();
const step = search.step ?? "select";
const selectedIds = parseSelectedIds(search.id);
const requestedCalendarIndex = search.index ?? 0;
const { data, isLoading, error, mutate: mutateCalendars } = useSWR("/api/sources");
const allCalendars = data ?? [];
const workflow = resolveSetupWorkflowData({
allCalendars,
accountId,
selectedIds,
requestedCalendarIndex,
});
const navigateToStep = (nextStep: SetupStep, nextIndex?: number) => {
navigate({
to: "/dashboard/accounts/$accountId/setup",
params: { accountId },
search: { step: nextStep, id: search.id, index: nextIndex },
});
};
const actions = createSetupStepActions({
destinationCount: workflow.destinationCalendars.length,
sourceCount: workflow.sourceCalendars.length,
navigateToStep,
navigateToDashboard: () => navigate({ to: "/dashboard" }),
});
if (error || isLoading) {
if (error) return mutateCalendars()} />;
return ;
}
return (
);
}
function SelectSection({
accountId,
calendars,
}: {
accountId: string;
calendars: CalendarSource[];
}) {
const navigate = useNavigate();
const [localSelectedIds, setLocalSelectedIds] = useState>(new Set());
const handleToggle = (calendarId: string, checked: boolean) => {
setLocalSelectedIds((prev) => {
const next = new Set(prev);
if (checked) {
next.add(calendarId);
} else {
next.delete(calendarId);
}
return next;
});
};
const handleNext = () => {
track(ANALYTICS_EVENTS.setup_step_completed, { step: "select" });
navigate({
to: "/dashboard/accounts/$accountId/setup",
params: { accountId },
search: { step: "rename", id: [...localSelectedIds].join(",") },
});
};
return (
<>
{calendars.map((calendar) => (
handleToggle(calendar.id, checked)}
>
{calendar.name}
))}
Next
Skip
>
);
}
function RenameSection({
calendars,
mutateCalendars,
onNext,
}: {
calendars: CalendarSource[];
mutateCalendars: ReturnType>["mutate"];
onNext: () => void;
}) {
const handleRename = async (calendarId: string, name: string) => {
await mutateCalendars(
async (current) => {
await apiFetch(`/api/sources/${calendarId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
return current?.map((calendar) =>
calendar.id === calendarId ? { ...calendar, name } : calendar,
);
},
{
optimisticData: (current) =>
(current ?? []).map((calendar) =>
calendar.id === calendarId ? { ...calendar, name } : calendar,
),
rollbackOnError: true,
revalidate: false,
},
);
};
return (
<>
{calendars.map((calendar, index) => (
handleRename(calendar.id, name)}
/>
))}
Next
>
);
}
function EmptyStepSection({ heading, message, buttonLabel, onNext }: {
heading: string;
message: string;
buttonLabel: string;
onNext: () => void;
}) {
return (
<>
{buttonLabel}
>
);
}
function DestinationsSection({
calendar,
allCalendars,
onNext,
}: {
calendar: CalendarSource;
allCalendars: CalendarSource[];
onNext: () => void;
}) {
const { selectedIds, handleToggle } = useCalendarMapping({
calendarId: calendar.id,
route: "destinations",
responseKey: "destinationIds",
});
const { data: entitlements } = useEntitlements();
const atLimit = !canAddMore(entitlements?.mappings);
const pushCalendars = allCalendars.filter(
(candidate) => canPush(candidate) && candidate.id !== calendar.id,
);
return (
<>
Where should '{calendar.name}' send events?>}
description="Select which calendars should receive events from this calendar."
headingClassName="overflow-visible text-wrap whitespace-normal"
/>
{pushCalendars.length === 0 && (
No destination calendars available
)}
{pushCalendars.map((destination) => {
const checked = selectedIds.has(destination.id);
const disabled = atLimit && !checked;
return (
!disabled && handleToggle(destination.id, next)}
>
{destination.name}
);
})}
{atLimit && Mapping limit reached. }
Next
>
);
}
function SourcesSection({
calendar,
allCalendars,
onNext,
}: {
calendar: CalendarSource;
allCalendars: CalendarSource[];
onNext: () => void;
}) {
const { selectedIds, handleToggle } = useCalendarMapping({
calendarId: calendar.id,
route: "sources",
responseKey: "sourceIds",
});
const { data: entitlements } = useEntitlements();
const atLimit = !canAddMore(entitlements?.mappings);
const pullCalendars = allCalendars.filter(
(candidate) => canPull(candidate) && candidate.id !== calendar.id,
);
return (
<>
Where should '{calendar.name}' pull events from?>}
description="Select which calendars should send events to this calendar."
headingClassName="overflow-visible text-wrap whitespace-normal"
/>
{pullCalendars.length === 0 && (
No source calendars available
)}
{pullCalendars.map((source) => {
const checked = selectedIds.has(source.id);
const disabled = atLimit && !checked;
return (
!disabled && handleToggle(source.id, next)}
>
{source.name}
);
})}
{atLimit && Mapping limit reached. }
Next
>
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/accounts/route.tsx
================================================
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/(dashboard)/dashboard/accounts")({
component: AccountsLayout,
});
function AccountsLayout() {
return ;
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/connect/index.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import Calendar from "lucide-react/dist/esm/icons/calendar";
import LinkIcon from "lucide-react/dist/esm/icons/link";
import { BackButton } from "@/components/ui/primitives/back-button";
import { ANALYTICS_EVENTS } from "@/lib/analytics";
import { PremiumFeatureGate } from "@/components/ui/primitives/upgrade-hint";
import { useEntitlements, canAddMore } from "@/hooks/use-entitlements";
import {
NavigationMenu,
NavigationMenuLinkItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
NavigationMenuItemTrailing,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
export const Route = createFileRoute("/(dashboard)/dashboard/connect/")({
component: ConnectPage,
});
function ConnectPage() {
const { data: entitlements } = useEntitlements();
const atLimit = !canAddMore(entitlements?.accounts);
return (
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/connect/route.tsx
================================================
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/(dashboard)/dashboard/connect")({
component: ConnectLayout,
});
function ConnectLayout() {
return ;
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/events/index.tsx
================================================
import { useEffect, useRef, memo } from "react";
import { createFileRoute } from "@tanstack/react-router";
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import { BackButton } from "@/components/ui/primitives/back-button";
import { ErrorState } from "@/components/ui/primitives/error-state";
import { DashboardHeading1, DashboardHeading2 } from "@/components/ui/primitives/dashboard-heading";
import { Text } from "@/components/ui/primitives/text";
import { formatTime, formatTimeUntil, isEventPast, formatDayHeader } from "@/lib/time";
import { useEvents, type CalendarEvent } from "@/hooks/use-events";
import { cn } from "@/utils/cn";
export const Route = createFileRoute("/(dashboard)/dashboard/events/")({
component: EventsPage,
});
interface DayGroup {
label: string;
events: CalendarEvent[];
}
interface DaySectionProps {
label: string;
events: CalendarEvent[];
}
interface EventRowProps {
event: CalendarEvent;
}
interface LoadMoreSentinelProps {
isValidating: boolean;
hasMore: boolean;
onLoadMore: () => void;
}
const groupEventsByDay = (events: CalendarEvent[]): DayGroup[] => {
const groups = new Map();
for (const event of events) {
const key = event.startTime.toDateString();
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(event);
}
return [...groups.entries()].map(([key, dayEvents]) => ({
label: formatDayHeader(new Date(key)),
events: dayEvents,
}));
};
function EventsPage() {
return (
);
}
function EventsContent() {
const { events, error, isLoading, isValidating, hasMore, loadMore } = useEvents();
const dayGroups = groupEventsByDay(events);
if (isLoading) return ;
if (error) return ;
return (
Events
View all of the events across all of your calendars.
{dayGroups.map((group) => (
))}
{hasMore && (
)}
);
}
function LoadMoreSentinel({ isValidating, hasMore, onLoadMore }: LoadMoreSentinelProps) {
const nodeRef = useRef(null);
useEffect(() => {
const node = nodeRef.current;
if (!node || isValidating || !hasMore) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) onLoadMore();
},
{ rootMargin: "200px" },
);
observer.observe(node);
return () => observer.disconnect();
}, [isValidating, hasMore, onLoadMore]);
return (
{isValidating && (
)}
);
}
function LoadingIndicator() {
return (
);
}
const areDaySectionPropsEqual = (prev: DaySectionProps, next: DaySectionProps): boolean => {
if (prev.label !== next.label) return false;
if (prev.events.length !== next.events.length) return false;
return prev.events.every((event, index) => event.id === next.events[index].id);
};
const DaySection = memo(function DaySection({ label, events }: DaySectionProps) {
return (
{label}
{events.map((event) => (
))}
);
}, areDaySectionPropsEqual);
function resolveEventRowClassName(past: boolean): string {
return cn("flex items-center justify-between gap-2 py-1.5", past && "line-through");
}
const EventRow = memo(function EventRow({ event }: EventRowProps) {
const past = isEventPast(event.endTime);
const startTime = formatTime(event.startTime);
const endTime = formatTime(event.endTime);
const timeUntil = formatTimeUntil(event.startTime);
return (
{event.calendarName}
from
{startTime}
to
{endTime}
{timeUntil}
);
});
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/feedback.tsx
================================================
import { useRef, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { BackButton } from "@/components/ui/primitives/back-button";
import { DashboardSection } from "@/components/ui/primitives/dashboard-heading";
import { Text } from "@/components/ui/primitives/text";
import { Button, ButtonText, LinkButton } from "@/components/ui/primitives/button";
import { apiFetch } from "@/lib/fetcher";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { resolveErrorMessage } from "@/utils/errors";
export const Route = createFileRoute("/(dashboard)/dashboard/feedback")({
component: FeedbackPage,
});
function FeedbackPage() {
const messageRef = useRef(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [submitted, setSubmitted] = useState(false);
const handleSubmit = async () => {
const message = messageRef.current?.value.trim();
if (!message) return;
setError(null);
setIsSubmitting(true);
try {
await apiFetch("/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, type: "feedback" }),
});
track(ANALYTICS_EVENTS.feedback_submitted);
setSubmitted(true);
} catch (err) {
setError(resolveErrorMessage(err, "Failed to submit feedback."));
} finally {
setIsSubmitting(false);
}
};
if (submitted) {
return (
Back to Dashboard
);
}
return (
{error && {error} }
{isSubmitting ? "Submitting..." : "Submit Feedback"}
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/ical.tsx
================================================
import { use, useMemo } from "react";
import { createFileRoute } from "@tanstack/react-router";
import useSWR from "swr";
import { atom, useAtomValue, useStore } from "jotai";
import Copy from "lucide-react/dist/esm/icons/copy";
import CheckIcon from "lucide-react/dist/esm/icons/check";
import { fetcher, apiFetch } from "@/lib/fetcher";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { serializedPatch } from "@/lib/serialized-mutate";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Input } from "@/components/ui/primitives/input";
import { Text } from "@/components/ui/primitives/text";
import { PremiumFeatureGate } from "@/components/ui/primitives/upgrade-hint";
import { DashboardSection } from "@/components/ui/primitives/dashboard-heading";
import { Button, ButtonIcon } from "@/components/ui/primitives/button";
import { ProviderIcon } from "@/components/ui/primitives/provider-icon";
import {
NavigationMenu,
NavigationMenuCheckboxItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
NavigationMenuToggleItem,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import {
NavigationMenuEditableTemplateItem,
} from "@/components/ui/composites/navigation-menu/navigation-menu-editable";
import { TemplateText } from "@/components/ui/primitives/template-text";
import { ItemDisabledContext, MenuVariantContext } from "@/components/ui/composites/navigation-menu/navigation-menu.contexts";
import {
DISABLED_LABEL_TONE,
LABEL_TONE,
} from "@/components/ui/composites/navigation-menu/navigation-menu.styles";
import {
icalSourceInclusionAtom,
selectSourceInclusion,
} from "@/state/ical-sources";
import {
feedSettingsAtom,
feedSettingsLoadedAtom,
feedSettingAtoms,
customEventNameAtom,
includeEventNameAtom,
} from "@/state/ical-feed-settings";
import type { FeedSettingToggleKey } from "@/state/ical-feed-settings";
import type { CalendarSource } from "@/types/api";
import { useEntitlements } from "@/hooks/use-entitlements";
type ICalTokenResponse = {
token: string;
icalUrl: string | null;
};
interface FeedSettings {
includeEventName: boolean;
includeEventDescription: boolean;
includeEventLocation: boolean;
excludeAllDayEvents: boolean;
customEventName: string;
}
export const Route = createFileRoute("/(dashboard)/dashboard/ical")({
component: ICalPage,
});
function patchFeedSettings(
store: ReturnType,
patch: Record,
) {
const swrKey = "/api/ical/settings";
serializedPatch(
swrKey,
patch,
(mergedPatch) => {
return apiFetch(swrKey, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(mergedPatch),
});
},
() => {
fetcher(swrKey).then((serverState) => {
store.set(feedSettingsAtom, serverState);
});
},
);
}
function ICalPage() {
const { data: entitlements } = useEntitlements();
const locked = entitlements ? !entitlements.canCustomizeIcalFeed : false;
return (
Choose which event details are included in your iCal feed. Use {"{{calendar_name}}"} or {"{{event_name}}"} in text fields for dynamic values.>}
/>
);
}
function ICalLinkSection() {
const { data } = useSWR("/api/ical/token", fetcher);
return (
);
}
const copiedAtom = atom(false);
function CopyButton({ value }: { value: string | null }) {
const store = useStore();
const handleCopy = async () => {
if (!value) return;
await navigator.clipboard.writeText(value);
track(ANALYTICS_EVENTS.ical_link_copied);
store.set(copiedAtom, true);
setTimeout(() => store.set(copiedAtom, false), 2000);
};
return (
);
}
function CopyIcon() {
const copied = useAtomValue(copiedAtom);
return copied ? : ;
}
function FeedSettingsSeed() {
const { data: settings } = useSWR("/api/ical/settings", fetcher);
const store = useStore();
if (settings && !store.get(feedSettingsLoadedAtom)) {
store.set(feedSettingsAtom, settings);
store.set(feedSettingsLoadedAtom, true);
}
return null;
}
function FeedSettingsToggles({ locked }: { locked: boolean }) {
const loaded = useAtomValue(feedSettingsLoadedAtom);
if (!loaded) return null;
return (
);
}
const TEMPLATE_VARIABLES = { event_name: "Event Name", calendar_name: "Calendar Name" };
function EventNameDisabledProvider({ locked, children }: { locked: boolean; children: React.ReactNode }) {
const includeEventName = useAtomValue(includeEventNameAtom);
return {children} ;
}
function EventNameTemplateItem({ locked }: { locked: boolean }) {
const store = useStore();
const eventName = useAtomValue(customEventNameAtom);
return (
(
)}
onCommit={(customEventName) => {
store.set(feedSettingsAtom, (prev) => ({ ...prev, customEventName }));
patchFeedSettings(store, { customEventName });
}}
>
);
}
function EventNameTemplateValue() {
const customEventName = useAtomValue(customEventNameAtom);
const includeEventName = useAtomValue(includeEventNameAtom);
const disabled = use(ItemDisabledContext);
const variant = use(MenuVariantContext);
const template = customEventName || "{{event_name}}";
return (
);
}
function EventNameToggle({ locked }: { locked: boolean }) {
const store = useStore();
const checked = useAtomValue(includeEventNameAtom);
const handleClick = () => {
if (locked) return;
const currentlyIncluded = store.get(feedSettingsAtom).includeEventName;
if (currentlyIncluded) {
const patch = { includeEventName: false, customEventName: "Busy" };
track(ANALYTICS_EVENTS.ical_setting_toggled, { field: "includeEventName", enabled: false });
store.set(feedSettingsAtom, (prev) => ({ ...prev, ...patch }));
patchFeedSettings(store, patch);
} else {
const patch = { includeEventName: true, customEventName: "{{event_name}}" };
track(ANALYTICS_EVENTS.ical_setting_toggled, { field: "includeEventName", enabled: true });
store.set(feedSettingsAtom, (prev) => ({ ...prev, ...patch }));
patchFeedSettings(store, patch);
}
};
return (
handleClick()}
>
Include Event Name
);
}
function FeedSettingToggle({
field,
label,
locked,
}: {
field: FeedSettingToggleKey;
label: string;
locked: boolean;
}) {
const store = useStore();
const checked = useAtomValue(feedSettingAtoms[field]);
const handleClick = () => {
if (locked) return;
const current = store.get(feedSettingsAtom)[field];
const newValue = !current;
track(ANALYTICS_EVENTS.ical_setting_toggled, { field, enabled: newValue });
store.set(feedSettingsAtom, (prev) => ({ ...prev, [field]: newValue }));
patchFeedSettings(store, { [field]: newValue });
};
return (
handleClick()}
>
{label}
);
}
function SourceSelectionSection() {
const { data: sources } = useSWR("/api/sources", fetcher);
const store = useStore();
if (sources && Object.keys(store.get(icalSourceInclusionAtom)).length === 0) {
const map: Record = {};
for (const source of sources) {
map[source.id] = source.includeInIcalFeed;
}
store.set(icalSourceInclusionAtom, map);
}
const pullSources = useMemo(
() => sources?.filter((source) => source.capabilities.includes("pull")),
[sources],
);
if (!pullSources || pullSources.length === 0) {
return (
);
}
return (
{pullSources.map((source) => (
))}
);
}
function SourceCheckboxItem({
sourceId,
name,
provider,
calendarType,
}: {
sourceId: string;
name: string;
provider: string;
calendarType: string;
}) {
const store = useStore();
const checkedAtom = useMemo(() => selectSourceInclusion(sourceId), [sourceId]);
const checked = useAtomValue(checkedAtom);
const handleCheckedChange = (nextChecked: boolean) => {
track(ANALYTICS_EVENTS.ical_source_toggled, { enabled: nextChecked });
store.set(icalSourceInclusionAtom, (prev) => ({ ...prev, [sourceId]: nextChecked }));
const sourceKey = `/api/sources/${sourceId}`;
serializedPatch(sourceKey, { includeInIcalFeed: nextChecked }, (mergedPatch) => {
return apiFetch(sourceKey, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(mergedPatch),
});
});
};
return (
{name}
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/index.tsx
================================================
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import useSWR, { preload } from "swr";
import { AnimatedReveal } from "@/components/ui/primitives/animated-reveal";
import Calendar from "lucide-react/dist/esm/icons/calendar";
import CalendarPlus from "lucide-react/dist/esm/icons/calendar-plus";
import CalendarDays from "lucide-react/dist/esm/icons/calendar-days";
import Link2 from "lucide-react/dist/esm/icons/link-2";
import Settings from "lucide-react/dist/esm/icons/settings";
import LogOut from "lucide-react/dist/esm/icons/log-out";
import MessageSquare from "lucide-react/dist/esm/icons/message-square";
import Bug from "lucide-react/dist/esm/icons/bug";
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import User from "lucide-react/dist/esm/icons/user";
import { ErrorState } from "@/components/ui/primitives/error-state";
import { signOut } from "@/lib/auth";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { fetcher } from "@/lib/fetcher";
import KeeperLogo from "@/assets/keeper.svg?react";
import { EventGraph } from "@/features/dashboard/components/event-graph";
import { ProviderIcon } from "@/components/ui/primitives/provider-icon";
import type { CalendarAccount, CalendarSource } from "@/types/api";
import {
NavigationMenu,
NavigationMenuButtonItem,
NavigationMenuLinkItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
NavigationMenuItemTrailing,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import { NavigationMenuPopover } from "@/components/ui/composites/navigation-menu/navigation-menu-popover";
import { Text } from "@/components/ui/primitives/text";
import { ProviderIconStack } from "@/components/ui/primitives/provider-icon-stack";
import { pluralize } from "@/lib/pluralize";
import { useAnimatedSWR } from "@/hooks/use-animated-swr";
import { SyncStatus } from "@/features/dashboard/components/sync-status";
export const Route = createFileRoute("/(dashboard)/dashboard/")({
component: DashboardPage,
});
function DashboardPage() {
const navigate = useNavigate();
const handleLogout = async () => {
track(ANALYTICS_EVENTS.logout);
await signOut();
navigate({ to: "/" });
};
return (
Import Calendars
Submit Feedback
Report a Problem
Settings
Logout
);
}
function CalendarsMenu() {
const { data: calendarsData, shouldAnimate: animateCalendars, isLoading: calendarsLoading, error, mutate: mutateCalendars } = useAnimatedSWR("/api/sources");
const calendars = calendarsData ?? [];
const { data: eventCountData, error: eventCountError } = useSWR<{ count: number }>("/api/events/count");
const eventCount = eventCountError ? undefined : eventCountData?.count;
return (
{calendars.length > 0 ? "Calendars" : "No Calendars"}
>
}
>
{error && mutateCalendars()} />}
{calendarsLoading && (
)}
{calendars.map((calendar) => (
{
preload(`/api/accounts/${calendar.accountId}`, fetcher);
preload(`/api/sources/${calendar.id}`, fetcher);
}}
>
{calendar.name}
{calendar.accountLabel}
))}
0} skipInitial={!animateCalendars}>
View Events
{eventCount != null && {pluralize(eventCount, "event")} }
iCal Link
);
}
function AccountsPopover() {
const { data: accountsData, isLoading: accountsLoading, error: accountsError, mutate: mutateAccounts } = useAnimatedSWR("/api/accounts");
const accounts = accountsData ?? [];
return (
{accounts.length > 0 ? "Calendar Sources" : "No Calendar Sources"}
>
}
>
{accountsError && mutateAccounts()} />}
{accountsLoading && (
)}
{accounts.map((account) => (
{account.accountLabel}
))}
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/integrations/index.tsx
================================================
import { createFileRoute, redirect } from "@tanstack/react-router";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
interface SearchParams {
error?: string;
}
function parseSearchError(value: unknown): string | undefined {
if (typeof value === "string") return value;
return undefined;
}
export const Route = createFileRoute("/(dashboard)/dashboard/integrations/")({
component: OAuthCallbackErrorPage,
validateSearch: (search: Record): SearchParams => ({
error: parseSearchError(search.error),
}),
beforeLoad: ({ search }) => {
if (!search.error) {
throw redirect({ to: "/dashboard" });
}
},
});
function OAuthCallbackErrorPage() {
const { error } = Route.useSearch();
return (
Connection failed
{error}
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/report.tsx
================================================
import { useRef, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { BackButton } from "@/components/ui/primitives/back-button";
import { DashboardSection } from "@/components/ui/primitives/dashboard-heading";
import { Text } from "@/components/ui/primitives/text";
import { Button, ButtonText, LinkButton } from "@/components/ui/primitives/button";
import { Checkbox } from "@/components/ui/primitives/checkbox";
import { apiFetch } from "@/lib/fetcher";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { resolveErrorMessage } from "@/utils/errors";
export const Route = createFileRoute("/(dashboard)/dashboard/report")({
component: ReportPage,
});
function ReportPage() {
const messageRef = useRef(null);
const [wantsFollowUp, setWantsFollowUp] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [submitted, setSubmitted] = useState(false);
const handleSubmit = async () => {
const message = messageRef.current?.value.trim();
if (!message) return;
setError(null);
setIsSubmitting(true);
try {
await apiFetch("/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, type: "report", wantsFollowUp }),
});
track(ANALYTICS_EVENTS.report_submitted, { wants_follow_up: wantsFollowUp });
setSubmitted(true);
} catch (err) {
setError(resolveErrorMessage(err, "Failed to submit report."));
} finally {
setIsSubmitting(false);
}
};
if (submitted) {
return (
Back to Dashboard
);
}
return (
{error && {error} }
{isSubmitting ? "Submitting..." : "Submit Report"}
Notify me when this is addressed
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/api-tokens.tsx
================================================
import { useRef, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import Check from "lucide-react/dist/esm/icons/check";
import Copy from "lucide-react/dist/esm/icons/copy";
import Gauge from "lucide-react/dist/esm/icons/gauge";
import KeySquare from "lucide-react/dist/esm/icons/key-square";
import Plus from "lucide-react/dist/esm/icons/plus";
import { Button, ButtonIcon, ButtonText } from "@/components/ui/primitives/button";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Input } from "@/components/ui/primitives/input";
import {
useApiTokens,
createApiToken,
deleteApiToken,
} from "@/hooks/use-api-tokens";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import type { ApiToken } from "@/hooks/use-api-tokens";
import { useEntitlements } from "@/hooks/use-entitlements";
import {
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalTitle,
} from "@/components/ui/primitives/modal";
import {
NavigationMenu,
NavigationMenuButtonItem,
NavigationMenuItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
NavigationMenuItemTrailing,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import { ErrorState } from "@/components/ui/primitives/error-state";
import { Text } from "@/components/ui/primitives/text";
import { resolveErrorMessage } from "@/utils/errors";
export const Route = createFileRoute(
"/(dashboard)/dashboard/settings/api-tokens",
)({
component: ApiTokensPage,
});
function CopyTokenIcon({ copied }: { copied: boolean }) {
if (copied) {
return ;
}
return ;
}
function resolveApiLimitLabel(plan: string | null): string {
if (plan === "pro") {
return "Unlimited";
}
return "25 calls/day";
}
function ApiTokensPage() {
const { data: entitlements } = useEntitlements();
const { data: tokens = [], error, mutate } = useApiTokens();
const [deleteTarget, setDeleteTarget] = useState(null);
const [mutationError, setMutationError] = useState(null);
const [createOpen, setCreateOpen] = useState(false);
const [revealedToken, setRevealedToken] = useState(null);
const [copied, setCopied] = useState(false);
const handleDelete = async () => {
if (!deleteTarget) return;
const targetId = deleteTarget.id;
setDeleteTarget(null);
setMutationError(null);
try {
await mutate(
async (current) => {
await deleteApiToken(targetId);
return current?.filter((entry) => entry.id !== targetId) ?? [];
},
{
optimisticData: tokens.filter((entry) => entry.id !== targetId),
rollbackOnError: true,
revalidate: false,
},
);
track(ANALYTICS_EVENTS.api_token_deleted);
} catch (err) {
setMutationError(resolveErrorMessage(err, "Failed to delete token."));
}
};
const handleCopy = async () => {
if (!revealedToken) return;
await navigator.clipboard.writeText(revealedToken);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleCloseReveal = () => {
setRevealedToken(null);
setCopied(false);
};
return (
);
}
function CreateSubmitButton({ isCreating }: { isCreating: boolean }) {
if (isCreating) {
return (
Creating...
);
}
return (
Create
);
}
function CreateTokenButton({
onCreated,
onError,
createOpen,
setCreateOpen,
}: {
onCreated: (plainToken: string) => void;
onError: (error: string | null) => void;
createOpen: boolean;
setCreateOpen: (open: boolean) => void;
}) {
const nameRef = useRef(null);
const [isCreating, setIsCreating] = useState(false);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const name = nameRef.current?.value?.trim();
if (!name) return;
onError(null);
setIsCreating(true);
try {
const result = await createApiToken(name);
track(ANALYTICS_EVENTS.api_token_created);
setCreateOpen(false);
onCreated(result.token);
} catch (err) {
onError(resolveErrorMessage(err, "Failed to create token."));
setCreateOpen(false);
} finally {
setIsCreating(false);
}
};
return (
<>
setCreateOpen(true)}>
Create Token
>
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/change-password.tsx
================================================
import { useState, useTransition, type SubmitEvent } from "react";
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
import LoaderCircle from "lucide-react/dist/esm/icons/loader-circle";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { BackButton } from "@/components/ui/primitives/back-button";
import { Text } from "@/components/ui/primitives/text";
import { Divider } from "@/components/ui/primitives/divider";
import { Input } from "@/components/ui/primitives/input";
import { changePassword } from "@/lib/auth";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import { resolveErrorMessage } from "@/utils/errors";
export const Route = createFileRoute(
"/(dashboard)/dashboard/settings/change-password",
)({
loader: async ({ context }) => {
const capabilities = await fetchAuthCapabilitiesWithApi(context.fetchApi);
if (!capabilities.supportsChangePassword) {
throw redirect({ to: "/dashboard/settings" });
}
return capabilities;
},
component: ChangePasswordPage,
});
function resolveInputTone(error: string | null): "error" | "neutral" {
if (error) return "error";
return "neutral";
}
function ChangePasswordPage() {
const navigate = useNavigate();
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = (event: SubmitEvent) => {
event.preventDefault();
setError(null);
const formData = new FormData(event.currentTarget);
const current = formData.get("current");
const newPassword = formData.get("new");
const confirm = formData.get("confirm");
if (typeof current !== "string" || typeof newPassword !== "string" || typeof confirm !== "string") return;
if (newPassword !== confirm) {
setError("Passwords do not match.");
return;
}
startTransition(async () => {
try {
await changePassword(current, newPassword);
track(ANALYTICS_EVENTS.password_changed);
navigate({ to: "/dashboard/settings" });
} catch (err) {
setError(resolveErrorMessage(err, "Failed to change password."));
}
});
};
const inputTone = resolveInputTone(error);
return (
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/index.tsx
================================================
import { useCallback, useRef, useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import CreditCard from "lucide-react/dist/esm/icons/credit-card";
import KeyRound from "lucide-react/dist/esm/icons/key-round";
import KeySquare from "lucide-react/dist/esm/icons/key-square";
import Lock from "lucide-react/dist/esm/icons/lock";
import Mail from "lucide-react/dist/esm/icons/mail";
import Sparkles from "lucide-react/dist/esm/icons/sparkles";
import Cookie from "lucide-react/dist/esm/icons/cookie";
import Trash2 from "lucide-react/dist/esm/icons/trash-2";
import { pluralize } from "@/lib/pluralize";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { BackButton } from "@/components/ui/primitives/back-button";
import { useSession } from "@/hooks/use-session";
import { useApiTokens } from "@/hooks/use-api-tokens";
import { usePasskeys } from "@/hooks/use-passkeys";
import { useHasPassword } from "@/hooks/use-has-password";
import { Input } from "@/components/ui/primitives/input";
import { deleteAccount } from "@/lib/auth";
import {
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalTitle,
} from "@/components/ui/primitives/modal";
import {
NavigationMenu,
NavigationMenuButtonItem,
NavigationMenuItem,
NavigationMenuLinkItem,
NavigationMenuToggleItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
NavigationMenuItemTrailing,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import { resolveEffectiveConsent, setAnalyticsConsent, track, ANALYTICS_EVENTS } from "@/lib/analytics";
import { Text } from "@/components/ui/primitives/text";
import { resolveErrorMessage } from "@/utils/errors";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import { getCommercialMode } from "@/config/commercial";
import { useSubscription, fetchSubscriptionStateWithApi } from "@/hooks/use-subscription";
import { openCustomerPortal } from "@/utils/checkout";
async function loadSubscription(context: { runtimeConfig: { commercialMode: boolean }; fetchApi: (path: string, init?: RequestInit) => Promise }) {
if (!context.runtimeConfig.commercialMode) return undefined;
try {
return await fetchSubscriptionStateWithApi(context.fetchApi);
} catch {
return undefined;
}
}
export const Route = createFileRoute("/(dashboard)/dashboard/settings/")({
loader: async ({ context }) => {
const [authCapabilities, subscription] = await Promise.all([
fetchAuthCapabilitiesWithApi(context.fetchApi),
loadSubscription(context),
]);
return { authCapabilities, subscription };
},
component: SettingsPage,
});
function SettingsPage() {
const { authCapabilities, subscription: loaderSubscription } = Route.useLoaderData();
const { user } = useSession();
const navigate = useNavigate();
const passwordRef = useRef(null);
const { data: hasPassword = false } = useHasPassword();
const accountLabel = authCapabilities.credentialMode === "username" ? "Username" : "Email";
const accountValue =
authCapabilities.credentialMode === "username"
? (user?.username ?? user?.name ?? "")
: (user?.email ?? "");
const { data: apiTokens = [] } = useApiTokens();
const { data: passkeys = [] } = usePasskeys(authCapabilities.supportsPasskeys);
const { data: subscription, isLoading: subscriptionLoading } = useSubscription({
fallbackData: loaderSubscription,
});
const isPro = subscription?.plan === "pro";
const [isManaging, setIsManaging] = useState(false);
const { runtimeConfig } = Route.useRouteContext();
const [analyticsConsent, setAnalyticsConsentState] = useState(() =>
resolveEffectiveConsent(runtimeConfig.gdprApplies),
);
const handleAnalyticsToggle = useCallback((checked: boolean) => {
track(ANALYTICS_EVENTS.analytics_consent_changed, { granted: checked });
setAnalyticsConsent(checked);
setAnalyticsConsentState(checked);
}, []);
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleteError, setDeleteError] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
const handleManagePlan = async () => {
if (!isPro) {
navigate({ to: "/dashboard/upgrade" });
return;
}
setIsManaging(true);
try {
await openCustomerPortal();
} catch {
setIsManaging(false);
}
};
const handleDeleteAccount = async () => {
const password = hasPassword ? passwordRef.current?.value : undefined;
if (hasPassword && !password) return;
setDeleteError(null);
setIsDeleting(true);
try {
await deleteAccount(password);
track(ANALYTICS_EVENTS.account_deleted);
setDeleteOpen(false);
navigate({ to: "/login" });
} catch (err) {
setDeleteError(resolveErrorMessage(err, "Failed to delete account."));
} finally {
setIsDeleting(false);
}
};
return (
{accountLabel}
{accountValue}
{hasPassword && (
Change Password
)}
{authCapabilities.supportsPasskeys && (
Passkeys
{pluralize(passkeys.length, "passkey", "passkeys")}
)}
API Tokens
{pluralize(apiTokens.length, "token", "tokens")}
Analytics Cookies
{getCommercialMode() && (
{isPro ? : }
{isPro ? "Manage Plan" : "Upgrade to Pro"}
)}
setDeleteOpen(true)}>
Delete Account
Delete account?
This action is permanent and cannot be undone. All of your data, calendars, and connected accounts will be permanently deleted.
{hasPassword && (
)}
{deleteError && {deleteError} }
{isDeleting ? "Deleting..." : "Delete my account"}
setDeleteOpen(false)}>
Cancel
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/passkeys.tsx
================================================
import { useState } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
import KeyRound from "lucide-react/dist/esm/icons/key-round";
import Plus from "lucide-react/dist/esm/icons/plus";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { BackButton } from "@/components/ui/primitives/back-button";
import { usePasskeys, addPasskey, deletePasskey } from "@/hooks/use-passkeys";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
import type { Passkey } from "@/hooks/use-passkeys";
import { formatDateShort } from "@/lib/time";
import {
Modal,
ModalContent,
ModalDescription,
ModalFooter,
ModalTitle,
} from "@/components/ui/primitives/modal";
import {
NavigationMenu,
NavigationMenuButtonItem,
NavigationMenuItemIcon,
NavigationMenuItemLabel,
NavigationMenuItemTrailing,
} from "@/components/ui/composites/navigation-menu/navigation-menu-items";
import { ErrorState } from "@/components/ui/primitives/error-state";
import { Text } from "@/components/ui/primitives/text";
import { resolveErrorMessage } from "@/utils/errors";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
export const Route = createFileRoute(
"/(dashboard)/dashboard/settings/passkeys",
)({
loader: async ({ context }) => {
const capabilities = await fetchAuthCapabilitiesWithApi(context.fetchApi);
if (!capabilities.supportsPasskeys) {
throw redirect({ to: "/dashboard/settings" });
}
return capabilities;
},
component: PasskeysPage,
});
function PasskeysPage() {
const { data: passkeys = [], error, mutate } = usePasskeys();
const [deleteTarget, setDeleteTarget] = useState(null);
const [mutationError, setMutationError] = useState(null);
const handleDelete = async () => {
if (!deleteTarget) return;
const targetId = deleteTarget.id;
setDeleteTarget(null);
setMutationError(null);
try {
await mutate(
async (current) => {
await deletePasskey(targetId);
return current?.filter((entry) => entry.id !== targetId) ?? [];
},
{
optimisticData: passkeys.filter((entry) => entry.id !== targetId),
rollbackOnError: true,
revalidate: false,
},
);
track(ANALYTICS_EVENTS.passkey_deleted);
} catch (err) {
setMutationError(resolveErrorMessage(err, "Failed to delete passkey."));
}
};
return (
{error &&
mutate()} />}
{mutationError && {mutationError} }
{passkeys.map((passkey) => (
setDeleteTarget(passkey)}>
{passkey.name ?? "Passkey"}
{formatDateShort(passkey.createdAt)}
))}
{ if (!open) setDeleteTarget(null); }}>
Delete passkey?
This will remove "{deleteTarget?.name ?? "Passkey"}" from your account. You won't be able to use it to sign in anymore.
Delete
setDeleteTarget(null)}>
Cancel
);
}
function AddPasskeyButton({
mutate,
onError,
}: {
mutate: ReturnType["mutate"];
onError: (error: string | null) => void;
}) {
const [isMutating, setIsMutating] = useState(false);
const handleAdd = async () => {
onError(null);
setIsMutating(true);
try {
await addPasskey();
track(ANALYTICS_EVENTS.passkey_created);
await mutate();
} catch (err) {
onError(resolveErrorMessage(err, "Failed to add passkey."));
} finally {
setIsMutating(false);
}
};
return (
{isMutating ? "Working..." : "Add Passkey"}
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/settings/route.tsx
================================================
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/(dashboard)/dashboard/settings")({
component: SettingsLayout,
});
function SettingsLayout() {
return ;
}
================================================
FILE: applications/web/src/routes/(dashboard)/dashboard/upgrade/index.tsx
================================================
import { useState, useTransition } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { BackButton } from "@/components/ui/primitives/back-button";
import { DashboardHeading1, DashboardHeading3 } from "@/components/ui/primitives/dashboard-heading";
import { Text } from "@/components/ui/primitives/text";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import {
UpgradeCard,
UpgradeCardSection,
UpgradeCardToggle,
UpgradeCardFeature,
UpgradeCardFeatureIcon,
UpgradeCardActions,
} from "@/features/dashboard/components/upgrade-card";
import Check from "lucide-react/dist/esm/icons/check";
import { HttpError } from "@/lib/fetcher";
import { track, ANALYTICS_EVENTS, reportPurchaseConversion } from "@/lib/analytics";
import {
fetchSubscriptionStateWithApi,
useSubscription,
} from "@/hooks/use-subscription";
import { openCheckout, openCustomerPortal } from "@/utils/checkout";
import { getPlans } from "@/config/plans";
import type { PlanConfig } from "@/config/plans";
import { resolveUpgradeRedirect } from "@/lib/route-access-guards";
import type { PublicRuntimeConfig } from "@/lib/runtime-config";
export const Route = createFileRoute("/(dashboard)/dashboard/upgrade/")({
beforeLoad: ({ context }) => {
if (!context.runtimeConfig.commercialMode) {
throw redirect({ to: "/dashboard" });
}
},
loader: async ({ context }) => {
const sessionRedirect = resolveUpgradeRedirect(
context.auth.hasSession(),
null,
);
if (sessionRedirect) {
throw redirect({ to: sessionRedirect });
}
try {
const subscription = await fetchSubscriptionStateWithApi(context.fetchApi);
const redirectTarget = resolveUpgradeRedirect(
true,
subscription.plan,
);
if (redirectTarget) {
throw redirect({ to: redirectTarget });
}
return { subscription };
} catch (error) {
if (error instanceof HttpError && error.status === 401) {
const redirectTarget = resolveUpgradeRedirect(false, null);
throw redirect({ to: redirectTarget ?? "/login" });
}
throw error;
}
},
component: UpgradePage,
});
function resolveProPlan(runtimeConfig: PublicRuntimeConfig): PlanConfig {
const plan = getPlans(runtimeConfig).find((candidatePlan) => candidatePlan.id === "pro");
if (!plan) {
throw new Error("Missing pro plan configuration.");
}
return plan;
}
function UpgradePage() {
const { runtimeConfig } = Route.useRouteContext();
const proPlan = resolveProPlan(runtimeConfig);
const { subscription: loaderSubscription } = Route.useLoaderData();
const { data: subscription, isLoading, mutate } = useSubscription({
fallbackData: loaderSubscription,
});
const [yearly, setYearly] = useState(false);
const handleBillingToggle = (checked: boolean) => {
const interval = checked ? "annual" : "monthly";
track(ANALYTICS_EVENTS.upgrade_billing_toggled, { interval });
setYearly(checked);
};
const [isPending, startTransition] = useTransition();
const currentPlan = subscription?.plan ?? "free";
const currentInterval = subscription?.interval;
const isCurrent = currentPlan === "pro";
const isCurrentInterval =
(currentInterval === "year" && yearly) ||
(currentInterval === "month" && !yearly);
const price = yearly ? (proPlan.yearlyPrice / 12).toFixed(2) : proPlan.monthlyPrice.toFixed(2);
const period = yearly ? "per month, billed annually" : "per month";
const productId = yearly ? proPlan.yearlyProductId : proPlan.monthlyProductId;
const handleUpgrade = () => {
if (!productId) return;
track(ANALYTICS_EVENTS.upgrade_started);
startTransition(async () => {
await openCheckout(productId, {
onSuccess: () => {
reportPurchaseConversion(runtimeConfig);
mutate();
},
});
});
};
const handleManage = () => {
track(ANALYTICS_EVENTS.plan_managed);
startTransition(async () => {
await openCustomerPortal();
});
};
const busy = isLoading || isPending;
const mode = !isCurrent ? "upgrade" : isCurrentInterval ? "manage" : "switch-interval";
return (
Upgrade to Pro
${price}
{period}
Annual billing
For power users who need fast syncs, advanced feed controls, and unlimited syncing. Thank you for supporting this project.
{proPlan.features.map((feature) => (
{feature}
))}
);
}
type UpgradeActionProps = {
isLoading: boolean;
onUpgrade: () => void;
onManage: () => void;
mode: "upgrade" | "manage" | "switch-interval";
};
function UpgradeAction({ mode, isLoading, onUpgrade, onManage }: UpgradeActionProps) {
const base = "w-full justify-center border-transparent bg-white text-neutral-900 hover:bg-neutral-100";
const label =
mode === "manage" ? "Manage Subscription" :
mode === "switch-interval" ? "Switch Billing Period" :
isLoading ? "Loading..." : "Upgrade to Pro";
const handler = mode === "upgrade" ? onUpgrade : onManage;
return (
{label}
);
}
================================================
FILE: applications/web/src/routes/(dashboard)/route.tsx
================================================
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { useAtomValue } from "jotai";
import { AnimatePresence, LazyMotion } from "motion/react";
import { loadMotionFeatures } from "@/lib/motion-features";
import * as m from "motion/react-m";
import { popoverOverlayAtom } from "@/state/popover-overlay";
import { SyncProvider } from "@/providers/sync-provider";
import { resolveDashboardRedirect } from "@/lib/route-access-guards";
export const Route = createFileRoute("/(dashboard)")({
beforeLoad: ({ context }) => {
const redirectTarget = resolveDashboardRedirect(context.auth.hasSession());
if (redirectTarget) {
throw redirect({ to: redirectTarget });
}
},
component: DashboardLayout,
head: () => ({
meta: [{ content: "noindex, nofollow", name: "robots" }],
links: [
{
rel: "preload",
href: "/assets/fonts/GeistMono-variable.woff2",
as: "font",
type: "font/woff2",
crossOrigin: "anonymous",
},
],
}),
});
function DashboardLayout() {
const overlayActive = useAtomValue(popoverOverlayAtom);
return (
);
}
================================================
FILE: applications/web/src/routes/(marketing)/blog/$slug.tsx
================================================
import { createFileRoute, notFound } from "@tanstack/react-router";
import { Streamdown } from "streamdown";
import { Heading1 } from "@/components/ui/primitives/heading";
import { markdownComponents } from "@/components/ui/primitives/markdown-component-map";
import { Text } from "@/components/ui/primitives/text";
import { BlogPostCta } from "@/features/blog/components/blog-post-cta";
import { findBlogPostBySlug, formatIsoDate } from "@/lib/blog-posts";
import { canonicalUrl, jsonLdScript, seoMeta, blogPostingSchema, breadcrumbSchema } from "@/lib/seo";
export const Route = createFileRoute("/(marketing)/blog/$slug")({
component: BlogPostPage,
head: ({ params }) => {
const blogPost = findBlogPostBySlug(params.slug);
if (!blogPost) {
return { meta: [{ title: "Blog Post · Keeper.sh" }] };
}
const postUrl = `/blog/${params.slug}`;
return {
links: [{ rel: "canonical", href: canonicalUrl(postUrl) }],
meta: [
...seoMeta({
title: blogPost.metadata.title,
description: blogPost.metadata.description,
path: postUrl,
type: "article",
}),
{ content: blogPost.metadata.tags.join(", "), name: "keywords" },
{ content: blogPost.metadata.createdAt, property: "article:published_time" },
{ content: blogPost.metadata.updatedAt, property: "article:modified_time" },
...blogPost.metadata.tags.map((tag) => ({
content: tag,
property: "article:tag",
})),
],
scripts: [
jsonLdScript(blogPostingSchema({
title: blogPost.metadata.title,
description: blogPost.metadata.description,
slug: params.slug,
createdAt: blogPost.metadata.createdAt,
updatedAt: blogPost.metadata.updatedAt,
tags: blogPost.metadata.tags,
})),
jsonLdScript(breadcrumbSchema([
{ name: "Home", path: "/" },
{ name: "Blog", path: "/blog" },
{ name: blogPost.metadata.title, path: postUrl },
])),
],
};
},
});
function BlogPostPage() {
const { slug } = Route.useParams();
const blogPost = findBlogPostBySlug(slug);
if (!blogPost) {
throw notFound();
}
const createdDate = formatIsoDate(blogPost.metadata.createdAt);
const updatedDate = formatIsoDate(blogPost.metadata.updatedAt);
const showUpdated = blogPost.metadata.updatedAt !== blogPost.metadata.createdAt;
return (
{blogPost.metadata.title}
By{" "}
Rida F'kih
{" · "}{createdDate}
{showUpdated && <> · Updated {updatedDate}>}
{blogPost.content}
);
}
================================================
FILE: applications/web/src/routes/(marketing)/blog/index.tsx
================================================
import { createFileRoute, Link } from "@tanstack/react-router";
import { Heading1, Heading3 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { blogPosts, formatIsoDate } from "@/lib/blog-posts";
import { canonicalUrl, jsonLdScript, seoMeta, breadcrumbSchema, collectionPageSchema } from "@/lib/seo";
const BLOG_ILLUSTRATION_STYLE = {
backgroundImage:
"repeating-linear-gradient(-45deg, transparent 0 14px, var(--color-illustration-stripe) 14px 15px)",
} as const;
export const Route = createFileRoute("/(marketing)/blog/")({
component: BlogDirectoryPage,
head: () => ({
links: [{ rel: "canonical", href: canonicalUrl("/blog") }],
meta: seoMeta({
title: "Blog",
description: "Product updates, engineering deep-dives, and calendar syncing tips from the Keeper.sh team.",
path: "/blog",
}),
scripts: [
jsonLdScript(breadcrumbSchema([
{ name: "Home", path: "/" },
{ name: "Blog", path: "/blog" },
])),
jsonLdScript(collectionPageSchema(blogPosts)),
],
}),
});
function BlogDirectoryPage() {
return (
Blog
Product updates, engineering deep-dives, and calendar syncing tips from the Keeper.sh team.
{blogPosts.map((blogPost) => (
{blogPost.metadata.title}
Created {formatIsoDate(blogPost.metadata.createdAt)}
{blogPost.metadata.blurb}
))}
);
}
================================================
FILE: applications/web/src/routes/(marketing)/blog/route.tsx
================================================
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/(marketing)/blog")({
component: BlogRouteLayout,
});
function BlogRouteLayout() {
return ;
}
================================================
FILE: applications/web/src/routes/(marketing)/index.tsx
================================================
import { useSetAtom } from 'jotai'
import { createFileRoute } from '@tanstack/react-router'
import { canonicalUrl, jsonLdScript, seoMeta, softwareApplicationSchema } from '../../lib/seo'
import { Heading1, Heading2, Heading3 } from '../../components/ui/primitives/heading'
import { Text } from '../../components/ui/primitives/text'
import {
MarketingHowItWorksSection,
MarketingHowItWorksCard,
MarketingHowItWorksRow,
MarketingHowItWorksStepBody,
MarketingHowItWorksStepIllustration,
} from '../../features/marketing/components/marketing-how-it-works'
import { MarketingFaqSection, MarketingFaqList, MarketingFaqItem, MarketingFaqQuestion } from '../../features/marketing/components/marketing-faq'
import { MarketingCtaSection, MarketingCtaCard } from '../../features/marketing/components/marketing-cta'
import { Collapsible } from '../../components/ui/primitives/collapsible'
import { ButtonIcon, ButtonText, ExternalLinkButton, LinkButton } from '../../components/ui/primitives/button'
import { MarketingIllustrationCalendar, MarketingIllustrationCalendarCard, type Skew, type SkewTuple } from '../../features/marketing/components/marketing-illustration-calendar'
import {
MarketingFeatureBentoBody,
MarketingFeatureBentoCard,
MarketingFeatureBentoGrid,
MarketingFeatureBentoIllustration,
MarketingFeatureBentoSection,
} from '../../features/marketing/components/marketing-feature-bento'
import { MarketingIllustrationContributors } from '../../illustrations/marketing-illustration-contributors'
import { MarketingIllustrationProviders } from '../../illustrations/marketing-illustration-providers'
import { MarketingIllustrationSync } from '../../illustrations/marketing-illustration-sync'
import { MarketingIllustrationSetup } from '../../illustrations/marketing-illustration-setup'
import { HowItWorksConnect } from '../../illustrations/how-it-works-connect'
import { HowItWorksConfigure } from '../../illustrations/how-it-works-configure'
import { HowItWorksSync } from '../../illustrations/how-it-works-sync'
import {
MarketingPricingComparisonGrid,
MarketingPricingComparisonSpacer,
MarketingPricingFeatureDisplay,
MarketingPricingFeatureLabel,
type MarketingPricingFeatureValueKind,
MarketingPricingFeatureMatrix,
MarketingPricingFeatureRow,
MarketingPricingFeatureValue,
MarketingPricingIntro,
MarketingPricingPlanCard,
MarketingPricingSection,
} from '../../features/marketing/components/marketing-pricing-section'
import { calendarEmphasizedAtom } from '../../state/calendar-emphasized'
import { ANALYTICS_EVENTS } from '../../lib/analytics'
import ArrowRightIcon from "lucide-react/dist/esm/icons/arrow-right";
import ArrowUpRightIcon from "lucide-react/dist/esm/icons/arrow-up-right";
const createSkew = (rotate: number, x: number, y: number): Skew => ({ rotate, x, y });
const SKEW_BACK_LEFT: SkewTuple = [
createSkew(-12, -24, 12),
createSkew(-8, -16, 8),
createSkew(-3, -8, 4),
]
const SKEW_BACK_RIGHT: SkewTuple = [
createSkew(9, 20, -8),
createSkew(5, 12, -4),
createSkew(1.5, 6, -2),
]
const SKEW_FRONT: SkewTuple = [
createSkew(-4, 4, -6),
createSkew(-2, 2, -2),
createSkew(0, 0, 0),
]
type MarketingFeature = {
id: number
title: string
description: string
gridClassName: string
illustration?: React.ReactNode
}
const MARKETING_FEATURES: MarketingFeature[] = [
{
id: 1,
title: 'Privacy-First & Open Source',
description:
'Open-source, released under an AGPL-3.0 license. Secure and community driven. Here are some of the latest contributors.',
gridClassName: 'lg:col-start-1 lg:col-span-4 lg:row-start-1',
illustration: ,
},
{
id: 2,
title: 'Universal Calendar Sync',
description:
'Google Calendar, Outlook, Apple Calendar, and more. Automatically sync events between all your calendars no matter the provider.',
gridClassName: 'lg:col-start-5 lg:col-span-6 lg:row-start-1',
illustration: ,
},
{
id: 3,
title: 'Simple Synchronization Engine',
description:
'Your events are aggregated and synced across all linked calendars. Discrepancies are reconciled. Built to prevent orphan events.',
gridClassName: 'lg:col-start-1 lg:col-span-6 lg:row-start-2',
illustration: ,
},
{
id: 4,
title: 'Quick Setup',
description:
'Link OAuth, ICS or CalDAV accounts in seconds. No complicated configuration or technical knowledge required. Connect and go.',
gridClassName: 'lg:col-start-7 lg:col-span-4 lg:row-start-2',
illustration: ,
},
]
type PricingFeature = {
label: string
free: MarketingPricingFeatureValueKind
pro: MarketingPricingFeatureValueKind
}
type PricingPlan = {
id: string
name: string
price: string
period: string
description: string
ctaLabel: string
tone?: "default" | "inverse"
}
const PRICING_PLANS: PricingPlan[] = [
{
id: 'free',
name: 'Free',
price: '$0',
period: 'per month',
description:
'For personal use and getting started with calendar sync.',
ctaLabel: 'Get Started',
},
{
id: 'pro',
name: 'Pro',
price: '$5',
period: 'per month',
description:
'For power users who need fast syncs, advanced feed controls, and unlimited syncing.',
ctaLabel: 'Get Started',
tone: "inverse" as const,
},
]
const PRICING_FEATURES: PricingFeature[] = [
{ label: 'Sync Interval', free: 'Every 30 minutes', pro: 'Every 1 minute' },
{ label: 'Linked Accounts', free: 'Up to 2', pro: 'infinity' },
{ label: 'Sync Mappings', free: 'Up to 3', pro: 'infinity' },
{ label: 'Aggregated iCal Feed', free: 'check', pro: 'check' },
{ label: 'iCal Feed Customization', free: 'minus', pro: 'check' },
{ label: 'Event Filters & Exclusions', free: 'minus', pro: 'check' },
{ label: 'API & MCP Access', free: '25 calls/day', pro: 'infinity' },
{ label: 'Priority Support', free: 'minus', pro: 'check' },
]
type HowItWorksStep = {
title: string
description: string
}
const HOW_IT_WORKS_STEPS: HowItWorksStep[] = [
{
title: 'Connect your calendars',
description:
'Link your Google, Outlook, iCloud, or CalDAV accounts using OAuth or ICS feeds. It takes seconds.',
},
{
title: 'Configure sync rules',
description:
'Choose which calendars to sync and how events should appear. Keeper handles the rest automatically.',
},
{
title: 'Stay in sync',
description:
'Events are continuously aggregated and pushed across all your linked calendars. Conflicts are reconciled.',
},
]
type FaqItem = {
question: string
answer: string
content?: React.ReactNode
}
const FAQ_ITEMS: FaqItem[] = [
{
question: 'Can I use ICS or iCal links as a source?',
answer:
'Yes. Any publicly accessible ICS or iCal link can be used as a calendar source in Keeper. This means you can pull events from services that only offer read-only calendar feeds.',
},
{
question: 'Which calendar providers does Keeper.sh support?',
answer:
'Keeper.sh works with Google Calendar, Microsoft Outlook, Apple iCloud, FastMail, and any provider that supports CalDAV or ICS feeds. If your calendar supports one of these protocols, it will work with Keeper.',
},
{
question: 'Can I self-host Keeper.sh?',
answer:
'Yes. Keeper.sh is open-source under the AGPL-3.0 license. Check the README on GitHub for setup instructions, or use one of the many Docker images we offer for quick deployment.',
content: <>Yes. Keeper.sh is open-source under the AGPL-3.0 license. Check the README on GitHub for setup instructions, or use one of the many Docker images we offer for quick deployment.>,
},
{
question: 'How often do calendars sync?',
answer:
'On the free plan, calendars sync every 30 minutes. On the Pro plan, calendars sync every minute.',
},
{
question: 'Are my event details visible to others?',
answer:
'Only if you want them to be. You can choose whether events display details, or just show a generic event summary. You can customize the title, and choose to hide the details you want to keep private. These are configurable per-calendar.',
},
{
question: 'Can I control how synced events appear?',
answer:
'Yes. You configure how events are displayed on each destination calendar. Titles, descriptions, and other details can be customized or stripped entirely.',
},
{
question: 'Can I cancel my subscription anytime?',
answer:
'Yes. You can cancel at any time from your account settings. Your access continues until the end of the current billing period.',
},
]
export const Route = createFileRoute('/(marketing)/')({
component: MarketingPage,
head: () => ({
links: [{ rel: "canonical", href: canonicalUrl("/") }],
meta: seoMeta({
title: "Open-Source Calendar Syncing for Google, Outlook & iCloud",
description:
"Keep your personal, work, and school calendars in sync automatically. Open-source (AGPL-3.0) calendar syncing for Google Calendar, Outlook, iCloud, FastMail, and CalDAV.",
path: "/",
brandPosition: "before",
}),
scripts: [jsonLdScript(softwareApplicationSchema())],
}),
})
function MarketingPage() {
const setEmphasized = useSetAtom(calendarEmphasizedAtom)
return (
All of your calendars in-sync.
Synchronize events between your personal, work, business and school calendars automatically. Works with Google Calendar, Outlook, iCloud, CalDAV, and ICS/iCal feeds. Open-source under AGPL-3.0.
setEmphasized(true)}
onMouseLeave={() => setEmphasized(false)}
data-visitors-event={ANALYTICS_EVENTS.marketing_cta_clicked}
data-visitors-cta="hero"
>
Sync Calendars
View GitHub
{MARKETING_FEATURES.map((feature) => (
{feature.illustration}
{feature.title}
{feature.description}
))}
Hosted Pricing
Keeper.sh uses a low-cost freemium model to give you a solid range of choice. Check the GitHub repository for self-hosting options.
{PRICING_PLANS.map((plan) => (
))}
{PRICING_FEATURES.map((feature) => (
{feature.label}
))}
How It Works
Three steps to keep every calendar on the same page. Connect, configure, and forget about it.
{HOW_IT_WORKS_STEPS[0].title}
{HOW_IT_WORKS_STEPS[0].description}
{HOW_IT_WORKS_STEPS[1].title}
{HOW_IT_WORKS_STEPS[1].description}
{HOW_IT_WORKS_STEPS[2].title}
{HOW_IT_WORKS_STEPS[2].description}
Frequently Asked Questions
Everything you need to know about Keeper.sh. Can't find what you're looking for? Reach out at{' '}
support@keeper.sh .
{FAQ_ITEMS.map((item) => (
{item.question}}
>
{item.content ?? item.answer}
))}
Ready to sync your calendars?
Start syncing your calendars in seconds. Free to use, no credit card required.
Get Started
View on GitHub
)
}
================================================
FILE: applications/web/src/routes/(marketing)/privacy.tsx
================================================
import type { PropsWithChildren } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { Heading1, Heading2, Heading3 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { canonicalUrl, jsonLdScript, seoMeta, webPageSchema, breadcrumbSchema } from "@/lib/seo";
import { privacyPageMetadata, formatMonthYear } from "@/lib/page-metadata";
export const Route = createFileRoute("/(marketing)/privacy")({
component: PrivacyPage,
head: () => ({
links: [{ rel: "canonical", href: canonicalUrl("/privacy") }],
meta: seoMeta({
title: "Privacy Policy",
description:
"How Keeper.sh collects, uses, and protects your calendar data. Privacy-first design with event anonymization and minimal data retention.",
path: "/privacy",
}),
scripts: [
jsonLdScript(webPageSchema("Privacy Policy", "Privacy policy for Keeper.sh, the open-source calendar syncing service.", "/privacy")),
jsonLdScript(breadcrumbSchema([{ name: "Home", path: "/" }, { name: "Privacy Policy", path: "/privacy" }])),
],
}),
});
function PrivacyPage() {
return (
Privacy Policy
Last updated: {formatMonthYear(privacyPageMetadata.updatedAt)}
Keeper.sh (“we”, “our”, or “us”) is committed to protecting your privacy. This Privacy Policy
explains how we collect, use, disclose, and safeguard your information when you use our
calendar synchronization service.
By using Keeper.sh, you consent to the data practices described in this policy. If you do not
agree with the terms of this policy, please do not access or use our service.
Account Information
When you create an account, we collect your email address and authentication credentials.
If you sign up using a third-party provider (such as Google), we receive basic profile
information as permitted by your privacy settings with that provider.
Calendar Data
To provide our service, we access calendar data from sources you connect. This includes
event titles, times, durations, and associated metadata. We only access calendars you
explicitly authorize.
Usage Data
We automatically collect certain information when you access our service, including your
IP address, browser type, operating system, access times, and pages viewed. This data
helps us improve our service and diagnose technical issues.
We use the information we collect to:
Provide, maintain, and improve our calendar syncing service
Aggregate and anonymize calendar events for shared feeds, showing only busy/free status
Push synchronized events to your designated destination calendars
Send service-related communications and respond to inquiries
Monitor and analyze usage patterns to improve user experience
Detect, prevent, and address technical issues or abuse
A core feature of Keeper.sh is event anonymization. When you generate a shared iCal feed or
push to external calendars, event details (titles, descriptions, attendees, locations) are
stripped. Only busy/free time blocks are shared, protecting the privacy of your schedule
details.
Your data is stored on secure servers with encryption at rest and in transit. We implement
industry-standard security measures including access controls, monitoring, and regular
security assessments.
Calendar data is cached temporarily to enable synchronization and is refreshed according
to your plan's sync interval. We do not retain historical calendar data beyond what is
necessary for the service to function.
We retain your account information and calendar data for as long as your account is
active. When you delete your account, we delete all associated data within 30 days, except
where retention is required by law or for legitimate business purposes (such as fraud
prevention).
We integrate with third-party calendar providers (such as Google Calendar) to access and
sync your calendar data. These integrations are governed by the respective providers'
terms and privacy policies. We only request the minimum permissions necessary to provide
our service.
We use third-party services for payment processing (Polar). Payment information is handled
directly by these processors and is not stored on our servers.
We do not sell, trade, or otherwise transfer your personal information to third parties
for marketing purposes.
You have the right to:
Access the personal data we hold about you
Request correction of inaccurate data
Request deletion of your data and account
Disconnect calendar sources at any time
Export your data in a portable format
Withdraw consent for data processing
To exercise these rights, contact us at{" "}
privacy@keeper.sh
.
Keeper.sh is operated from the Province of Alberta, Canada. Your information may be
transferred to and processed in countries other than your own. We ensure appropriate
safeguards are in place to protect your data in compliance with applicable Canadian
privacy laws, including the Personal Information Protection and Electronic Documents
Act (PIPEDA).
Keeper.sh is not intended for use by individuals under the age of 13. We do not knowingly
collect personal information from children. If we become aware that we have collected data
from a child, we will take steps to delete it promptly.
We may update this Privacy Policy from time to time. We will notify you of significant
changes by posting the new policy on this page and updating the “Last updated” date. Your
continued use of the service after changes constitutes acceptance of the updated policy.
If you have questions or concerns about this Privacy Policy or our data practices, please
contact us at{" "}
privacy@keeper.sh
.
);
}
function Section({ title, children }: PropsWithChildren<{ title: string }>) {
return (
);
}
================================================
FILE: applications/web/src/routes/(marketing)/route.tsx
================================================
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { jsonLdScript, organizationSchema } from '../../lib/seo'
import { Layout, LayoutItem } from '../../components/ui/shells/layout'
import { MarketingHeader, MarketingHeaderActions, MarketingHeaderBranding } from '../../features/marketing/components/marketing-header'
import { MarketingFooter, MarketingFooterTagline, MarketingFooterNav, MarketingFooterNavGroup, MarketingFooterNavGroupLabel, MarketingFooterNavItem } from '../../features/marketing/components/marketing-footer'
import KeeperLogo from "@/assets/keeper.svg?react";
import { ButtonText, LinkButton } from '../../components/ui/primitives/button';
import { GithubStarButton } from '../../components/ui/primitives/github-star-button';
import { SessionSlot } from '../../components/ui/shells/session-slot';
import HeartIcon from "lucide-react/dist/esm/icons/heart";
import { ExternalTextLink } from "@/components/ui/primitives/text-link";
import { CookieConsent } from "@/components/cookie-consent";
interface GithubStarsLoaderData {
count: number | null;
fetchedAt: string | null;
}
export const Route = createFileRoute('/(marketing)')({
beforeLoad: ({ context }) => {
if (!context.runtimeConfig.commercialMode) {
const hasSession = context.auth.hasSession();
throw redirect({ to: hasSession ? "/dashboard" : "/login" });
}
},
loader: async ({ context }) => {
try {
return await context.fetchWeb("/internal/github-stars");
} catch {
return {
count: null,
fetchedAt: null,
} satisfies GithubStarsLoaderData;
}
},
head: () => ({
scripts: [jsonLdScript(organizationSchema)],
}),
component: MarketingLayout,
})
function MarketingLayout() {
const githubStars = Route.useLoaderData();
const { runtimeConfig } = Route.useRouteContext();
return (
<>
Dashboard
}
unauthenticated={
Login
Register
}
/>
Made with by{" "}
Rida F'kih
Product
Get Started
Features
Pricing
Resources
Blog
GitHub
Legal
Privacy Policy
Terms of Service
{runtimeConfig.gdprApplies && }
>
)
}
================================================
FILE: applications/web/src/routes/(marketing)/terms.tsx
================================================
import type { PropsWithChildren } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { Heading1, Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { canonicalUrl, jsonLdScript, seoMeta, webPageSchema, breadcrumbSchema } from "@/lib/seo";
import { termsPageMetadata, formatMonthYear } from "@/lib/page-metadata";
export const Route = createFileRoute("/(marketing)/terms")({
component: TermsPage,
head: () => ({
links: [{ rel: "canonical", href: canonicalUrl("/terms") }],
meta: seoMeta({
title: "Terms & Conditions",
description:
"Terms of service for Keeper.sh. Covers account registration, subscription billing, acceptable use, and data ownership for our calendar syncing service.",
path: "/terms",
}),
scripts: [
jsonLdScript(webPageSchema("Terms & Conditions", "Terms and conditions for using Keeper.sh, the open-source calendar syncing service.", "/terms")),
jsonLdScript(breadcrumbSchema([{ name: "Home", path: "/" }, { name: "Terms & Conditions", path: "/terms" }])),
],
}),
});
function TermsPage() {
return (
Terms & Conditions
Last updated: {formatMonthYear(termsPageMetadata.updatedAt)}
These Terms and Conditions (“Terms”) constitute a legally binding agreement between you
and Keeper.sh (“we”, “our”, or “us”) governing your access to and use of the Keeper.sh calendar
synchronization service.
By accessing or using Keeper.sh, you agree to be bound by these Terms. If you disagree with
any part of these Terms, you may not access or use the service.
Keeper.sh provides calendar aggregation and synchronization services that allow you to
combine events from multiple calendar sources into a unified, anonymized feed. The service
includes generating iCal feeds and pushing events to external calendar providers.
We reserve the right to modify, suspend, or discontinue any aspect of the service at any
time, with or without notice. We shall not be liable to you or any third party for any
modification, suspension, or discontinuation of the service.
To use Keeper.sh, you must create an account. You agree to provide accurate, current, and
complete information during registration and to update such information as necessary.
You are responsible for safeguarding your account credentials and for all activities that
occur under your account. You agree to notify us immediately of any unauthorized use of
your account.
We reserve the right to suspend or terminate accounts that violate these Terms or that we
reasonably believe pose a security risk.
Keeper.sh offers both free and paid subscription plans. Paid subscriptions are billed in
advance on a monthly or yearly basis. All fees are non-refundable except as required by
law or as explicitly stated in these Terms.
We may change subscription fees upon reasonable notice. Continued use of the service after
a price change constitutes acceptance of the new fees. You may cancel your subscription at
any time; access will continue until the end of your current billing period.
You agree not to:
Use the service for any unlawful purpose or in violation of any applicable laws
Attempt to gain unauthorized access to any part of the service or its related systems
Interfere with or disrupt the integrity or performance of the service
Use automated means to access the service beyond normal API usage
Resell, sublicense, or redistribute the service without our written consent
Use the service to transmit malicious code or engage in abusive behavior
The service and its original content, features, and functionality are owned by Keeper.sh and
are protected by international copyright, trademark, and other intellectual property laws.
Keeper.sh is open-source software licensed under the AGPL-3.0 license. The source code is
available at{" "}
github.com/ridafkih/keeper.sh
. Your use of the source code is governed by the terms of that license.
You retain ownership of any calendar data and content you provide to the service. By using
Keeper.sh, you grant us a limited license to access, process, and display your content solely
as necessary to provide the service.
You represent that you have the right to share any calendar data you connect to Keeper.sh and
that doing so does not violate any third-party rights or agreements.
Keeper.sh integrates with third-party calendar providers and services. Your use of these
integrations is subject to the terms and policies of those third parties. We are not
responsible for the practices of third-party services.
THE SERVICE IS PROVIDED “AS IS” AND “AS AVAILABLE” WITHOUT WARRANTIES OF ANY KIND, EITHER
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
We do not warrant that the service will be uninterrupted, secure, or error-free, that
defects will be corrected, or that the service is free of viruses or other harmful
components.
TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL KEEPER.SH, ITS OFFICERS, DIRECTORS,
EMPLOYEES, OR AGENTS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR
PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, USE, OR GOODWILL,
ARISING OUT OF OR IN CONNECTION WITH YOUR USE OF THE SERVICE.
OUR TOTAL LIABILITY FOR ANY CLAIMS ARISING FROM YOUR USE OF THE SERVICE SHALL NOT EXCEED
THE AMOUNT YOU PAID US, IF ANY, DURING THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
You agree to indemnify and hold harmless Keeper.sh and its officers, directors, employees,
and agents from any claims, damages, losses, or expenses (including reasonable attorneys'
fees) arising from your use of the service, violation of these Terms, or infringement of
any third-party rights.
We may terminate or suspend your access to the service immediately, without prior notice,
for any reason, including breach of these Terms. Upon termination, your right to use the
service ceases immediately.
You may terminate your account at any time by deleting it through the service settings.
Provisions of these Terms that by their nature should survive termination shall survive,
including ownership, warranty disclaimers, and limitations of liability.
These Terms shall be governed by and construed in accordance with the laws of the
Province of Alberta, Canada, and the federal laws of Canada applicable therein, without
regard to conflict of law principles. Any disputes arising under these Terms shall be
subject to the exclusive jurisdiction of the courts of the Province of Alberta.
We reserve the right to modify these Terms at any time. We will notify you of material
changes by posting the updated Terms on this page and updating the “Last updated” date.
Your continued use of the service after changes constitutes acceptance of the modified
Terms.
If any provision of these Terms is found to be unenforceable or invalid, that provision
shall be limited or eliminated to the minimum extent necessary, and the remaining
provisions shall remain in full force and effect.
If you have questions about these Terms, please contact us at{" "}
legal@keeper.sh
.
);
}
function Section({ title, children }: PropsWithChildren<{ title: string }>) {
return (
);
}
================================================
FILE: applications/web/src/routes/(oauth)/auth/google.tsx
================================================
import { createFileRoute, redirect } from "@tanstack/react-router";
import { AuthOAuthPreamble } from "@/features/auth/components/oauth-preamble";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import {
getMcpAuthorizationSearch,
toStringSearchParams,
} from "@/lib/mcp-auth-flow";
export const Route = createFileRoute("/(oauth)/auth/google")({
loader: async ({ context }) => {
const capabilities = await fetchAuthCapabilitiesWithApi(context.fetchApi);
if (!capabilities.socialProviders.google) {
throw redirect({ to: "/login" });
}
return capabilities;
},
component: GoogleAuthPage,
validateSearch: toStringSearchParams,
});
function GoogleAuthPage() {
const search = Route.useSearch();
return (
);
}
================================================
FILE: applications/web/src/routes/(oauth)/auth/outlook.tsx
================================================
import { createFileRoute, redirect } from "@tanstack/react-router";
import { AuthOAuthPreamble } from "@/features/auth/components/oauth-preamble";
import { fetchAuthCapabilitiesWithApi } from "@/lib/auth-capabilities";
import {
getMcpAuthorizationSearch,
toStringSearchParams,
} from "@/lib/mcp-auth-flow";
export const Route = createFileRoute("/(oauth)/auth/outlook")({
loader: async ({ context }) => {
const capabilities = await fetchAuthCapabilitiesWithApi(context.fetchApi);
if (!capabilities.socialProviders.microsoft) {
throw redirect({ to: "/login" });
}
return capabilities;
},
component: OutlookAuthPage,
validateSearch: toStringSearchParams,
});
function OutlookAuthPage() {
const search = Route.useSearch();
return (
);
}
================================================
FILE: applications/web/src/routes/(oauth)/auth/route.tsx
================================================
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { resolveAuthRedirect } from "@/lib/route-access-guards";
export const Route = createFileRoute("/(oauth)/auth")({
beforeLoad: ({ context }) => {
const redirectTarget = resolveAuthRedirect(context.auth.hasSession());
if (redirectTarget) {
throw redirect({ to: redirectTarget });
}
},
component: OAuthAuthLayout,
});
function OAuthAuthLayout() {
return ;
}
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/apple.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import { CalDAVConnectPage } from "@/features/auth/components/caldav-connect-page";
export const Route = createFileRoute("/(oauth)/dashboard/connect/apple")({
component: ConnectApplePage,
});
function ConnectApplePage() {
return (
}
heading="Connect Apple Calendar"
description="iCloud uses an app-specific password to authenticate Keeper.sh to interact with your calendar."
steps={[
<>
Navigate to{" "}
iCloud Apple ID
>,
"Sign in with your Apple ID",
<>Select “App-Specific Passwords”>,
<>Click the “+” next to “Passwords”>,
"Label and generate the password, then copy it",
"Paste the app-specific password below",
]}
/>
);
}
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/caldav.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import Calendar from "lucide-react/dist/esm/icons/calendar";
import { Text } from "@/components/ui/primitives/text";
import { CalDAVConnectPage } from "@/features/auth/components/caldav-connect-page";
export const Route = createFileRoute("/(oauth)/dashboard/connect/caldav")({
component: ConnectCalDAVPage,
});
function ConnectCalDAVPage() {
return (
}
heading="Connect CalDAV Server"
description="Connect to any CalDAV-compatible server."
steps={[
"Enter your CalDAV server URL",
"Provide a username",
"Enter a password, or app-specific password",
<>Click “Connect”>,
]}
footer={
Your CalDAV server URL can typically be found in your calendar provider's settings or documentation.
}
/>
);
}
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/fastmail.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import { CalDAVConnectPage } from "@/features/auth/components/caldav-connect-page";
export const Route = createFileRoute(
"/(oauth)/dashboard/connect/fastmail",
)({
component: ConnectFastmailPage,
});
function ConnectFastmailPage() {
return (
}
heading="Connect Fastmail"
description="Fastmail uses an app-specific password to authenticate Keeper.sh to interact with your calendar."
steps={[
<>
Navigate to{" "}
Fastmail Settings
>,
<>Click “Manage app password and access”>,
<>Click “New app password”>,
<>Under “Access”, select “Calendars (CalDAV)”>,
<>Click “Generate password”>,
"Copy the password, and paste it below",
]}
/>
);
}
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/google.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import { LinkOAuthPreamble } from "@/features/auth/components/oauth-preamble";
export const Route = createFileRoute("/(oauth)/dashboard/connect/google")({
component: () => ,
});
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/ical-link.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import Link from "lucide-react/dist/esm/icons/link";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { ProviderIconPair } from "@/features/auth/components/oauth-preamble";
import { ICSFeedForm } from "@/features/auth/components/ics-connect-form";
export const Route = createFileRoute(
"/(oauth)/dashboard/connect/ical-link",
)({
component: ConnectICalLinkPage,
});
function ConnectICalLinkPage() {
return (
<>
Subscribe to ICS Feed
Subscribe to a read-only calendar feed from any ICS-compatible source, supported by most calendar providers.
>
);
}
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/ics-file.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import Calendar from "lucide-react/dist/esm/icons/calendar";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { ProviderIconPair } from "@/features/auth/components/oauth-preamble";
import { ButtonText, LinkButton } from "@/components/ui/primitives/button";
export const Route = createFileRoute(
"/(oauth)/dashboard/connect/ics-file",
)({
component: ConnectICSFilePage,
});
function ConnectICSFilePage() {
return (
<>
Upload ICS File
ICS snapshot uploads are not available in Canary yet. Use an ICS feed link to keep your events synced.
Use ICS Feed Instead
>
);
}
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/microsoft.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import { LinkOAuthPreamble } from "@/features/auth/components/oauth-preamble";
export const Route = createFileRoute(
"/(oauth)/dashboard/connect/microsoft",
)({
component: () => ,
});
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/outlook.tsx
================================================
import { createFileRoute } from "@tanstack/react-router";
import { LinkOAuthPreamble } from "@/features/auth/components/oauth-preamble";
export const Route = createFileRoute("/(oauth)/dashboard/connect/outlook")({
component: () => ,
});
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/connect/route.tsx
================================================
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/(oauth)/dashboard/connect")({
component: OAuthConnectLayout,
});
function OAuthConnectLayout() {
return (
);
}
================================================
FILE: applications/web/src/routes/(oauth)/dashboard/route.tsx
================================================
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { resolveDashboardRedirect } from "@/lib/route-access-guards";
export const Route = createFileRoute("/(oauth)/dashboard")({
beforeLoad: ({ context }) => {
const redirectTarget = resolveDashboardRedirect(context.auth.hasSession());
if (redirectTarget) {
throw redirect({ to: redirectTarget });
}
},
component: OAuthDashboardLayout,
});
function OAuthDashboardLayout() {
return ;
}
================================================
FILE: applications/web/src/routes/(oauth)/oauth/consent.tsx
================================================
import { useState } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
import Terminal from "lucide-react/dist/esm/icons/terminal";
import { Button, ButtonText } from "@/components/ui/primitives/button";
import { Divider } from "@/components/ui/primitives/divider";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import {
PermissionsList,
ProviderIconPair,
} from "@/features/auth/components/oauth-preamble";
import {
getMcpAuthorizationSearch,
toStringSearchParams,
} from "@/lib/mcp-auth-flow";
import { resolveErrorMessage } from "@/utils/errors";
import { track, ANALYTICS_EVENTS } from "@/lib/analytics";
type SearchParams = Record;
const SCOPE_LABELS: Record = {
"keeper.read": "Read your Keeper data",
"keeper.sources.read": "View your calendar sources",
"keeper.destinations.read": "View your sync destinations",
"keeper.mappings.read": "View your source-destination mappings",
"keeper.events.read": "View your calendar events",
"keeper.sync-status.read": "View sync status",
"offline_access": "Stay connected when you're away",
};
const resolveScopeLabel = (scope: string): string => {
const label = SCOPE_LABELS[scope];
if (label) {
return label;
}
return scope;
};
// This route lives under (oauth)/mcp rather than (oauth)/auth/mcp because
// the (oauth)/auth layout redirects logged-in users away, but the consent
// page requires an active session.
export const Route = createFileRoute("/(oauth)/oauth/consent")({
beforeLoad: ({ search }) => {
if (!getMcpAuthorizationSearch(search)) {
throw redirect({ to: "/login", search });
}
},
component: McpConsentPage,
validateSearch: (search: Record): SearchParams => toStringSearchParams(search),
});
function extractConsentErrorMessage(payload: unknown): string {
if (typeof payload !== "object" || payload === null) {
return "Failed to complete consent";
}
if (!("message" in payload)) {
return "Failed to complete consent";
}
if (typeof payload.message !== "string") {
return "Failed to complete consent";
}
return payload.message;
}
function extractConsentRedirectUrl(payload: unknown): string {
if (typeof payload !== "object" || payload === null) {
throw new TypeError("Expected consent response to contain a redirect URL");
}
if (!("url" in payload) || typeof payload.url !== "string") {
throw new TypeError("Expected consent response to contain a redirect URL");
}
return payload.url;
}
function McpConsentPage() {
const search = Route.useSearch();
const [error, setError] = useState(null);
const [status, setStatus] = useState<"idle" | "loading">("idle");
const authorizationSearch = getMcpAuthorizationSearch(search);
if (!authorizationSearch) {
return null;
}
const { client_id: clientId, scope } = authorizationSearch;
const scopes = scope
.split(" ")
.filter((value) => value.length > 0)
.map(resolveScopeLabel);
const handleDecision = async (accept: boolean) => {
setStatus("loading");
setError(null);
try {
const oauthQuery = window.location.search.slice(1);
const response = await fetch("/api/auth/oauth2/consent", {
body: JSON.stringify({
accept,
oauth_query: oauthQuery,
}),
credentials: "include",
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
throw new Error(extractConsentErrorMessage(payload));
}
const payload = await response.json();
if (accept) {
track(ANALYTICS_EVENTS.oauth_consent_granted);
} else {
track(ANALYTICS_EVENTS.oauth_consent_denied);
}
window.location.assign(extractConsentRedirectUrl(payload));
} catch (requestError) {
setError(resolveErrorMessage(requestError, "Failed to complete consent"));
setStatus("idle");
}
};
return (
<>
Authorize MCP access
{clientId} is requesting permission to access your Keeper data.
{scopes.length > 0 && }
{error && (
{error}
)}
{
void handleDecision(false);
}}
>
Deny
{
void handleDecision(true);
}}
>
Allow
>
);
}
================================================
FILE: applications/web/src/routes/(oauth)/route.tsx
================================================
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/(oauth)")({
component: OAuthLayout,
head: () => ({
meta: [{ content: "noindex, nofollow", name: "robots" }],
}),
});
function OAuthLayout() {
return (
);
}
================================================
FILE: applications/web/src/routes/__root.tsx
================================================
import { HeadContent, Outlet, Scripts, createRootRouteWithContext, useLocation } from "@tanstack/react-router";
import type { ErrorComponentProps } from "@tanstack/react-router";
import { useEffect } from "react";
import { SWRConfig } from "swr";
import { Heading2 } from "@/components/ui/primitives/heading";
import { Text } from "@/components/ui/primitives/text";
import { LinkButton, ButtonText } from "@/components/ui/primitives/button";
import { fetcher, HttpError } from "@/lib/fetcher";
import { resolveErrorMessage } from "@/utils/errors";
import type { AppRouterContext, ViteScript } from "@/lib/router-context";
import { serializePublicRuntimeConfig } from "@/lib/runtime-config";
import { AnalyticsScripts } from "@/components/analytics-scripts";
const NON_RETRYABLE_STATUSES = new Set([401, 403, 404]);
const SWR_CONFIG = {
fetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 2000,
onError: (error: unknown, key: string) => {
console.error(`[SWR] ${key}:`, error);
},
onErrorRetry: (
error: unknown,
_key: string,
_config: unknown,
revalidate: (opts: { retryCount: number }) => void,
{ retryCount }: { retryCount: number },
) => {
if (error instanceof HttpError && NON_RETRYABLE_STATUSES.has(error.status)) return;
if (retryCount >= 3) return;
setTimeout(() => revalidate({ retryCount }), 5000 * (retryCount + 1));
},
};
export const Route = createRootRouteWithContext()({
component: RootComponent,
notFoundComponent: NotFound,
errorComponent: ErrorFallback,
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "Keeper.sh" },
],
}),
});
function ViteScriptTag({ script }: { script: ViteScript }) {
if (script.src) {
return ;
}
if (script.content) {
return ;
}
return null;
}
function RootComponent() {
const { runtimeConfig, viteAssets } = Route.useRouteContext();
return (
{viteAssets?.inlineStyles?.map((css, index) => (
))}
{viteAssets?.stylesheets.map((href) => (
))}
{viteAssets?.modulePreloads?.map((href) => (
))}
{viteAssets?.headScripts.map((script, index) => (
))}
{viteAssets?.bodyScripts.map((script, index) => (
))}
);
}
function ScrollToTopOnNavigation() {
const location = useLocation();
useEffect(() => {
if (location.hash.length > 0) {
return;
}
window.scrollTo({ left: 0, top: 0, behavior: "auto" });
}, [location.hash, location.pathname]);
return null;
}
function NotFound() {
return (
Page not found
The page you're looking for doesn't exist.
Go home
);
}
function ErrorFallback({ error }: ErrorComponentProps) {
return (
Something went wrong
{resolveErrorMessage(error, "An unexpected error occurred.")}
Go home
);
}
================================================
FILE: applications/web/src/server/cache/stale-cache.ts
================================================
interface CacheEntry {
fetchedAtMs: number;
value: TValue;
}
interface CreateStaleCacheOptions {
name: string;
now?: () => number;
ttlMs: number;
load: () => Promise;
revalidationPolicy?: CacheRevalidationPolicy;
}
interface CacheSnapshot {
fetchedAtMs: number;
value: TValue;
}
type CacheRevalidationPolicy = "always" | "when-stale";
interface CacheState {
entry: CacheEntry | null;
refreshTask: Promise | null;
}
function createInitialState(): CacheState {
return {
entry: null,
refreshTask: null,
};
}
function isStale(state: CacheState, nowMs: number, ttlMs: number): boolean {
if (state.entry === null) {
return true;
}
return nowMs - state.entry.fetchedAtMs >= ttlMs;
}
function createRefreshTask(
state: CacheState