From 37f49a7b6ca773a51913e61efc8b46f76fadbc38 Mon Sep 17 00:00:00 2001 From: Oleksandr Maslakov Date: Mon, 17 Mar 2025 13:02:37 +0200 Subject: [PATCH 1/2] v2 init --- .prettierrc.json | 1 + .vscode/settings.json | 5 + _frontend/components/app-sidebar.tsx | 214 + _frontend/components/app/app.js | 14 - _frontend/components/app/index.tsx | 13 + _frontend/components/ui/accordion.tsx | 64 + _frontend/components/ui/alert-dialog.tsx | 155 + _frontend/components/ui/button.tsx | 59 + _frontend/components/ui/dropdown-menu.tsx | 255 ++ _frontend/components/ui/input.tsx | 21 + _frontend/components/ui/popover.tsx | 46 + _frontend/components/ui/separator.tsx | 28 + _frontend/components/ui/sheet.tsx | 137 + _frontend/components/ui/sidebar.tsx | 724 +++ _frontend/components/ui/skeleton.tsx | 13 + _frontend/components/ui/tooltip.tsx | 61 + _frontend/entrypoints/application.css | 20 +- _frontend/entrypoints/application.js | 29 - _frontend/entrypoints/application.ts | 9 + _frontend/hooks/use-mobile.ts | 19 + _frontend/lib/utils.ts | 6 + _frontend/source-providers/SourceProvider.ts | 49 + .../source-providers/local-storage/index.ts | 37 + _frontend/styles/globals.css | 124 + _includes/custom-head.html | 2 +- _layouts/default.html | 11 +- components.json | 21 + eslint.config.js | 33 + index.html | 29 +- package-lock.json | 4047 +++++++++++++---- package.json | 30 +- tsconfig.json | 23 + vite.config.js | 8 + vite.config.ts | 12 - 34 files changed, 5213 insertions(+), 1106 deletions(-) create mode 100644 .prettierrc.json create mode 100644 .vscode/settings.json create mode 100644 _frontend/components/app-sidebar.tsx delete mode 100644 _frontend/components/app/app.js create mode 100644 _frontend/components/app/index.tsx create mode 100644 _frontend/components/ui/accordion.tsx create mode 100644 _frontend/components/ui/alert-dialog.tsx create mode 100644 _frontend/components/ui/button.tsx create mode 100644 _frontend/components/ui/dropdown-menu.tsx create mode 100644 _frontend/components/ui/input.tsx create mode 100644 _frontend/components/ui/popover.tsx create mode 100644 _frontend/components/ui/separator.tsx create mode 100644 _frontend/components/ui/sheet.tsx create mode 100644 _frontend/components/ui/sidebar.tsx create mode 100644 _frontend/components/ui/skeleton.tsx create mode 100644 _frontend/components/ui/tooltip.tsx delete mode 100644 _frontend/entrypoints/application.js create mode 100644 _frontend/entrypoints/application.ts create mode 100644 _frontend/hooks/use-mobile.ts create mode 100644 _frontend/lib/utils.ts create mode 100644 _frontend/source-providers/SourceProvider.ts create mode 100644 _frontend/source-providers/local-storage/index.ts create mode 100644 _frontend/styles/globals.css create mode 100644 components.json create mode 100644 eslint.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.js delete mode 100644 vite.config.ts diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5ac1c06 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/_frontend/components/app-sidebar.tsx b/_frontend/components/app-sidebar.tsx new file mode 100644 index 0000000..4f5c441 --- /dev/null +++ b/_frontend/components/app-sidebar.tsx @@ -0,0 +1,214 @@ +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; +import { + ChevronsUpDown, + FileJson, + FileText, + Folder, + FolderPen, + FolderUp, + Github, + MoreHorizontal, + Pencil, + Plus, + PlusIcon, + Trash, +} from "lucide-react"; + +export function AppSidebar() { + return ( + + + + + + + + Github{" "} + jmas/create + + + + + + Local Storage + + + Github jmas/create + + + Google Drive{" "} + jmas.ukraine@gmail.com + + + + + Add source + + + + + + + + + Files + + + + Add folder or file + + + + + Add Folder + + + Add File + + + + + + + _posts + + + + archive + + + + + + + + + + Rename or Move + + + + Delete + + + + + + + post1.md + + + + + + + + + + Edit + + + + Rename or Move + + + + Delete + + + + + + + post2.md + + + + + + + + + + Edit + + + + Rename or Move + + + + Delete + + + + + + + post3.md + + + + + + + + + + Edit + + + + Rename or Move + + + + Delete + + + + + + + .reproserc.json{" "} + Settings + + + + + + + + Add settings + + + + + + + + ); +} diff --git a/_frontend/components/app/app.js b/_frontend/components/app/app.js deleted file mode 100644 index 7a39408..0000000 --- a/_frontend/components/app/app.js +++ /dev/null @@ -1,14 +0,0 @@ -window.app = () => { - return { - init() {}, - - login() { - location.href = "/auth"; - }, - - logout() { - auth.clear(); - location.reload(); - }, - }; -}; diff --git a/_frontend/components/app/index.tsx b/_frontend/components/app/index.tsx new file mode 100644 index 0000000..3c79541 --- /dev/null +++ b/_frontend/components/app/index.tsx @@ -0,0 +1,13 @@ +import { AppSidebar } from "@/components/app-sidebar"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; + +export const App = () => { + return ( + + +
+ +
+
+ ); +}; diff --git a/_frontend/components/ui/accordion.tsx b/_frontend/components/ui/accordion.tsx new file mode 100644 index 0000000..d21b65f --- /dev/null +++ b/_frontend/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/_frontend/components/ui/alert-dialog.tsx b/_frontend/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..935eecf --- /dev/null +++ b/_frontend/components/ui/alert-dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/_frontend/components/ui/button.tsx b/_frontend/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/_frontend/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/_frontend/components/ui/dropdown-menu.tsx b/_frontend/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0d82e5c --- /dev/null +++ b/_frontend/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/_frontend/components/ui/input.tsx b/_frontend/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/_frontend/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/_frontend/components/ui/popover.tsx b/_frontend/components/ui/popover.tsx new file mode 100644 index 0000000..83728a8 --- /dev/null +++ b/_frontend/components/ui/popover.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/_frontend/components/ui/separator.tsx b/_frontend/components/ui/separator.tsx new file mode 100644 index 0000000..67c73e5 --- /dev/null +++ b/_frontend/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/_frontend/components/ui/sheet.tsx b/_frontend/components/ui/sheet.tsx new file mode 100644 index 0000000..6906f5b --- /dev/null +++ b/_frontend/components/ui/sheet.tsx @@ -0,0 +1,137 @@ +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/_frontend/components/ui/sidebar.tsx b/_frontend/components/ui/sidebar.tsx new file mode 100644 index 0000000..2239314 --- /dev/null +++ b/_frontend/components/ui/sidebar.tsx @@ -0,0 +1,724 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps, cva } from "class-variance-authority" +import { PanelLeftIcon } from "lucide-react" + +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( +