diff --git a/bun.lockb b/bun.lockb index 69ef0f39..dc8827a8 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/web/app/(pages)/tasks/page.tsx b/web/app/(pages)/tasks/page.tsx new file mode 100644 index 00000000..cabad9b4 --- /dev/null +++ b/web/app/(pages)/tasks/page.tsx @@ -0,0 +1,15 @@ +import { TaskRoute } from "@/components/routes/task/TaskRoute" +import { currentUser } from "@clerk/nextjs/server" +import { notFound } from "next/navigation" +import { get } from "ronin" + +export default async function TaskPage() { + const user = await currentUser() + const flag = await get.featureFlag.with.name("TASK") + + if (!user?.emailAddresses.some(email => flag?.emails.includes(email.emailAddress))) { + notFound() + } + + return +} diff --git a/web/app/actions.ts b/web/app/actions.ts index 14795825..42f598f6 100644 --- a/web/app/actions.ts +++ b/web/app/actions.ts @@ -10,6 +10,19 @@ import { ZSAError } from "zsa" const MAX_FILE_SIZE = 1 * 1024 * 1024 const ALLOWED_FILE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"] +export const getFeatureFlag = authedProcedure + .input( + z.object({ + name: z.string() + }) + ) + .handler(async ({ input }) => { + const { name } = input + const flag = await get.featureFlag.with.name(name) + + return { flag } + }) + export const sendFeedback = authedProcedure .input( z.object({ diff --git a/web/components/custom/sidebar/partial/task-section.tsx b/web/components/custom/sidebar/partial/task-section.tsx new file mode 100644 index 00000000..203100f7 --- /dev/null +++ b/web/components/custom/sidebar/partial/task-section.tsx @@ -0,0 +1,149 @@ +import Link from "next/link" +import { usePathname } from "next/navigation" +import { cn } from "@/lib/utils" +import { ListOfTasks } from "@/lib/schema/tasks" +import { LaIcon } from "../../la-icon" +import { useEffect, useState } from "react" +import { useAuth, useUser } from "@clerk/nextjs" +import { getFeatureFlag } from "@/app/actions" + +export const TaskSection: React.FC<{ pathname: string }> = ({ pathname }) => { + const me = { root: { tasks: [{ id: "1", title: "Test Task" }] } } + + const taskCount = me?.root.tasks?.length || 0 + const isActive = pathname === "/tasks" + + const [isFetching, setIsFetching] = useState(false) + const [isFeatureActive, setIsFeatureActive] = useState(false) + const { isLoaded, isSignedIn } = useAuth() + const { user } = useUser() + + useEffect(() => { + async function checkFeatureFlag() { + setIsFetching(true) + + if (isLoaded && isSignedIn) { + const [data, err] = await getFeatureFlag({ name: "TASK" }) + + if (err) { + console.error(err) + setIsFetching(false) + return + } + + if (user?.emailAddresses.some(email => data.flag?.emails.includes(email.emailAddress))) { + setIsFeatureActive(true) + } + setIsFetching(false) + } + } + + checkFeatureFlag() + }, [isLoaded, isSignedIn, user]) + + if (!isLoaded || !isSignedIn) { + return
Loading...
+ } + + if (!me) return null + + if (!isFeatureActive) { + return null + } + + return ( +
+ + {isFetching ? ( +
Fetching tasks...
+ ) : ( + + )} +
+ ) +} + +interface TaskSectionHeaderProps { + taskCount: number + isActive: boolean +} + +const TaskSectionHeader: React.FC = ({ taskCount, isActive }) => ( +
+ +

+ Tasks + {taskCount > 0 && {taskCount}} +

+ +
+ //
+ // + //
+) + +interface ListProps { + tasks: ListOfTasks +} + +const List: React.FC = ({ tasks }) => { + const pathname = usePathname() + + return ( +
+ +
+ ) +} + +interface ListItemProps { + label: string + href: string + count: number + isActive: boolean +} + +const ListItem: React.FC = ({ label, href, count, isActive }) => ( +
+
+ +
+ +

{label}

+
+ + {count > 0 && ( + {count} + )} +
+
+) diff --git a/web/components/custom/sidebar/sidebar.tsx b/web/components/custom/sidebar/sidebar.tsx index 64a4f9ae..b1f70df1 100644 --- a/web/components/custom/sidebar/sidebar.tsx +++ b/web/components/custom/sidebar/sidebar.tsx @@ -13,6 +13,7 @@ import { LinkSection } from "./partial/link-section" import { PageSection } from "./partial/page-section" import { TopicSection } from "./partial/topic-section" import { ProfileSection } from "./partial/profile-section" +import { TaskSection } from "./partial/task-section" import { useAccountOrGuest } from "@/lib/providers/jazz-provider" import { LaIcon } from "../la-icon" @@ -114,21 +115,20 @@ const SidebarContent: React.FC = React.memo(() => { const pathname = usePathname() return ( - <> - ) }) diff --git a/web/components/routes/task/TaskForm.tsx b/web/components/routes/task/TaskForm.tsx new file mode 100644 index 00000000..990170a7 --- /dev/null +++ b/web/components/routes/task/TaskForm.tsx @@ -0,0 +1,114 @@ +"use client" +import { useState, useEffect, useRef } from "react" +import { motion, AnimatePresence } from "framer-motion" +import { ListOfTasks, Task } from "@/lib/schema/tasks" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { useAccount } from "@/lib/providers/jazz-provider" +import { LaIcon } from "@/components/custom/la-icon" +import { Checkbox } from "@/components/ui/checkbox" +import { format } from "date-fns" + +interface TaskFormProps {} + +export const TaskForm: React.FC = ({}) => { + const [title, setTitle] = useState("") + const [inputVisible, setInputVisible] = useState(false) + const { me } = useAccount({ root: {} }) + const inputRef = useRef(null) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (title.trim()) { + if (me?.root?.tasks === undefined) { + if (!me) return + me.root.tasks = ListOfTasks.create([], { owner: me }) + } + + const newTask = Task.create( + { + title, + description: "", + status: "todo", + createdAt: new Date() + // updatedAt: new Date() + }, + { owner: me._owner } + ) + me.root.tasks?.push(newTask) + resetForm() + } + } + + const resetForm = () => { + setTitle("") + setInputVisible(false) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + resetForm() + } else if (e.key === "Backspace" && title.trim() === "") { + resetForm() + } + } + + useEffect(() => { + if (inputVisible && inputRef.current) { + inputRef.current.focus() + } + }, [inputVisible]) + + const formattedDate = format(new Date(), "EEE, MMMM do, yyyy") + + return ( +
+ + {!inputVisible ? ( + + + + ) : ( + +
+ {}} /> + setTitle(e.target.value)} + onKeyDown={handleKeyDown} + // placeholder="Task title" + /> +
+
+ {formattedDate} +
+
+ )} +
+
+ ) +} diff --git a/web/components/routes/task/TaskItem.tsx b/web/components/routes/task/TaskItem.tsx new file mode 100644 index 00000000..e4fd058c --- /dev/null +++ b/web/components/routes/task/TaskItem.tsx @@ -0,0 +1,26 @@ +import { Task } from "@/lib/schema/tasks" +import { Checkbox } from "@/components/ui/checkbox" +import { format } from "date-fns" + +interface TaskItemProps { + task: Task + onUpdateTask: (taskId: string, updates: Partial) => void +} + +export const TaskItem: React.FC = ({ task, onUpdateTask }) => { + const statusChange = (checked: boolean) => { + onUpdateTask(task.id, { status: checked ? "done" : "todo" }) + } + + const formattedDate = format(new Date(task.createdAt), "EEE, MMMM do, yyyy") + + return ( +
  • +
    + +

    {task.title}

    +
    + {formattedDate} +
  • + ) +} diff --git a/web/components/routes/task/TaskList.tsx b/web/components/routes/task/TaskList.tsx new file mode 100644 index 00000000..8478dae3 --- /dev/null +++ b/web/components/routes/task/TaskList.tsx @@ -0,0 +1,23 @@ +import React from "react" +import { ListOfTasks, Task } from "@/lib/schema/tasks" +import { TaskItem } from "./TaskItem" + +interface TaskListProps { + tasks?: ListOfTasks + onUpdateTask: (taskId: string, updates: Partial) => void +} + +export const TaskList: React.FC = ({ tasks, onUpdateTask }) => { + return ( +
      + {tasks?.map( + task => + task?.id && ( +
    • + +
    • + ) + )} +
    + ) +} diff --git a/web/components/routes/task/TaskRoute.tsx b/web/components/routes/task/TaskRoute.tsx new file mode 100644 index 00000000..a2efe5ec --- /dev/null +++ b/web/components/routes/task/TaskRoute.tsx @@ -0,0 +1,33 @@ +"use client" + +import { useAccount } from "@/lib/providers/jazz-provider" +import { Task } from "@/lib/schema/tasks" +import { TaskList } from "./TaskList" +import { TaskForm } from "./TaskForm" +import { LaIcon } from "@/components/custom/la-icon" + +export const TaskRoute: React.FC = () => { + const { me } = useAccount({ root: { tasks: [] } }) + const tasks = me?.root.tasks + console.log(tasks, "tasks here") + + const updateTask = (taskId: string, updates: Partial) => { + if (me?.root?.tasks) { + const taskIndex = me.root.tasks.findIndex(task => task?.id === taskId) + if (taskIndex !== -1) { + Object.assign(me.root.tasks[taskIndex]!, updates) + } + } + } + + return ( +
    +
    + +

    Current Tasks

    +
    + + +
    + ) +} diff --git a/web/lib/schema/index.ts b/web/lib/schema/index.ts index 12ebd60e..8b15d634 100644 --- a/web/lib/schema/index.ts +++ b/web/lib/schema/index.ts @@ -12,6 +12,7 @@ import { CoMap, co, Account, Profile } from "jazz-tools" import { PersonalPageLists } from "./personal-page" import { PersonalLinkLists } from "./personal-link" import { ListOfTopics } from "./master/topic" +import { ListOfTasks } from "./tasks" declare module "jazz-tools" { interface Profile { @@ -32,6 +33,8 @@ export class UserRoot extends CoMap { topicsWantToLearn = co.ref(ListOfTopics) topicsLearning = co.ref(ListOfTopics) topicsLearned = co.ref(ListOfTopics) + + tasks = co.ref(ListOfTasks) } export class LaAccount extends Account { @@ -59,7 +62,9 @@ export class LaAccount extends Account { topicsWantToLearn: ListOfTopics.create([], { owner: this }), topicsLearning: ListOfTopics.create([], { owner: this }), - topicsLearned: ListOfTopics.create([], { owner: this }) + topicsLearned: ListOfTopics.create([], { owner: this }), + + tasks: ListOfTasks.create([], { owner: this }) }, { owner: this } ) diff --git a/web/lib/schema/tasks.ts b/web/lib/schema/tasks.ts new file mode 100644 index 00000000..f96b8df9 --- /dev/null +++ b/web/lib/schema/tasks.ts @@ -0,0 +1,12 @@ +import { co, CoList, CoMap, Encoders } from "jazz-tools" + +export class Task extends CoMap { + title = co.string + description = co.optional.string + status = co.literal("todo", "in_progress", "done") + createdAt = co.encoded(Encoders.Date) + // updatedAt = co.encoded(Encoders.Date) + completedAt = co.optional.encoded(Encoders.Date) +} + +export class ListOfTasks extends CoList.Of(co.ref(Task)) {} diff --git a/web/middleware.ts b/web/middleware.ts index 9081e846..32be843a 100644 --- a/web/middleware.ts +++ b/web/middleware.ts @@ -10,7 +10,8 @@ const ROUTE_PATTERNS = { "/search(.*)", "/settings(.*)", "/tauri(.*)", - "/onboarding(.*)" + "/onboarding(.*)", + "/tasks(.*)" ] } diff --git a/web/package.json b/web/package.json index 0135be5a..ae4a70da 100644 --- a/web/package.json +++ b/web/package.json @@ -1,128 +1,128 @@ { - "name": "web", - "version": "0.1.0", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "jest" - }, - "dependencies": { - "@clerk/nextjs": "^5.6.0", - "@dnd-kit/core": "^6.1.0", - "@dnd-kit/modifiers": "^7.0.0", - "@dnd-kit/sortable": "^8.0.0", - "@hookform/resolvers": "^3.9.0", - "@nothing-but/force-graph": "^0.9.5", - "@nothing-but/utils": "^0.16.0", - "@omit/react-confirm-dialog": "^1.1.5", - "@omit/react-fancy-switch": "^0.1.3", - "@radix-ui/react-alert-dialog": "^1.1.1", - "@radix-ui/react-avatar": "^1.1.0", - "@radix-ui/react-checkbox": "^1.1.1", - "@radix-ui/react-context-menu": "^2.2.1", - "@radix-ui/react-dialog": "^1.1.1", - "@radix-ui/react-dismissable-layer": "^1.1.0", - "@radix-ui/react-dropdown-menu": "^2.1.1", - "@radix-ui/react-focus-scope": "^1.1.0", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-popover": "^1.1.1", - "@radix-ui/react-scroll-area": "^1.1.0", - "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.0", - "@radix-ui/react-toggle": "^1.1.0", - "@radix-ui/react-toggle-group": "^1.1.0", - "@radix-ui/react-tooltip": "^1.1.2", - "@sentry/nextjs": "^8.30.0", - "@tanstack/react-virtual": "^3.10.8", - "@tiptap/core": "^2.7.2", - "@tiptap/extension-blockquote": "^2.7.2", - "@tiptap/extension-bold": "^2.7.2", - "@tiptap/extension-bullet-list": "^2.7.2", - "@tiptap/extension-code": "^2.7.2", - "@tiptap/extension-code-block-lowlight": "^2.7.2", - "@tiptap/extension-color": "^2.7.2", - "@tiptap/extension-document": "^2.7.2", - "@tiptap/extension-dropcursor": "^2.7.2", - "@tiptap/extension-focus": "^2.7.2", - "@tiptap/extension-gapcursor": "^2.7.2", - "@tiptap/extension-hard-break": "^2.7.2", - "@tiptap/extension-heading": "^2.7.2", - "@tiptap/extension-history": "^2.7.2", - "@tiptap/extension-horizontal-rule": "^2.7.2", - "@tiptap/extension-image": "^2.7.2", - "@tiptap/extension-italic": "^2.7.2", - "@tiptap/extension-link": "^2.7.2", - "@tiptap/extension-list-item": "^2.7.2", - "@tiptap/extension-ordered-list": "^2.7.2", - "@tiptap/extension-paragraph": "^2.7.2", - "@tiptap/extension-placeholder": "^2.7.2", - "@tiptap/extension-strike": "^2.7.2", - "@tiptap/extension-task-item": "^2.7.2", - "@tiptap/extension-task-list": "^2.7.2", - "@tiptap/extension-text": "^2.7.2", - "@tiptap/extension-typography": "^2.7.2", - "@tiptap/pm": "^2.7.2", - "@tiptap/react": "^2.7.2", - "@tiptap/starter-kit": "^2.7.2", - "@tiptap/suggestion": "^2.7.2", - "axios": "^1.7.7", - "cheerio": "1.0.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "framer-motion": "^11.5.6", - "geist": "^1.3.1", - "jazz-browser-auth-clerk": "0.8.0", - "jazz-react": "0.8.0", - "jazz-react-auth-clerk": "0.8.0", - "jazz-tools": "0.8.0", - "jotai": "^2.10.0", - "lowlight": "^3.1.0", - "lucide-react": "^0.429.0", - "next": "14.2.10", - "next-themes": "^0.3.0", - "nuqs": "^1.19.1", - "query-string": "^9.1.0", - "react": "^18.3.1", - "react-day-picker": "^8.10.1", - "react-dom": "^18.3.1", - "react-hook-form": "^7.53.0", - "react-textarea-autosize": "^8.5.3", - "ronin": "^4.3.1", - "slugify": "^1.6.6", - "sonner": "^1.5.0", - "streaming-markdown": "^0.0.14", - "tailwind-merge": "^2.5.2", - "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.4", - "zod": "^3.23.8", - "zsa": "^0.6.0", - "zsa-react": "^0.2.3" - }, - "devDependencies": { - "@ronin/learn-anything": "0.0.0-3452357373461", - "@testing-library/jest-dom": "^6.5.0", - "@testing-library/react": "^16.0.1", - "@types/jest": "^29.5.13", - "@types/node": "^22.5.5", - "@types/react": "^18.3.8", - "@types/react-dom": "^18.3.0", - "dotenv": "^16.4.5", - "eslint": "^8.57.1", - "eslint-config-next": "14.2.5", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "postcss": "^8.4.47", - "prettier-plugin-tailwindcss": "^0.6.6", - "tailwindcss": "^3.4.12", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.2" - } + "name": "web", + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "jest" + }, + "dependencies": { + "@clerk/nextjs": "^5.6.0", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@hookform/resolvers": "^3.9.0", + "@nothing-but/force-graph": "^0.9.5", + "@nothing-but/utils": "^0.16.0", + "@omit/react-confirm-dialog": "^1.1.5", + "@omit/react-fancy-switch": "^0.1.3", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dismissable-layer": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-focus-scope": "^1.1.0", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "@sentry/nextjs": "^8.31.0", + "@tanstack/react-virtual": "^3.10.8", + "@tiptap/core": "^2.7.2", + "@tiptap/extension-blockquote": "^2.7.2", + "@tiptap/extension-bold": "^2.7.2", + "@tiptap/extension-bullet-list": "^2.7.2", + "@tiptap/extension-code": "^2.7.2", + "@tiptap/extension-code-block-lowlight": "^2.7.2", + "@tiptap/extension-color": "^2.7.2", + "@tiptap/extension-document": "^2.7.2", + "@tiptap/extension-dropcursor": "^2.7.2", + "@tiptap/extension-focus": "^2.7.2", + "@tiptap/extension-gapcursor": "^2.7.2", + "@tiptap/extension-hard-break": "^2.7.2", + "@tiptap/extension-heading": "^2.7.2", + "@tiptap/extension-history": "^2.7.2", + "@tiptap/extension-horizontal-rule": "^2.7.2", + "@tiptap/extension-image": "^2.7.2", + "@tiptap/extension-italic": "^2.7.2", + "@tiptap/extension-link": "^2.7.2", + "@tiptap/extension-list-item": "^2.7.2", + "@tiptap/extension-ordered-list": "^2.7.2", + "@tiptap/extension-paragraph": "^2.7.2", + "@tiptap/extension-placeholder": "^2.7.2", + "@tiptap/extension-strike": "^2.7.2", + "@tiptap/extension-task-item": "^2.7.2", + "@tiptap/extension-task-list": "^2.7.2", + "@tiptap/extension-text": "^2.7.2", + "@tiptap/extension-typography": "^2.7.2", + "@tiptap/pm": "^2.7.2", + "@tiptap/react": "^2.7.2", + "@tiptap/starter-kit": "^2.7.2", + "@tiptap/suggestion": "^2.7.2", + "axios": "^1.7.7", + "cheerio": "1.0.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "framer-motion": "^11.5.6", + "geist": "^1.3.1", + "jazz-browser-auth-clerk": "0.8.0", + "jazz-react": "0.8.0", + "jazz-react-auth-clerk": "0.8.0", + "jazz-tools": "0.8.0", + "jotai": "^2.10.0", + "lowlight": "^3.1.0", + "lucide-react": "^0.429.0", + "next": "14.2.10", + "next-themes": "^0.3.0", + "nuqs": "^1.19.2", + "query-string": "^9.1.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-textarea-autosize": "^8.5.3", + "ronin": "^4.3.1", + "slugify": "^1.6.6", + "sonner": "^1.5.0", + "streaming-markdown": "^0.0.14", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.4", + "zod": "^3.23.8", + "zsa": "^0.6.0", + "zsa-react": "^0.2.3" + }, + "devDependencies": { + "@ronin/learn-anything": "^0.0.0-3453250405724", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@types/jest": "^29.5.13", + "@types/node": "^22.6.1", + "@types/react": "^18.3.8", + "@types/react-dom": "^18.3.0", + "dotenv": "^16.4.5", + "eslint": "^8.57.1", + "eslint-config-next": "14.2.5", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss": "^8.4.47", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.13", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.6.2" + } }