Started different page modes
This commit is contained in:
1081
package-lock.json
generated
1081
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
30
src/components/ProfilePicture.tsx
Normal file
30
src/components/ProfilePicture.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
220
src/components/projects/Displays.tsx
Normal file
220
src/components/projects/Displays.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
0
src/components/projects/Panels.tsx
Normal file
0
src/components/projects/Panels.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
6
src/utils/types/projects.ts
Normal file
6
src/utils/types/projects.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum ProjectPageMode {
|
||||
visitor,
|
||||
member,
|
||||
editor,
|
||||
preview,
|
||||
}
|
||||
Reference in New Issue
Block a user