Skip to content

Commit

Permalink
bio
Browse files Browse the repository at this point in the history
  • Loading branch information
blackmann committed Apr 26, 2024
1 parent d315170 commit 0e656c0
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 18 deletions.
8 changes: 3 additions & 5 deletions client/app/components/post-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
stringifySelections,
} from "./tag-input";
import { TagSelect } from "./tag-select";
import { Textarea } from "./textarea";

interface Props {
level?: number;
Expand Down Expand Up @@ -167,11 +168,8 @@ function PostInput({ disabled, level = 0, parent, dataExtra }: Props) {
</header>
)}

<textarea
className={clsx(
"w-full rounded-lg bg-zinc-100 dark:bg-neutral-800 border-zinc-200 dark:border-neutral-700 p-2 h-30",
{ hidden: !hidePreview },
)}
<Textarea
className={clsx({ hidden: !hidePreview })}
placeholder={
isComment ? "What do you think?" : "What have you got to share?"
}
Expand Down
20 changes: 20 additions & 0 deletions client/app/components/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import clsx from "clsx";
import React from "react";

const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={clsx(
"w-full rounded-lg bg-zinc-100 dark:bg-neutral-800 border-zinc-200 dark:border-neutral-700 p-2 h-30",
className,
)}
ref={ref}
{...props}
/>
);
});

export { Textarea };
31 changes: 31 additions & 0 deletions client/app/lib/render-bio.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import rehypeSanitize from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified, type Plugin } from "unified";
import { visit } from "unist-util-visit";

const processor = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(exclude, { elements: ["table", "img"] })
.use(rehypeSanitize)
.use(rehypeStringify);

async function renderBio(bio: string) {
return (await processor.process(bio)).toString();
}

function exclude({ elements }: { elements: string[] }): ReturnType<Plugin> {
return (tree) => {
visit(tree, "element", (child, index, parent) => {
if (elements.includes(child.tagName)) {
parent.children.splice(index, 1);
}
});
};
}

export { renderBio };
4 changes: 2 additions & 2 deletions client/app/routes/communities_.new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useFetcher } from "@remix-run/react";
import { FieldValues, useForm } from "react-hook-form";
import { Button } from "~/components/button";
import { Input } from "~/components/input";
import { Textarea } from "~/components/textarea";
import { checkAuth } from "~/lib/check-auth";
import { getModerators } from "~/lib/get-moderators";
import { prisma } from "~/lib/prisma.server";
Expand Down Expand Up @@ -103,8 +104,7 @@ export default function CreateCommunity() {

<label>
Description
<textarea
className="w-full rounded-lg bg-zinc-100 dark:bg-neutral-800 border-zinc-200 dark:border-neutral-700 p-2 h-30"
<Textarea
maxLength={512}
placeholder="Talk about the community."
{...register("description", {
Expand Down
4 changes: 2 additions & 2 deletions client/app/routes/events_.add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FieldValues, useForm } from "react-hook-form";
import { Button } from "~/components/button";
import { FileInput } from "~/components/file-input";
import { Input } from "~/components/input";
import { Textarea } from "~/components/textarea";
import { timeFromString } from "~/lib/time";
import { uploadMedia } from "~/lib/upload-media";

Expand Down Expand Up @@ -167,8 +168,7 @@ export default function AddEvent() {

<label className="block mt-2">
Description*
<textarea
className="w-full rounded-lg bg-zinc-100 dark:bg-neutral-800 border-zinc-200 dark:border-neutral-700 p-2 h-30"
<Textarea
maxLength={512}
{...register("description", {
required: true,
Expand Down
147 changes: 138 additions & 9 deletions client/app/routes/p.$username.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node";
import { NavLink, useLoaderData, useOutlet } from "@remix-run/react";
import {
ActionFunctionArgs,
LoaderFunctionArgs,
MetaFunction,
json,
} from "@remix-run/node";
import {
NavLink,
useFetcher,
useLoaderData,
useOutlet,
useParams,
} from "@remix-run/react";
import clsx from "clsx";
import parse from "html-react-parser";
import React from "react";
import { FieldValues, useForm } from "react-hook-form";
import { Jsonify } from "type-fest";
import { Avatar } from "~/components/avatar";
import { Button } from "~/components/button";
import { Modal } from "~/components/modal";
import { PostItem, PostItemProps } from "~/components/post-item";
import { PostTime } from "~/components/post-time";
import { Textarea } from "~/components/textarea";
import { Username } from "~/components/username";
import { checkAuth } from "~/lib/check-auth";
import { useGlobalCtx } from "~/lib/global-ctx";
import { prisma } from "~/lib/prisma.server";
import { renderBio } from "~/lib/render-bio.server";
import { renderSummary } from "~/lib/render-summary.server";
import { values } from "~/lib/values.server";

Expand All @@ -19,10 +39,12 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
throw json({}, { status: 404 });
}

user.bio = await renderBio(user.bio || "");

const posts = await prisma.post.findMany({
where: { userId: user.id, parentId: null },
orderBy: { createdAt: "desc" },
include: { user: true, media: true },
include: { user: true, media: true, community: true },
});

for (const post of posts) {
Expand All @@ -32,6 +54,30 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
return { user, meta: values.meta(), posts };
};

export const action = async ({ params, request }: ActionFunctionArgs) => {
if (request.method !== "PATCH") {
throw json({}, { status: 405 });
}

const userId = await checkAuth(request);
const user = await prisma.user.findFirst({
where: { id: userId },
});

if (!user) {
throw json({}, { status: 404 });
}

if (user.username !== params.username) {
throw json({}, { status: 403 });
}

const data = await request.json();
await prisma.user.update({ where: { id: userId }, data: { bio: data.bio } });

return json({});
};

export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: `@${data?.user?.username} | ${data?.meta.shortName} ✽ compa` },
Expand Down Expand Up @@ -59,21 +105,27 @@ export default function Profile() {
<main className="container mx-auto min-h-[60vh] mt-2">
<div className="grid lg:grid-cols-3">
<div className="col-span-1 lg:col-span-2">
<div className="flex gap-2 pb-5">
<div className="flex gap-2 mb-2">
<Avatar name={user.username} />

<div>
<div className="flex-1">
<div className="font-mono font-medium">
<Username user={user} showVerfied />
</div>

<div className="text-secondary text-sm font-medium">
joined <PostTime time={user.createdAt} />
Joined <PostTime time={user.createdAt} />
</div>

<div className="text-secondary bg-zinc-200 dark:bg-neutral-800 inline px-1 py-0.5 rounded-lg font-medium text-sm">
{posts.length} discussions
<div className="flex gap-2">
<div className="text-secondary bg-zinc-200 dark:bg-neutral-800 inline px-1 py-0.5 rounded-lg font-medium text-sm">
{posts.length} discussions
</div>

<EditBio />
</div>

<div className="mt-2 bio">{parse(user.bio || "")}</div>
</div>
</div>

Expand Down Expand Up @@ -112,7 +164,7 @@ export default function Profile() {
);
}

function Posts({ posts }: { posts: PostItemProps["post"][] }) {
function Posts({ posts }: { posts: Jsonify<PostItemProps["post"]>[] }) {
if (posts.length === 0) {
return (
<div className="p-4 text-secondary">
Expand All @@ -132,3 +184,80 @@ function Posts({ posts }: { posts: PostItemProps["post"][] }) {
</React.Fragment>
));
}

function EditBio() {
const { username } = useParams();
const { user: authUser } = useGlobalCtx();
const { register, handleSubmit, watch } = useForm({
defaultValues: { bio: authUser?.bio },
});

const fetcher = useFetcher();

const [showForm, setShowForm] = React.useState(false);

const isSelf = authUser?.username === username;

async function updateBio(data: FieldValues) {
fetcher.submit(JSON.stringify(data), {
method: "PATCH",
encType: "application/json",
action: `/p/${username}`,
});

setShowForm(false);
}

React.useEffect(() => {
if (!fetcher.data) {
return;
}

setShowForm(false);
}, [fetcher.data]);

if (!isSelf) {
return null;
}

const $bio = watch("bio");

return (
<>
<button
type="button"
className="flex items-center gap-1 text-secondary bg-zinc-200 dark:bg-neutral-800 inline px-1 py-0.5 rounded-lg font-medium text-sm"
onClick={() => setShowForm(true)}
>
<div className="i-lucide-pencil" /> Edit bio
</button>

<Modal
className="w-full max-w-[24rem]"
open={showForm}
onClose={() => setShowForm(false)}
>
<form onSubmit={handleSubmit(updateBio)}>
<header className="px-2 py-1 font-bold">Edit bio</header>
<div className="px-2">
<Textarea maxLength={240} {...register("bio")} />
<div className="text-end text-xs text-secondary">
{$bio?.length || 0}/240
</div>
</div>

<footer className="border-t dark:border-neutral-800 flex justify-end p-2 gap-2 mt-2">
<Button
type="button"
variant="neutral"
onClick={() => setShowForm(false)}
>
Cancel
</Button>
<Button disabled={fetcher.state === "submitting"}>Save</Button>
</footer>
</form>
</Modal>
</>
);
}
4 changes: 4 additions & 0 deletions client/app/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,8 @@ input {

.ptr--text {
@apply !dark:text-neutral-500 font-medium;
}

.bio a {
@apply text-blue-500 underline font-medium;
}
2 changes: 2 additions & 0 deletions client/prisma/migrations/20240425224659_bio/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "bio" TEXT;
1 change: 1 addition & 0 deletions client/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ model User {
updatedAt DateTime @updatedAt
verified Boolean @default(false)
role String @default("common")
bio String?
PasswordResetRequest PasswordResetRequest[]
Post Post[]
Vote Vote[]
Expand Down

0 comments on commit 0e656c0

Please sign in to comment.