Skip to content

Commit

Permalink
wip: sidebar context menu
Browse files Browse the repository at this point in the history
  • Loading branch information
cloverich committed Jul 20, 2024
1 parent 44aa5ce commit cd5a715
Show file tree
Hide file tree
Showing 17 changed files with 1,134 additions and 426 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@
"devDependencies": {
"@electron/packager": "^18.1.3",
"@electron/rebuild": "^3.4.1",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@types/better-sqlite3": "^5.4.0",
"@types/chai": "^4.3.11",
"@types/klaw": "^3.0.1",
Expand Down
140 changes: 140 additions & 0 deletions src/components/Sidesheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@udecode/cn";

const Sheet = SheetPrimitive.Root;

const SheetTrigger = SheetPrimitive.Trigger;

const SheetClose = SheetPrimitive.Close;

const SheetPortal = SheetPrimitive.Portal;

const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
// Initial use case is for side bar (journal selection) -- imo looks better when backgroud is not faded / covered w/ animation; may want to re-enable
// for other use cases
// "fixed inset-0 z-50 bg-slate-400 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;

const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);

interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));

SheetContent.displayName = SheetPrimitive.Content.displayName;

const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
);
SheetHeader.displayName = "SheetHeader";

const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";

const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;

const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;

export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
8 changes: 7 additions & 1 deletion src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
ExternalLink,
Eye,
FileCode,
Folder,
FolderArchive,
FolderCheck,
GripVertical,
Heading1,
Heading2,
Expand Down Expand Up @@ -196,14 +199,16 @@ export const Icons = {
color: Baseline,
column: RectangleVertical,
combine: Combine,
ungroup: Ungroup,
comment: MessageSquare,
commentAdd: MessageSquarePlus,
delete: Trash2,
dragHandle: GripVertical,
editing: Edit2,
emoji: Smile,
externalLink: ExternalLink,
folder: Folder,
folderArchive: FolderArchive,
folderCheck: FolderCheck,
h1: Heading1,
h2: Heading2,
h3: Heading3,
Expand Down Expand Up @@ -234,6 +239,7 @@ export const Icons = {
trash: Trash2,
ul: List,
underline: Underline,
ungroup: Ungroup,
unlink: Link2Off,
viewing: Eye,

Expand Down
2 changes: 0 additions & 2 deletions src/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useState, useEffect, useContext, Fragment } from "react";
import { observer } from "mobx-react-lite";
import Layout, { LayoutDummy } from "./layout";
import Preferences from "./views/preferences";
import Journals from "./views/journals";
import Documents from "./views/documents";
import Editor from "./views/edit";
import DocumentCreator from "./views/create";
Expand Down Expand Up @@ -40,7 +39,6 @@ export default observer(function Container() {
<JournalsStoreContext.Provider value={journalsStore!}>
<Layout>
<Routes>
<Route path="journals" element={<Journals />} />
<Route path="preferences" element={<Preferences />} />
<Route path="documents" element={<SearchProvider />}>
<Route index element={<Documents />} />
Expand Down
44 changes: 43 additions & 1 deletion src/hooks/stores/journals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,31 @@ export class JournalsStore {
}
};

validateName = (name: string) => {
name = name.trim();
if (!name) return ["Journal name cannot be empty", name];
if (name.length > 25)
return ["Journal name cannot be longer than 25 characters", name];

if (this.journals.find((j) => j.name === name)) {
return ["Journal with that name already exists", name];
}

return [null, name];
};

create = async ({ name }: { name: string }) => {
this.saving = true;
this.error = null;

try {
const [err, validName] = this.validateName(name);
if (err) throw new Error(err);

const newJournal = await this.client.journals.create({
name: name,
name: validName!,
});

this.journals.push(newJournal);
} catch (err: any) {
console.error(err);
Expand All @@ -96,6 +114,28 @@ export class JournalsStore {
}
};

updateName = async (journalId: string, name: string) => {
this.saving = true;
try {
const [err, validName] = this.validateName(name);
if (err) throw new Error(err);

const journal = this.journals.find((j) => j.id === journalId);
if (!journal) throw new Error(`Journal not found: ${journalId}`);

const updatedAttrs = await this.client.journals.update({
id: journalId,
name: validName!,
});
Object.assign(journal, updatedAttrs);
} catch (err: any) {
console.error(`Error updating journal name for ${journalId}:`, err);
throw err;
} finally {
this.saving = false;
}
};

toggleArchive = async (journal: JournalResponse) => {
this.saving = true;

Expand Down Expand Up @@ -129,6 +169,8 @@ export class JournalsStore {
* default when creating a new document, if no journal is selected.
*/
setDefault = async (journalId: string) => {
if (this.defaultJournalId === journalId) return;

this.saving = true;
try {
await this.client.preferences.setDefaultJournal(journalId);
Expand Down
3 changes: 0 additions & 3 deletions src/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ export default function Layout(props: Props2) {
<NavLink to="documents" className={classnameFunc}>
documents
</NavLink>
<NavLink to="journals" className={classnameFunc}>
journals
</NavLink>
<NavLink to="preferences" className={classnameFunc}>
preferences
</NavLink>
Expand Down
23 changes: 22 additions & 1 deletion src/preload/client/journals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class JournalsClient {
constructor(private db: Database) {}

list = async (): Promise<JournalResponse[]> => {
return this.db.prepare("select * from journals").all();
return this.db.prepare("select * from journals order by name").all();
};

create = (journal: { name: string }): Promise<JournalResponse> => {
Expand All @@ -35,6 +35,27 @@ export class JournalsClient {
return this.db.prepare("select * from journals where id = :id").get({ id });
};

update = (journal: {
id: string;
name: string;
}): Promise<JournalResponse> => {
if (!journal.name?.trim()) throw new Error("Journal name cannot be empty");

this.db
.prepare(
"update journals set name = :name, updatedAt = :updatedAt where id = :id",
)
.run({
name: journal.name,
id: journal.id,
updatedAt: new Date().toISOString(),
});

return this.db
.prepare("select * from journals where id = :id")
.get({ id: journal.id });
};

remove = (journal: { id: string }): Promise<JournalResponse[]> => {
// TODO: ensure there is always at least one journal. Deleting the last journal breaks the app.
this.db
Expand Down
26 changes: 26 additions & 0 deletions src/views/documents/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from "react";

import { cn } from "@udecode/cn";

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex w-full rounded-md border border-input bg-transparent text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);

Input.displayName = "Input";

export { Input };
Loading

0 comments on commit cd5a715

Please sign in to comment.