Skip to content

Commit

Permalink
Communities (#68)
Browse files Browse the repository at this point in the history
* rearrange navigation

* properly style

* comm form

* create community

* load community

* approve community

* list members

* post to community, load post

* progress

* looking good

* validation

* collate nocase
  • Loading branch information
blackmann authored Apr 25, 2024
1 parent 998cd38 commit d315170
Show file tree
Hide file tree
Showing 50 changed files with 1,305 additions and 285 deletions.
83 changes: 0 additions & 83 deletions client/.eslintrc.cjs

This file was deleted.

6 changes: 4 additions & 2 deletions client/app/components/anchor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Link } from "@remix-run/react";
import { RemixLinkProps } from "@remix-run/react/dist/components";
import clsx from "clsx";
import React from "react";

interface Props extends React.ComponentProps<"a"> {
interface Props extends RemixLinkProps {
variant?: "primary" | "neutral";
}

const Anchor = React.forwardRef<HTMLAnchorElement, Props>(
({ className, variant = "primary", ...props }, ref) => {
return (
<a
<Link
ref={ref}
className={clsx(
"inline-flex items-center gap-2 rounded-lg bg-blue-600 px-2 py-1 font-medium",
Expand Down
9 changes: 5 additions & 4 deletions client/app/components/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import BoringAvatar from "boring-avatars";
import BoringAvatar, { AvatarProps } from "boring-avatars";
import clsx from "clsx";

interface Props {
name: string;
size?: number;
className?: string;
variant?: AvatarProps["variant"];
}

const colors = ["#ffe12e", "#4d8c3a", "#0060ff", "#ff7d10", "#4e412b"];

const BA = BoringAvatar.default;

function Avatar({ className, name, size = 28 }: Props) {
function Avatar({ className, name, size = 28, variant = "beam" }: Props) {
return (
<div className={clsx("rounded-full", className)}>
<BA colors={colors} size={size} name={name} variant="beam" />
<div className={clsx("rounded-full overflow-hidden aspect-square self-start", className)}>
<BA colors={colors} size={size} name={name} square variant={variant} />
</div>
);
}
Expand Down
3 changes: 2 additions & 1 deletion client/app/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import clsx from "clsx";
import React from "react";

interface Props extends React.ComponentProps<"button"> {
variant?: "primary" | "neutral";
variant?: "primary" | "neutral" | "secondary";
}

const Button = React.forwardRef<HTMLButtonElement, Props>(
Expand All @@ -15,6 +15,7 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(
{
"bg-zinc-200 px-2 py-1 dark:bg-neutral-800": variant === "neutral",
"text-white": variant === "primary",
"!bg-amber-500 text-white": variant === "secondary",
"opacity-60": props.disabled,
},
className,
Expand Down
109 changes: 109 additions & 0 deletions client/app/components/community-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useFetcher, useLoaderData, useParams } from "@remix-run/react";
import dayjs from "dayjs";
import { useGlobalCtx } from "~/lib/global-ctx";
import { loader } from "~/routes/communities_.$slug";
import { Anchor } from "./anchor";
import { Avatar } from "./avatar";
import { Button } from "./button";

function CommunityInfo() {
const { user } = useGlobalCtx();
const { community, membership, members } = useLoaderData<typeof loader>();
const { slug } = useParams();
const fetcher = useFetcher();

async function join() {
fetcher.submit("", {
action: `/communities/${slug}/members`,
method: "POST",
});
}

return (
<>
<h1 className="font-bold text-lg leading-none">{community.name}</h1>
<div className="bg-rose-100 dark:bg-rose-900 dark:bg-opacity-20 rounded-lg inline-block text-rose-500 font-medium px-1 text-sm">
+{community.handle}
</div>

{/* <div className="rounded-lg aspect-[5/2] bg-zinc-100 dark:bg-neutral-800 mt-2" /> */}

<p className="">{community.description}</p>

{!membership && (
<div className="border dark:border-neutral-800 rounded-lg p-2 mt-2">
<header className="font-mono text-xs text-secondary">
Join to interact
</header>
<p className="text-sm">
To be able to start and participate in conversations in this
community, you need to join first.
</p>

<div className="mt-2">
{!user ? (
<Anchor to="/login">Login & join</Anchor>
) : (
<Button
variant="secondary"
disabled={fetcher.state === "submitting"}
onClick={join}
>
{fetcher.state === "submitting" ? (
<>Joining</>
) : (
<>Join community</>
)}
</Button>
)}
</div>
</div>
)}

{user && membership && (
<div className="flex gap-2">
<div className="text-xs text-secondary">
Member since {dayjs(membership.createdAt).format("DD MMM YYYY")}
</div>
{/** [ ] Add "leave community" */}
</div>
)}

{/* <div className="border rounded-lg p-2 mt-2">
<header className="font-mono text-xs text-secondary">
Upcoming event
</header>
<p className="text-sm">Lecture 7: Developing a pixel art editor</p>
<p className="text-secondary text-sm font-medium">Sat, 17 Apr.</p>
</div> */}

<div className="max-lg:hidden">
<div className="flex gap-2 items-center mt-2 font-medium text-secondary text-sm">
<div className="i-lucide-users-2 inline-block" />
{community.members} members
</div>

<ul>
{members.map((member) => (
<li key={member.userId}>
<div className="flex gap-2 py-1 px-2 rounded-lg hover:bg-zinc-100 items-center hover-bg-light">
<Avatar size={22} name={member.user.username} />

<div>
{member.user.username}{" "}
{member.role === "moderator" && (
<span className="bg-zinc-200 dark:bg-neutral-800 rounded-full px-2 text-sm text-secondary font-medium">
MOD
</span>
)}
</div>
</div>
</li>
))}
</ul>
</div>
</>
);
}

export { CommunityInfo };
60 changes: 60 additions & 0 deletions client/app/components/community-mod.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Community } from "@prisma/client";
import { useFetcher } from "@remix-run/react";
import { Jsonify } from "type-fest";
import { useGlobalCtx } from "~/lib/global-ctx";
import { Button } from "./button";

interface Props {
community: Community | Jsonify<Community>;
}

function CommunityMod({ community }: Props) {
const { user } = useGlobalCtx();
const fetcher = useFetcher();

const actions: { title: string; id: string }[] = [];

if (community.status === "pending-approval") {
actions.push({ title: "Approve", id: "approve" });
}

function handleAction(action: string) {
switch (action) {
case "approve": {
fetcher.submit(JSON.stringify({ action }), {
encType: "application/json",
method: "POST",
action: `/communities/${community.handle}/mod`,
});
}
}
}

if (actions.length === 0) {
return null;
}

if (user?.role !== "moderator") return null;

return (
<div className="mb-2 p-1 bg-red-50 border border-red-200 rounded-lg">
<header className="font-mono text-xs text-red-500">community mod</header>

<div className="flex">
{actions.map((action) => (
<Button
className="text-sm p-1"
variant="neutral"
key={action.id}
onClick={() => handleAction(action.id)}
disabled={fetcher.state === "submitting"}
>
{action.title}
</Button>
))}
</div>
</div>
);
}

export { CommunityMod };
2 changes: 1 addition & 1 deletion client/app/components/crowdsource-notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ function CrowdsourceNotice() {
return (
<div className="p-2 rounded-lg border bg-white dark:bg-neutral-800 dark:border-neutral-700">
<header className="font-bold flex gap-2 items-center mb-2">
<div className="i-lucide-badge-check text-green-500"></div>
<div className="i-lucide-badge-check text-green-500" />
<span>Crowdsourcing</span>
</header>
<p>
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function ErrorBoundary() {
</p>

<div className="mt-2">
<Anchor href="/discussions">
<Anchor to="/discussions">
Go Home <div className="i-lucide-arrow-right opacity-50" />
</Anchor>
</div>
Expand Down
5 changes: 4 additions & 1 deletion client/app/components/file-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Prisma } from "@prisma/client";
import { useFetcher } from "@remix-run/react";
import { Jsonify } from "type-fest";
import { useGlobalCtx } from "~/lib/global-ctx";
import { DropdownMenu } from "./dropdown-menu";

type RepositoryFile = Prisma.RepositoryGetPayload<{ include: { user: true } }>;

interface Props {
file: Prisma.RepositoryGetPayload<{ include: { user: true } }>;
file: RepositoryFile | Jsonify<RepositoryFile>;
}

function FileMenu({ file }: Props) {
Expand Down
18 changes: 9 additions & 9 deletions client/app/components/lesson-form.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useLoaderData, useParams, useSubmit } from "@remix-run/react";
import dayjs from "dayjs";
import React from "react";
import { LargeSelect } from "./large-select";
import { Input } from "./input";
import { FieldValues, useForm, useFormContext } from "react-hook-form";
import { useAsyncFetcher } from "~/lib/use-async-fetcher";
import { Button } from "./button";
import dayjs from "dayjs";
import { useLoaderData, useParams, useSubmit } from "@remix-run/react";
import { days } from "~/lib/days";
import { AddLessonLoader } from "~/routes/timetable_.$year.$programme.$level.$sem.$day.add";
import { isBefore } from "~/lib/time";
import { useAsyncFetcher } from "~/lib/use-async-fetcher";
import { AddLessonLoader } from "~/routes/timetable_.$year.$programme.$level.$sem.$day.add";
import { Button } from "./button";
import { Input } from "./input";
import { LargeSelect } from "./large-select";

interface Props {
courses: { id: number; code: string; name: string }[];
Expand Down Expand Up @@ -120,7 +120,7 @@ function LessonForm({
</div>

<div className="text-secondary flex gap-2 mt-2">
<div className="i-lucide-corner-left-up"></div> You're adding a lesson
<div className="i-lucide-corner-left-up" /> You're adding a lesson
for the above
</div>

Expand Down Expand Up @@ -220,7 +220,7 @@ function LessonForm({

<div className="mt-2">
<Button>
<div className="i-lucide-corner-down-left opacity-50"></div> Save
<div className="i-lucide-corner-down-left opacity-50" /> Save
lesson
</Button>
</div>
Expand Down
Loading

0 comments on commit d315170

Please sign in to comment.