Started different page modes

This commit is contained in:
2023-06-09 14:09:49 -05:00
parent 857800e8e4
commit ed69848489
8 changed files with 1381 additions and 96 deletions

1081
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"@radix-ui/react-hover-card": "^1.0.5",
"@radix-ui/react-scroll-area": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.5",
"@tanstack/react-query": "^4.20.0",
"@trpc/client": "^10.9.0",

View File

@@ -0,0 +1,30 @@
import * as Avatar from "@radix-ui/react-avatar";
interface ProfilePictureProps {
name: string | undefined | null;
username: string | undefined | null;
image: string | undefined | null;
}
export const ProfilePicture: React.FC<ProfilePictureProps> = ({
name,
image,
}) => {
// TODO: on hover, show "Name @username" tool tip
// TODO: on click, navigate to profile
return (
<Avatar.Root className="flex min-w-max select-none items-center justify-center overflow-hidden rounded-full bg-quaternary p-2 align-middle shadow-solid-medium">
<Avatar.Image
className="h-10 w-10 rounded-[inherit] object-cover"
src={image ?? undefined}
alt={name ?? "User avatar"}
/>
<Avatar.Fallback
className="leading-1 flex h-10 w-10 items-center justify-center rounded-[inherit] bg-tertiary font-medium"
delayMs={600}
>
{name?.charAt(0)}
</Avatar.Fallback>
</Avatar.Root>
);
};

View File

@@ -0,0 +1,220 @@
import type { User, Project, ProjectPreview } from "@prisma/client";
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
import { PropsWithChildren } from "react";
import * as Toolbar from "@radix-ui/react-toolbar";
import Image from "next/image";
import { MDX } from "@components/MDX";
import { FiEdit3, FiSettings, FiEye } from "react-icons/fi";
import { ProjectPageMode } from "@utils/types/projects";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
// TODO: all icon buttons should have tooltips
/* === Toolbar ===
* Visitor: join, leave a comment/review (later)
* Member: edit, settings
* Editor: save, cancel
* Preview: exit
*/
type FetchedProjectAsProps = {
project:
| (Project & {
author: User;
members: User[];
previews: ProjectPreview[];
})
| null
| undefined;
};
interface DisplayProps extends FetchedProjectAsProps, PropsWithChildren {
description: MDXRemoteSerializeResult<
Record<string, unknown>,
Record<string, unknown>
>;
}
type CanSetMode = {
setMode: React.Dispatch<React.SetStateAction<ProjectPageMode>>;
};
type GenericDisplayProps = DisplayProps & PropsWithChildren;
type MemberDisplayProps = DisplayProps & CanSetMode;
type EditorDisplayProps = DisplayProps & CanSetMode;
type PreviewDisplayProps = DisplayProps & CanSetMode;
const GenericDisplay: React.FC<GenericDisplayProps> = ({
project,
description,
children: toolbarItems,
}) => {
return (
<div className="flex-1">
<div className="relative h-44 w-full shadow">
<Image
src={
project?.bannerImageUrl ??
`https://picsum.photos/seed/${project?.id ?? "A"}/800/200.webp`
}
alt="project banner"
className="object-cover"
fill
/>
</div>
<Toolbar.Root
className="flex w-full flex-row items-center border-y-2 border-y-fg/10 bg-bg-600 px-5 py-3"
aria-label="Project options"
>
{toolbarItems}
</Toolbar.Root>
<article className="mx-5 my-3">
<h1 className="font-bold text-primary text-r-5xl">{project?.title}</h1>
<MDX {...description} />
</article>
</div>
);
};
export const VisitorDisplay: React.FC<DisplayProps> = (props) => {
const router = useRouter();
const handleGoBack = () => {
router.back();
};
const handleJoinRequest = () => {
toast.error("TODO: Implement 'request to join'");
};
return (
<GenericDisplay {...props}>
<span className="text-r-base">You are not a member of this project.</span>
<Toolbar.Separator className="mx-3 h-5 w-[2px] rounded-full bg-fg/30" />
<div className="flex-grow" />
<Toolbar.Button asChild>
<button
className="mr-5 text-fg/80 text-r-base hover:text-fg-600/80"
onClick={handleGoBack}
>
Go back
</button>
</Toolbar.Button>
<Toolbar.Button asChild>
<button
className="font-semibold text-fg text-r-base hover:text-fg-600"
onClick={handleJoinRequest}
>
Request to join
</button>
</Toolbar.Button>
</GenericDisplay>
);
};
export const PreviewDisplay: React.FC<PreviewDisplayProps> = ({
setMode,
...rest
}) => {
const handleExitPreview = () => {
setMode(ProjectPageMode.editor);
};
return (
<GenericDisplay {...rest}>
<span className="text-r-base">This is a preview.</span>
<div className="flex-grow" />
<Toolbar.Button asChild>
<button
className="font-semibold text-fg text-r-base hover:text-fg-600"
onClick={handleExitPreview}
>
Exit preview
</button>
</Toolbar.Button>
</GenericDisplay>
);
};
export const MemberDisplay: React.FC<MemberDisplayProps> = ({
setMode,
...rest
}) => {
const handleEditMode = () => {
setMode(ProjectPageMode.editor);
};
return (
<GenericDisplay {...rest}>
<span className="text-r-base">You are a member of this project.</span>
<Toolbar.Separator className="mx-3 h-5 w-[2px] rounded-full bg-fg/30" />
<span className="text-r-base">Last updated X minutes ago.</span>
<div className="flex-grow" />
<Toolbar.Button asChild>
<button
className="-my-2 mr-1 rounded-full p-2 hover:bg-bg-300/30"
onClick={handleEditMode}
>
<FiEdit3 className="font-bold text-r-2xl" />
</button>
</Toolbar.Button>
{/* TODO: Make dropdown menu popup */}
<Toolbar.Button asChild>
<button className="-my-2 -mr-2 rounded-full p-2 hover:bg-bg-300/30">
<FiSettings className="font-bold text-r-2xl" />
</button>
</Toolbar.Button>
</GenericDisplay>
);
};
export const EditorDisplay: React.FC<EditorDisplayProps> = ({
setMode,
...rest
}) => {
const handleCancel = () => {
setMode(ProjectPageMode.member);
};
const handleSave = () => {
toast.error("TODO: Need to save changes");
setMode(ProjectPageMode.member);
};
const handlePreview = () => {
setMode(ProjectPageMode.preview);
};
return (
<GenericDisplay {...rest}>
<span className="text-r-base">Editing</span>
<Toolbar.Separator className="mx-3 h-5 w-[2px] rounded-full bg-fg/30" />
<Toolbar.Button asChild>
<button
className="-my-2 mr-1 rounded-full p-2 hover:bg-bg-300/30"
onClick={handlePreview}
>
<FiEye className="font-bold text-r-2xl" />
</button>
</Toolbar.Button>
<div className="flex-grow" />
<Toolbar.Button asChild>
<button
className="mr-5 text-fg/80 text-r-base hover:text-fg-600/80"
onClick={handleCancel}
>
Cancel
</button>
</Toolbar.Button>
<Toolbar.Button asChild>
<button
className="font-semibold text-fg text-r-base hover:text-fg-600"
onClick={handleSave}
>
Save changes
</button>
</Toolbar.Button>
</GenericDisplay>
);
};

View File

View File

@@ -18,19 +18,16 @@ import superjson from "superjson";
import { appRouter } from "@server/api/root";
import { getServerAuthSession } from "@server/auth";
import { createProxySSGHelpers } from "@trpc/react-query/ssg";
import { MDX } from "@components/MDX";
import Image from "next/image";
import { FiEdit3 } from "react-icons/fi";
import { MDXRemoteSerializeResult } from "next-mdx-remote";
import React, { useState } from "react";
import * as Avatar from "@radix-ui/react-avatar";
enum PageMode {
visitor,
member,
editor,
preview,
}
import React, { useEffect, useMemo, useState } from "react";
import { ProfilePicture } from "@components/ProfilePicture";
import {
EditorDisplay,
MemberDisplay,
PreviewDisplay,
VisitorDisplay,
} from "@components/projects/Displays";
import { useSession } from "next-auth/react";
import { ProjectPageMode } from "@utils/types/projects";
type FetchedProjectAsProps = {
project:
@@ -43,14 +40,25 @@ type FetchedProjectAsProps = {
| undefined;
};
const componentsByMode = {
// dont show edit button
[ProjectPageMode.visitor]: VisitorDisplay,
// show edit button
[ProjectPageMode.member]: MemberDisplay,
// show cancel + save buttons + editable fields
[ProjectPageMode.editor]: EditorDisplay,
// show visitor and replace edit button with back/end preview button
[ProjectPageMode.preview]: PreviewDisplay,
};
const StateOrder: ProjectLifecycle[] = Object.values(ProjectLifecycle);
const SpecificProject: NextPage<
InferGetServerSidePropsType<typeof getServerSideProps>
> = ({ projectId, description }) => {
const [mode, setMode] = useState(PageMode.visitor);
const [mode, setMode] = useState(ProjectPageMode.visitor);
const ctx = api.useContext();
const session = useSession();
const { data, isRefetching } = api.projects.getProjectById.useQuery(
{
@@ -61,6 +69,16 @@ const SpecificProject: NextPage<
refetchOnWindowFocus: false,
}
);
useEffect(() => {
setMode(
session.status === "authenticated"
? ProjectPageMode.member
: ProjectPageMode.visitor
);
}, [session.status]);
// const ctx = api.useContext();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// const { mutate, isLoading: isUpdatingState } =
// api.projects.updateState.useMutation({
@@ -85,30 +103,16 @@ const SpecificProject: NextPage<
// }
// };
// TODO: Make edit mode do something
const componentsByMode = {
[PageMode.visitor]: (
<DisplayProject project={data} description={description} />
),
[PageMode.member]: (
<DisplayProject project={data} description={description} />
),
[PageMode.editor]: (
<DisplayProject project={data} description={description} />
),
[PageMode.preview]: (
<DisplayProject project={data} description={description} />
),
};
const Display = useMemo(() => componentsByMode[mode], [mode]);
return (
<MainLayout>
<div className="flex flex-row justify-between">
<DisplayProject project={data} description={description} />
<Display project={data} description={description} setMode={setMode} />
{/* Right side panel */}
<div className="sticky top-0 flex h-screen w-96 flex-row">
<div className="w-1.5 border-x-2 border-x-fg/10" />
<aside className="flex w-full flex-col gap-y-4 overflow-x-hidden px-11 py-8">
<aside className="flex w-full flex-col gap-y-4 overflow-x-hidden px-5 py-8">
<div>
<h2 className="mb-2 font-bold text-primary text-r-4xl">
Contributors
@@ -117,10 +121,15 @@ const SpecificProject: NextPage<
{/* TODO: Handle overflow */}
<ProfilePicture
name={data?.author.name}
username={data?.author.username}
image={data?.author.image}
/>
{data?.members.map((member) => (
<ProfilePicture name={member.name} image={member.image} />
<ProfilePicture
name={member.name}
username={member.username}
image={member.image}
/>
))}
</div>
</div>
@@ -184,29 +193,6 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
return { props: { projectId, description: mdxSource } };
};
interface ProfilePictureProps {
name: string | undefined | null;
image: string | undefined | null;
}
const ProfilePicture: React.FC<ProfilePictureProps> = ({ name, image }) => {
return (
<Avatar.Root className="flex min-w-max select-none items-center justify-center overflow-hidden rounded-full bg-quaternary p-2 align-middle shadow-solid-medium">
<Avatar.Image
className="h-10 w-10 rounded-[inherit] object-cover"
src={image ?? undefined}
alt={name ?? "User avatar"}
/>
<Avatar.Fallback
className="leading-1 flex h-10 w-10 items-center justify-center rounded-[inherit] bg-tertiary font-medium"
delayMs={600}
>
{name?.charAt(0)}
</Avatar.Fallback>
</Avatar.Root>
);
};
interface ProjectProgressProps {
progress: ProjectLifecycle | undefined | null;
}
@@ -230,42 +216,3 @@ const ProjectProgress: React.FC<ProjectProgressProps> = ({ progress }) => {
</div>
);
};
interface DisplayProjectProps extends FetchedProjectAsProps {
description: MDXRemoteSerializeResult<
Record<string, unknown>,
Record<string, unknown>
>;
}
const DisplayProject: React.FC<DisplayProjectProps> = ({
project,
description,
}) => {
return (
<div className="flex-1">
<div className="relative h-44 w-full shadow">
<Image
src={
project?.bannerImageUrl ??
`https://picsum.photos/seed/${project?.id ?? "A"}/800/200.webp`
}
alt="project banner"
className="object-cover"
fill
/>
</div>
<article className="mx-5 mt-8">
<div className="flex flex-row items-start justify-between">
<h1 className="font-bold text-primary text-r-5xl">
{project?.title}
</h1>
<button className="-mr-3 mt-1 rounded-full p-3 hover:bg-bg-300/30">
<FiEdit3 className="font-bold text-r-4xl" />
</button>
</div>
<MDX {...description} />
</article>
</div>
);
};

View File

@@ -88,7 +88,7 @@ const CreateNewProposal: NextPage = () => {
error={formState.errors.description?.message}
{...register("description")}
/>
{/* <ImageInput label="Image" /> */}
{/* <ImageInputp label="Image" /> */}
</div>
<div className="flex flex-row justify-end">
<Button

View File

@@ -0,0 +1,6 @@
export enum ProjectPageMode {
visitor,
member,
editor,
preview,
}