Set up side panel + banner image for project pages

This commit is contained in:
2023-06-05 13:19:44 -05:00
parent 681ca29a2a
commit 857800e8e4
5 changed files with 368 additions and 68 deletions

205
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"@next-auth/prisma-adapter": "^1.0.5",
"@next/font": "^13.1.6",
"@prisma/client": "^4.9.0",
"@radix-ui/react-avatar": "^1.0.2",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-hover-card": "^1.0.5",
@@ -701,19 +701,138 @@
}
},
"node_modules/@radix-ui/react-avatar": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.2.tgz",
"integrity": "sha512-XRL8z2l9V7hRLCPjHWg/34RBPZUGpmOjmsRSNvIh2DI28GyIWDChbcsDUVc63MzOItk6Q83Ob2KK8k2FUlXlGA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.3.tgz",
"integrity": "sha512-9ToF7YNex3Ste45LrAeTlKtONI9yVRt/zOS158iilIkW5K/Apeyb/TUQlcEFTEFvWr8Kzdi2ZYrm1/suiXPajQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-layout-effect": "1.0.0"
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
@@ -2702,7 +2821,7 @@
"version": "18.0.10",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/react": "*"
}
@@ -9108,15 +9227,67 @@
}
},
"@radix-ui/react-avatar": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.2.tgz",
"integrity": "sha512-XRL8z2l9V7hRLCPjHWg/34RBPZUGpmOjmsRSNvIh2DI28GyIWDChbcsDUVc63MzOItk6Q83Ob2KK8k2FUlXlGA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.3.tgz",
"integrity": "sha512-9ToF7YNex3Ste45LrAeTlKtONI9yVRt/zOS158iilIkW5K/Apeyb/TUQlcEFTEFvWr8Kzdi2ZYrm1/suiXPajQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-layout-effect": "1.0.0"
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"dependencies": {
"@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
}
},
"@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
}
},
"@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"requires": {
"@babel/runtime": "^7.13.10"
}
}
}
},
"@radix-ui/react-collection": {
@@ -10696,7 +10867,7 @@
"version": "18.0.10",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
"dev": true,
"devOptional": true,
"requires": {
"@types/react": "*"
}

View File

@@ -15,7 +15,7 @@
"@next-auth/prisma-adapter": "^1.0.5",
"@next/font": "^13.1.6",
"@prisma/client": "^4.9.0",
"@radix-ui/react-avatar": "^1.0.2",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-hover-card": "^1.0.5",

View File

@@ -145,7 +145,7 @@ const ProjectLifecycleIndicator: React.FC<{ state: ProjectLifecycle }> = ({
<Tooltip.Content
side="right"
sideOffset={4}
className="animate-slideRightAndFade rounded border-4 border-fg bg-quaternary-600 px-3 py-2 text-r-lg"
className="animate-slideRightAndFade rounded-md bg-bg-700 p-3 px-3 py-2 text-r-lg"
>
{lifecycleTooltips[state]}
{/* <Tooltip.Arrow className="fill-fg" /> */}

View File

@@ -1,5 +1,10 @@
import { MainLayout } from "@components/layouts";
import { ProjectLifecycle } from "@prisma/client";
import {
Project,
ProjectLifecycle,
ProjectPreview,
User,
} from "@prisma/client";
import { api } from "@utils/api";
import type {
GetServerSidePropsContext,
@@ -16,12 +21,35 @@ 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,
}
type FetchedProjectAsProps = {
project:
| (Project & {
author: User;
members: User[];
previews: ProjectPreview[];
})
| null
| undefined;
};
const StateOrder: ProjectLifecycle[] = Object.values(ProjectLifecycle);
const SpecificProject: NextPage<
InferGetServerSidePropsType<typeof getServerSideProps>
> = ({ projectId, description }) => {
const [mode, setMode] = useState(PageMode.visitor);
const ctx = api.useContext();
const { data, isRefetching } = api.projects.getProjectById.useQuery(
@@ -34,49 +62,83 @@ const SpecificProject: NextPage<
}
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { mutate, isLoading: isUpdatingState } =
api.projects.updateState.useMutation({
onSuccess: () => {
void ctx.projects.getProjectById.invalidate();
},
});
// const { mutate, isLoading: isUpdatingState } =
// api.projects.updateState.useMutation({
// onSuccess: () => {
// void ctx.projects.getProjectById.invalidate();
// },
// });
const changeState = (diff: -1 | 1) => {
if (!data) return;
// const changeState = (diff: -1 | 1) => {
// if (!data) return;
const newState =
StateOrder[
StateOrder.findIndex((state) => state === data.state) + diff
] ?? data.state;
// const newState =
// StateOrder[
// StateOrder.findIndex((state) => state === data.state) + diff
// ] ?? data.state;
if (newState !== data.state) {
mutate({
projectId: data.id,
state: newState,
});
}
// if (newState !== data.state) {
// mutate({
// projectId: data.id,
// state: newState,
// });
// }
// };
// 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} />
),
};
return (
<MainLayout>
<div className="max-w-4xl">
<BannerImage
bannerImageUrl={
data?.bannerImageUrl ??
`https://picsum.photos/seed/${data?.id ?? "A"}/800/200.webp`
}
/>
<article className="mx-11 mt-8 flex-grow">
<div className="flex flex-row items-start justify-between">
<h1 className="font-bold text-primary text-r-5xl">{data?.title}</h1>
<button className="rounded-full p-3 hover:bg-bg-300/30">
<FiEdit3 size={24} className="font-bold" />
</button>
</div>
<MDX {...description} />
</article>
<div className="flex flex-row justify-between">
<DisplayProject project={data} description={description} />
{/* 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">
<div>
<h2 className="mb-2 font-bold text-primary text-r-4xl">
Contributors
</h2>
<div className="flex flex-row gap-x-4">
{/* TODO: Handle overflow */}
<ProfilePicture
name={data?.author.name}
image={data?.author.image}
/>
{data?.members.map((member) => (
<ProfilePicture name={member.name} image={member.image} />
))}
</div>
</div>
<div>
<h2 className="mb-2 font-bold text-primary text-r-4xl">
Progress
</h2>
<ProjectProgress progress={data?.state} />
</div>
<div>
<h2 className="mb-2 font-bold text-primary text-r-4xl">
Messages
</h2>
<p>[Coming soon]</p>
</div>
</aside>
</div>
</div>
{/* Right side panel */}
</MainLayout>
);
};
@@ -122,22 +184,88 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
return { props: { projectId, description: mdxSource } };
};
const BannerImage: React.FC<{ bannerImageUrl: string }> = ({
bannerImageUrl,
}) => {
// TODO: On hover, show edit button + popup for a new image picker
interface ProfilePictureProps {
name: string | undefined | null;
image: string | undefined | null;
}
const ProfilePicture: React.FC<ProfilePictureProps> = ({ name, image }) => {
return (
<button className="relative h-44 w-full shadow hover:opacity-80">
<Image
src={bannerImageUrl}
alt="project banner"
className="object-cover"
fill
<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"}
/>
</button>
<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>
);
};
const ProjectDetails: React.FC<{ title: string }> = ({ title }) => {
return <></>;
interface ProjectProgressProps {
progress: ProjectLifecycle | undefined | null;
}
const ProjectProgress: React.FC<ProjectProgressProps> = ({ progress }) => {
const stateIndex = StateOrder.findIndex((state) => state === progress) ?? -1;
return (
<div className="relative flex w-full flex-row justify-between gap-x-4">
{/* TODO: Fix shadows */}
<div className="absolute top-1/2 -z-10 h-4 w-full -translate-y-1/2 bg-quaternary" />
{["bg-disabled", "bg-warning", "bg-error", "bg-success"].map((bg, i) => {
return (
<div key={bg} className="h-12 w-12 rounded-full bg-quaternary p-2">
{i <= stateIndex && (
<div className={`h-full w-full rounded-full ${bg}`} />
)}
</div>
);
})}
</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

@@ -77,6 +77,7 @@ export const projectsRouter = createTRPCRouter({
.query(({ ctx, input }) => {
return ctx.prisma.project.findUnique({
where: { id: input.projectId },
include: { author: true, members: true, previews: true },
});
}),
createProposal: protectedProcedure