refactor: migrate to monorepo structure with Docker, PocketBase, and e2e tests
- Reorganize into apps/, packages/, docs/, e2e/, pocketbase/ directories - Add Dockerfiles for web, worker, and PocketBase services - Add docker-compose.yml for local orchestration - Add turbo.json for monorepo task management - Add Playwright e2e test infrastructure - Add PocketBase backend with migrations - Remove Vite/Next.js/ESLint/PostCSS config files - Update package.json with workspace dependencies - Add .env.example and .dockerignore
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
export { useThemeStore } from './use-theme-store';
|
||||
export { useSidebarStore } from './use-sidebar-store';
|
||||
export { useDashboardStore } from './use-dashboard-store';
|
||||
export { useTimerStore } from './use-timer-store';
|
||||
export { useFilterStore } from './use-filter-store';
|
||||
export { useKeyboardShortcutsStore } from './use-keyboard-shortcuts-store';
|
||||
@@ -0,0 +1,62 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface WidgetConfig {
|
||||
id: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface DashboardState {
|
||||
widgets: WidgetConfig[];
|
||||
|
||||
// Actions
|
||||
setWidgets: (widgets: WidgetConfig[]) => void;
|
||||
updateWidget: (id: string, updates: Partial<WidgetConfig>) => void;
|
||||
addWidget: (widget: WidgetConfig) => void;
|
||||
removeWidget: (id: string) => void;
|
||||
resetLayout: () => void;
|
||||
}
|
||||
|
||||
const defaultWidgets: WidgetConfig[] = [
|
||||
{ id: 'today-tasks', type: 'TodayTasks', x: 0, y: 0, w: 6, h: 4, visible: true },
|
||||
{ id: 'habit-checklist', type: 'HabitChecklist', x: 6, y: 0, w: 3, h: 4, visible: true },
|
||||
{ id: 'weekly-stats', type: 'WeeklyStats', x: 9, y: 0, w: 3, h: 4, visible: true },
|
||||
{ id: 'project-progress', type: 'ProjectProgress', x: 0, y: 4, w: 4, h: 3, visible: true },
|
||||
{ id: 'habit-streaks', type: 'HabitStreaks', x: 4, y: 4, w: 4, h: 3, visible: true },
|
||||
{ id: 'calendar-mini', type: 'CalendarMini', x: 8, y: 4, w: 4, h: 3, visible: true },
|
||||
{ id: 'quick-add', type: 'QuickAdd', x: 0, y: 7, w: 3, h: 3, visible: true },
|
||||
{ id: 'recent-activity', type: 'RecentActivity', x: 3, y: 7, w: 9, h: 3, visible: true },
|
||||
];
|
||||
|
||||
export const useDashboardStore = create<DashboardState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
widgets: defaultWidgets,
|
||||
|
||||
setWidgets: (widgets) => set({ widgets }),
|
||||
updateWidget: (id, updates) =>
|
||||
set((state) => ({
|
||||
widgets: state.widgets.map((w) =>
|
||||
w.id === id ? { ...w, ...updates } : w
|
||||
),
|
||||
})),
|
||||
addWidget: (widget) =>
|
||||
set((state) => ({
|
||||
widgets: [...state.widgets, widget],
|
||||
})),
|
||||
removeWidget: (id) =>
|
||||
set((state) => ({
|
||||
widgets: state.widgets.filter((w) => w.id !== id),
|
||||
})),
|
||||
resetLayout: () => set({ widgets: defaultWidgets }),
|
||||
}),
|
||||
{
|
||||
name: 'project-e-dashboard',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface FilterState {
|
||||
domain: string | null;
|
||||
project: string | null;
|
||||
tags: string[];
|
||||
status: string | null;
|
||||
priority: string | null;
|
||||
|
||||
// Actions
|
||||
setDomain: (domain: string | null) => void;
|
||||
setProject: (project: string | null) => void;
|
||||
setTags: (tags: string[]) => void;
|
||||
addTag: (tag: string) => void;
|
||||
removeTag: (tag: string) => void;
|
||||
setStatus: (status: string | null) => void;
|
||||
setPriority: (priority: string | null) => void;
|
||||
clearAll: () => void;
|
||||
}
|
||||
|
||||
export const useFilterStore = create<FilterState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
domain: null,
|
||||
project: null,
|
||||
tags: [],
|
||||
status: null,
|
||||
priority: null,
|
||||
|
||||
setDomain: (domain) => set({ domain }),
|
||||
setProject: (project) => set({ project }),
|
||||
setTags: (tags) => set({ tags }),
|
||||
addTag: (tag) =>
|
||||
set((state) => ({
|
||||
tags: state.tags.includes(tag) ? state.tags : [...state.tags, tag],
|
||||
})),
|
||||
removeTag: (tag) =>
|
||||
set((state) => ({
|
||||
tags: state.tags.filter((t) => t !== tag),
|
||||
})),
|
||||
setStatus: (status) => set({ status }),
|
||||
setPriority: (priority) => set({ priority }),
|
||||
clearAll: () =>
|
||||
set({
|
||||
domain: null,
|
||||
project: null,
|
||||
tags: [],
|
||||
status: null,
|
||||
priority: null,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'project-e-filters',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface Shortcut {
|
||||
key: string;
|
||||
description: string;
|
||||
action: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface KeyboardShortcutsState {
|
||||
enabled: boolean;
|
||||
shortcuts: Shortcut[];
|
||||
|
||||
// Actions
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
updateShortcut: (key: string, updates: Partial<Shortcut>) => void;
|
||||
resetShortcuts: () => void;
|
||||
}
|
||||
|
||||
const defaultShortcuts: Shortcut[] = [
|
||||
{ key: 'g+d', description: 'Go to Dashboard', action: 'navigate_dashboard', enabled: true },
|
||||
{ key: 'g+t', description: 'Go to Tasks', action: 'navigate_tasks', enabled: true },
|
||||
{ key: 'g+h', description: 'Go to Habits', action: 'navigate_habits', enabled: true },
|
||||
{ key: 'g+p', description: 'Go to Projects', action: 'navigate_projects', enabled: true },
|
||||
{ key: 'g+n', description: 'Go to Notes', action: 'navigate_notes', enabled: true },
|
||||
{ key: 'g+r', description: 'Go to Reports', action: 'navigate_reports', enabled: true },
|
||||
{ key: 'g+c', description: 'Go to Calendar', action: 'navigate_calendar', enabled: true },
|
||||
{ key: 'g+a', description: 'Go to Analytics', action: 'navigate_analytics', enabled: true },
|
||||
{ key: '/', description: 'Focus search', action: 'focus_search', enabled: true },
|
||||
{ key: 'n', description: 'Quick add', action: 'quick_add', enabled: true },
|
||||
{ key: '?', description: 'Show shortcuts help', action: 'show_help', enabled: true },
|
||||
{ key: 'j', description: 'Move down', action: 'move_down', enabled: true },
|
||||
{ key: 'k', description: 'Move up', action: 'move_up', enabled: true },
|
||||
{ key: 'enter', description: 'Open selected', action: 'open', enabled: true },
|
||||
{ key: 'e', description: 'Edit selected', action: 'edit', enabled: true },
|
||||
{ key: 'space', description: 'Toggle complete', action: 'toggle_complete', enabled: true },
|
||||
{ key: 'c', description: 'Complete selected', action: 'complete', enabled: true },
|
||||
{ key: 'x', description: 'Cancel', action: 'cancel', enabled: true },
|
||||
];
|
||||
|
||||
export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
enabled: true,
|
||||
shortcuts: defaultShortcuts,
|
||||
|
||||
setEnabled: (enabled) => set({ enabled }),
|
||||
updateShortcut: (key, updates) =>
|
||||
set((state) => ({
|
||||
shortcuts: state.shortcuts.map((s) =>
|
||||
s.key === key ? { ...s, ...updates } : s
|
||||
),
|
||||
})),
|
||||
resetShortcuts: () => set({ shortcuts: defaultShortcuts }),
|
||||
}),
|
||||
{
|
||||
name: 'project-e-keyboard-shortcuts',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface SidebarState {
|
||||
collapsed: boolean;
|
||||
mobileOpen: boolean;
|
||||
|
||||
// Actions
|
||||
toggle: () => void;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
setMobileOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useSidebarStore = create<SidebarState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
collapsed: false,
|
||||
mobileOpen: false,
|
||||
|
||||
toggle: () => set((state) => ({ collapsed: !state.collapsed })),
|
||||
setCollapsed: (collapsed) => set({ collapsed }),
|
||||
setMobileOpen: (mobileOpen) => set({ mobileOpen }),
|
||||
}),
|
||||
{
|
||||
name: 'project-e-sidebar',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'system';
|
||||
type Density = 'compact' | 'comfortable' | 'spacious';
|
||||
|
||||
interface ThemeState {
|
||||
mode: ThemeMode;
|
||||
accent: string;
|
||||
font: string;
|
||||
density: Density;
|
||||
reducedMotion: boolean;
|
||||
|
||||
// Actions
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
setAccent: (color: string) => void;
|
||||
setFont: (font: string) => void;
|
||||
setDensity: (density: Density) => void;
|
||||
setReducedMotion: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
mode: 'system',
|
||||
accent: '#356bff',
|
||||
font: 'geist',
|
||||
density: 'comfortable',
|
||||
reducedMotion: false,
|
||||
|
||||
setMode: (mode) => set({ mode }),
|
||||
setAccent: (accent) => set({ accent }),
|
||||
setFont: (font) => set({ font }),
|
||||
setDensity: (density) => set({ density }),
|
||||
setReducedMotion: (reducedMotion) => set({ reducedMotion }),
|
||||
}),
|
||||
{
|
||||
name: 'project-e-theme',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type TimerMode = 'focus' | 'break' | 'idle';
|
||||
|
||||
interface TimerState {
|
||||
mode: TimerMode;
|
||||
isRunning: boolean;
|
||||
secondsRemaining: number;
|
||||
focusMinutes: number;
|
||||
breakMinutes: number;
|
||||
currentTaskId: string | null;
|
||||
|
||||
// Actions
|
||||
startFocus: (taskId?: string) => void;
|
||||
startBreak: () => void;
|
||||
pause: () => void;
|
||||
resume: () => void;
|
||||
reset: () => void;
|
||||
tick: () => void;
|
||||
setFocusMinutes: (minutes: number) => void;
|
||||
setBreakMinutes: (minutes: number) => void;
|
||||
}
|
||||
|
||||
export const useTimerStore = create<TimerState>()((set, get) => ({
|
||||
mode: 'idle',
|
||||
isRunning: false,
|
||||
secondsRemaining: 0,
|
||||
focusMinutes: 25,
|
||||
breakMinutes: 5,
|
||||
currentTaskId: null,
|
||||
|
||||
startFocus: (taskId) =>
|
||||
set({
|
||||
mode: 'focus',
|
||||
isRunning: true,
|
||||
secondsRemaining: get().focusMinutes * 60,
|
||||
currentTaskId: taskId ?? null,
|
||||
}),
|
||||
|
||||
startBreak: () =>
|
||||
set({
|
||||
mode: 'break',
|
||||
isRunning: true,
|
||||
secondsRemaining: get().breakMinutes * 60,
|
||||
}),
|
||||
|
||||
pause: () => set({ isRunning: false }),
|
||||
resume: () => set({ isRunning: true }),
|
||||
|
||||
reset: () =>
|
||||
set({
|
||||
mode: 'idle',
|
||||
isRunning: false,
|
||||
secondsRemaining: 0,
|
||||
currentTaskId: null,
|
||||
}),
|
||||
|
||||
tick: () => {
|
||||
const state = get();
|
||||
if (!state.isRunning || state.secondsRemaining <= 0) {
|
||||
if (state.secondsRemaining <= 0) {
|
||||
set({ isRunning: false, mode: 'idle' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
set({ secondsRemaining: state.secondsRemaining - 1 });
|
||||
},
|
||||
|
||||
setFocusMinutes: (focusMinutes) => set({ focusMinutes }),
|
||||
setBreakMinutes: (breakMinutes) => set({ breakMinutes }),
|
||||
}));
|
||||
Reference in New Issue
Block a user