Skip to content

Commit

Permalink
Feature/NewUser (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
ttizze authored Aug 12, 2024
2 parents 96e624d + 4b863f8 commit ee1a1b7
Show file tree
Hide file tree
Showing 58 changed files with 1,183 additions and 453 deletions.
11 changes: 1 addition & 10 deletions web/app/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Link } from "@remix-run/react";
import { Form } from "@remix-run/react";
import { LogIn, Search } from "lucide-react";
import { useTheme } from "next-themes";
import { NewPageButton } from "~/components/NewPageButton";
import { ModeToggle } from "~/components/dark-mode-toggle";
import { Button } from "~/components/ui/button";
import type { SafeUser } from "~/types";

Expand All @@ -12,17 +10,11 @@ interface HeaderProps {
}

export function Header({ safeUser }: HeaderProps) {
const { resolvedTheme } = useTheme();

return (
<header className="shadow-sm mb-10 z-10 ">
<div className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 flex justify-between items-center">
<Link to="/">
<img
src={`/title-logo-${resolvedTheme || "light"}.png `}
alt=""
className="w-40"
/>
<img src="/title-logo-dark.png" alt="" className="w-40" />
</Link>
<div className="flex items-center">
<Button variant="ghost">
Expand All @@ -33,7 +25,6 @@ export function Header({ safeUser }: HeaderProps) {
<Search className="w-6 h-6" />
</Link>
</Button>
<ModeToggle />
{safeUser ? (
<>
<NewPageButton userId={safeUser.id} />
Expand Down
6 changes: 3 additions & 3 deletions web/app/components/NewPageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { PlusCircle } from "lucide-react";
import { Button } from "~/components/ui/button";

interface NewPageButtonProps {
userId: number;
userName: string;
}

export function NewPageButton({ userId }: NewPageButtonProps) {
export function NewPageButton({ userName }: NewPageButtonProps) {
const navigate = useNavigate();

const handleNewPage = () => {
const newSlug = crypto.randomUUID();
navigate(`/${userId}/page/${newSlug}/edit`);
navigate(`/${userName}/page/${newSlug}/edit`);
};

return (
Expand Down
8 changes: 7 additions & 1 deletion web/app/features/translate/functions/mutations.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ export async function getOrCreateAIUser(name: string): Promise<number> {
const user = await prisma.user.upsert({
where: { email: `${name}@ai.com` },
update: {},
create: { name, email: `${name}@ai.com`, isAI: true, image: "" },
create: {
email: `${name}@ai.com`,
isAI: true,
image: "",
userName: name,
displayName: name,
},
});

return user.id;
Expand Down
2 changes: 1 addition & 1 deletion web/app/features/translate/translate-user-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Queue } from "~/utils/queue.server";
import { translate } from "./lib/translate.server";
import type { TranslateJobParams } from "./types";

const QUEUE_VERSION = 2;
const QUEUE_VERSION = 4;

export const getTranslateUserQueue = (userId: number) => {
return Queue<TranslateJobParams>(
Expand Down
34 changes: 33 additions & 1 deletion web/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import type { LinksFunction } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/node";
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { useLocation } from "@remix-run/react";
import { typedjson } from "remix-typedjson";
import { useTypedLoaderData } from "remix-typedjson";
import { ThemeProvider } from "~/components/theme-provider";
import { Footer } from "~/routes/resources+/footer";
import { Header } from "~/routes/resources+/header";
import tailwind from "~/tailwind.css?url";
import { authenticator } from "~/utils/auth.server";

export async function loader({ request }: LoaderFunctionArgs) {
const safeUser = await authenticator.isAuthenticated(request);
return typedjson({ safeUser });
}
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: tailwind },
];
Expand All @@ -30,15 +42,35 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}

function CommonLayout({ children }: { children: React.ReactNode }) {
const { safeUser } = useTypedLoaderData<typeof loader>();
return (
<>
<Header safeUser={safeUser} />
<div className="container mx-auto">{children}</div>
<Footer safeUser={safeUser} />
</>
);
}

export default function App() {
const location = useLocation();
const isEditPage = /^\/\w+\/page\/[\w-]+\/edit$/.test(location.pathname);

return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Outlet />
{isEditPage ? (
<Outlet />
) : (
<CommonLayout>
<Outlet />
</CommonLayout>
)}
</ThemeProvider>
);
}
19 changes: 0 additions & 19 deletions web/app/routes/$userId+/page+/$slug+/_layout.tsx

This file was deleted.

21 changes: 21 additions & 0 deletions web/app/routes/$userName+/edit/functions/queries.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Page } from "@prisma/client";
import type { SafeUser } from "~/types";
import { prisma } from "~/utils/prisma";
import type { User } from "@prisma/client";

export async function getUserByUserName(userName: string) {
return prisma.user.findUnique({
where: {
userName,
},
});
}

export async function updateUser(userId: number, data: Partial<User>) {
return prisma.user.update({
where: {
id: userId,
},
data,
});
}
72 changes: 72 additions & 0 deletions web/app/routes/$userName+/edit/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { z } from "zod";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { authenticator } from "~/utils/auth.server";
import { getUserByUserName, updateUser } from "./functions/queries.server";
import { typedjson, useTypedLoaderData } from "remix-typedjson";

const schema = z.object({
displayName: z.string().min(1, "Display name is required").max(50, "Display name must be 50 characters or less"),
});

export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const currentUser = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});

const user = await getUserByUserName(params.userName || "");
if (!user) throw new Response("Not Found", { status: 404 });

if (user.userName !== params.userName) {
throw new Response("Unauthorized", { status: 403 });
}

return typedjson({ user });
};

export const action = async ({ request, params }: ActionFunctionArgs) => {
const currentUser = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});

const formData = await request.formData();
const displayName = formData.get("displayName") as string;

const result = schema.safeParse({ displayName });

if (!result.success) {
return typedjson({ errors: result.error.flatten().fieldErrors });
}

await updateUser(currentUser.id, { displayName });

return redirect(`/${params.userName}`);
};

export default function EditProfile() {
const { user } = useTypedLoaderData<typeof loader>();

return (
<div className="container mx-auto mt-10">
<h1 className="text-3xl font-bold mb-6">Edit Profile</h1>
<Form method="post" className="space-y-4">
<div>
<Label htmlFor="displayName">Display Name</Label>
<Input
type="text"
id="displayName"
name="displayName"
defaultValue={user.displayName}
required
/>
</div>
<Button type="submit">Save Changes</Button>
</Form>
</div>
);
}
39 changes: 39 additions & 0 deletions web/app/routes/$userName+/functions/queries.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Page } from "@prisma/client";
import type { SafeUser } from "~/types";
import { prisma } from "~/utils/prisma";
import type { UserWithPages } from "../types";
export async function getUserWithPages(
userName: string,
): Promise<UserWithPages | null> {
const user = await prisma.user.findUnique({
where: { userName },
include: {
pages: {
orderBy: { createdAt: "desc" },
take: 10,
},
},
});

if (!user) return null;

const safeUser: SafeUser = {
id: user.id,
userName: user.userName,
displayName: user.displayName,
plan: user.plan,
totalPoints: user.totalPoints,
isAI: user.isAI,
provider: user.provider,
image: user.image,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};

const pages: Page[] = user.pages;

return {
...safeUser,
pages,
};
}
76 changes: 76 additions & 0 deletions web/app/routes/$userName+/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { authenticator } from "~/utils/auth.server";
import { getUserWithPages } from "./functions/queries.server";
import type { UserWithPages } from "./types";
import { Link } from "@remix-run/react";
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const { userName } = params;
if (!userName) throw new Error("Username is required");

const userWithPages = await getUserWithPages(userName);
if (!userWithPages) throw new Response("Not Found", { status: 404 });

const currentUser = await authenticator.isAuthenticated(request);

const isOwnProfile = currentUser?.id === userWithPages.id;

return { userWithPages: userWithPages, isOwnProfile };
};

export default function UserProfile() {
const { userWithPages, isOwnProfile } = useLoaderData<{
userWithPages: UserWithPages;
isOwnProfile: boolean;
}>();

return (
<div className="container mx-auto mt-10">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">
{userWithPages.displayName}
</h1>
</div>

<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{userWithPages.pages.map((page) => (
<Link
to={`/${userWithPages.userName}/page/${page.slug}`}
key={page.id}
className="h-full"
>
<Card className="flex flex-col h-full">
<CardHeader>
<CardTitle className="line-clamp-2">{page.title}</CardTitle>
<CardDescription>
{new Date(page.createdAt).toLocaleDateString()}
</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-sm text-gray-600 line-clamp-4">
{page.content}
</p>
</CardContent>
</Card>
</Link>
))}
</div>

{userWithPages.pages.length === 0 && (
<p className="text-center text-gray-500 mt-10">
{isOwnProfile
? "You haven't created any pages yet."
: "No pages yet."}
</p>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function TranslationItem({
<div className="mb-2">{sanitizeAndParseText(translation.text)}</div>
{showAuthor && (
<p className="text-sm text-gray-500 text-right">
Translated by: {translation.userName}
Translated by: {translation.displayName}
</p>
)}
<VoteButtons translationWithVote={translation} userId={userId} />
Expand Down
Loading

0 comments on commit ee1a1b7

Please sign in to comment.