feat: add personal project list, add project settings, etc

This commit is contained in:
2024-02-24 03:55:50 +00:00
parent 49b80a2f4c
commit 751210c819
31 changed files with 877 additions and 148 deletions
+4 -1
View File
@@ -15,7 +15,7 @@ import trpc from "~/lib/trpc";
import { usePageContext } from "~/renderer/context";
const Navbar = () => {
const {user, urlPathname } = usePageContext();
const { user, urlPathname } = usePageContext();
const logout = trpc.auth.logout.useMutation({
onSuccess() {
window.location.reload();
@@ -46,6 +46,9 @@ const Navbar = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href="/projects">My Projects</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => logout.mutate()}>
Logout
</DropdownMenuItem>
+46
View File
@@ -0,0 +1,46 @@
import Link from "~/renderer/link";
import type { ProjectSchema } from "~/server/db/schema/project";
import type { UserSchema } from "~/server/db/schema/user";
import { Skeleton } from "../ui/skeleton";
type Props = {
project: Omit<ProjectSchema, "settings"> & {
user: UserSchema;
};
};
const ProjectCard = ({ project }: Props) => {
return (
<Link
key={project.id}
href={`/${project.slug}`}
className="border border-white/20 hover:border-white/40 rounded-lg transition-colors overflow-hidden"
>
<div className="w-full aspect-[3/2] bg-gray-900"></div>
<div className="py-2 px-3 flex items-center gap-3">
<div className="size-8 rounded-full bg-white/80"></div>
<div>
<p className="text-md truncate">{project.title}</p>
<p className="text-xs truncate">{project.user.name}</p>
</div>
</div>
</Link>
);
};
const ProjectCardSkeleton = () => (
<div>
<Skeleton className="w-full aspect-[3/2]" />
<div className="py-2 flex items-center gap-3">
<Skeleton className="size-8 rounded-full" />
<div className="flex-1">
<Skeleton className="w-full h-4" />
<Skeleton className="w-full h-4 mt-2" />
</div>
</div>
</div>
);
ProjectCard.Skeleton = ProjectCardSkeleton;
export default ProjectCard;
+82
View File
@@ -0,0 +1,82 @@
import React, { forwardRef, useId } from "react";
import FormField, { FormFieldProps } from "./form-field";
import { cn } from "~/lib/utils";
import { Controller, FieldValues, Path } from "react-hook-form";
import { useFormReturn } from "~/hooks/useForm";
export type CheckboxItem = {
label: string;
value: string;
};
type BaseCheckboxProps = React.ComponentPropsWithoutRef<"input"> &
FormFieldProps & {
inputClassName?: string;
items?: CheckboxItem[] | null;
};
const BaseCheckbox = forwardRef<HTMLInputElement, BaseCheckboxProps>(
(props, ref) => {
const { className, label, error, inputClassName, items, ...restProps } =
props;
const id = useId();
const input = (
<label
htmlFor={id}
className={cn(
"inline-flex items-center gap-2 cursor-pointer",
className
)}
>
<input
ref={ref}
id={id}
type="checkbox"
{...restProps}
className={inputClassName}
/>
{label ? <span>{label}</span> : null}
</label>
);
if (error) {
return <FormField id={id} error={error} children={input} />;
}
return input;
}
);
type CheckboxProps<T extends FieldValues> = Omit<
BaseCheckboxProps,
"form" | "name"
> & {
form?: useFormReturn<T>;
name?: Path<T>;
};
const Checkbox = <T extends FieldValues>(props: CheckboxProps<T>) => {
const { form, ...restProps } = props;
if (form && props.name) {
return (
<Controller
control={form.control}
name={props.name}
render={({ field, fieldState }) => (
<BaseCheckbox
{...restProps}
{...field}
checked={field.value}
error={fieldState.error?.message}
/>
)}
/>
);
}
return <BaseCheckbox {...restProps} />;
};
export default Checkbox;
+1 -1
View File
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 md:p-8 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:rounded-xl dark:border-slate-800 dark:bg-slate-950",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg max-h-dvh overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 md:p-8 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:rounded-xl dark:border-slate-600 dark:bg-slate-800",
className
)}
{...props}
+2 -2
View File
@@ -1,4 +1,4 @@
import { ForwardedRef, forwardRef, useId } from "react";
import { forwardRef, useId } from "react";
import { Controller, FieldValues, Path } from "react-hook-form";
import { useFormReturn } from "~/hooks/useForm";
import { cn } from "~/lib/utils";
@@ -17,7 +17,7 @@ const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/40 dark:bg-background dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
"flex h-10 w-full rounded-md border border-slate-200 px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/40 bg-transparent dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
inputClassName
)}
ref={ref}
+88
View File
@@ -0,0 +1,88 @@
import React, { forwardRef, useId } from "react";
import FormField, { FormFieldProps } from "./form-field";
import { cn } from "~/lib/utils";
import { Controller, FieldValues, Path } from "react-hook-form";
import { useFormReturn } from "~/hooks/useForm";
export type SelectItem = {
label: string;
value: string;
};
type BaseSelectProps = React.ComponentPropsWithoutRef<"select"> &
FormFieldProps & {
inputClassName?: string;
items?: SelectItem[] | null;
};
const BaseSelect = forwardRef<HTMLSelectElement, BaseSelectProps>(
(props, ref) => {
const { className, label, error, inputClassName, items, ...restProps } =
props;
const id = useId();
const input = (
<select
ref={ref}
className={cn(
"h-10 w-full rounded-md border border-slate-200 px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/40 bg-transparent dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
inputClassName
)}
{...restProps}
>
{items?.map((item) => (
<option key={item.value} value={item.value} className="text-gray-900">
{item.label}
</option>
))}
</select>
);
if (label || error) {
return (
<FormField
id={id}
label={label}
error={error}
className={className}
children={input}
/>
);
}
return input;
}
);
type SelectProps<T extends FieldValues> = Omit<
BaseSelectProps,
"form" | "name"
> & {
form?: useFormReturn<T>;
name?: Path<T>;
};
const Select = <T extends FieldValues>(props: SelectProps<T>) => {
const { form, ...restProps } = props;
if (form && props.name) {
return (
<Controller
control={form.control}
name={props.name}
render={({ field, fieldState }) => (
<BaseSelect
{...restProps}
{...field}
value={field.value || ""}
error={fieldState.error?.message}
/>
)}
/>
);
}
return <BaseSelect {...restProps} />;
};
export default Select;
+15
View File
@@ -0,0 +1,15 @@
import { cn } from "~/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-slate-100 dark:bg-slate-800", className)}
{...props}
/>
)
}
export { Skeleton }
+25 -11
View File
@@ -2,7 +2,6 @@ import { cn } from "~/lib/utils";
import React, { useEffect, useMemo, useRef } from "react";
import ActionButton from "./action-button";
import { FiX } from "react-icons/fi";
import FileIcon from "./file-icon";
export type Tab = {
title: string;
@@ -16,9 +15,18 @@ type Props = {
current?: number;
onChange?: (idx: number) => void;
onClose?: (idx: number) => void;
className?: string;
containerClassName?: string;
};
const Tabs = ({ tabs, current = 0, onChange, onClose }: Props) => {
const Tabs = ({
tabs,
current = 0,
onChange,
onClose,
className,
containerClassName,
}: Props) => {
const tabContainerRef = useRef<HTMLDivElement>(null);
const onWheel = (e: WheelEvent) => {
@@ -63,7 +71,12 @@ const Tabs = ({ tabs, current = 0, onChange, onClose }: Props) => {
}, [tabs, current]);
return (
<div className="w-full h-full flex flex-col items-stretch bg-slate-800">
<div
className={cn(
"w-full flex flex-col items-stretch bg-slate-800",
className
)}
>
{tabs.length > 0 ? (
<nav
ref={tabContainerRef}
@@ -77,13 +90,15 @@ const Tabs = ({ tabs, current = 0, onChange, onClose }: Props) => {
icon={tab.icon}
isActive={idx === current}
onSelect={() => onChange && onChange(idx)}
onClose={!tab.locked ? () => onClose && onClose(idx) : null}
onClose={!tab.locked && onClose ? () => onClose(idx) : null}
/>
))}
</nav>
) : null}
<main className="flex-1 overflow-hidden">{tabView}</main>
<main className={cn("flex-1 overflow-hidden", containerClassName)}>
{tabView}
</main>
</div>
);
};
@@ -119,17 +134,16 @@ const TabItem = ({
onClick={onSelect}
>
<button
type="button"
className={cn(
"pl-4 pr-4 truncate flex items-center self-stretch",
onClose ? "pr-0" : ""
)}
>
{icon != null ? (
icon
) : (
<FileIcon file={{ isDirectory: false, filename: title }} />
)}
<span className="inline-block ml-2 truncate">{filename}</span>
{icon}
<span className={cn("inline-block truncate", icon ? "ml-2" : "")}>
{filename}
</span>
<span>{ext}</span>
</button>