Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user profile page #37

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/app/(public)/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";

import { useEffect, useState } from "react";

import { getArticlesByUsername } from "@/lib/query/article";
import { getUserByUsername } from "@/lib/query/user";

import { ArticleCard } from "@/components/articles/article-card";
import { UserProfile } from "@/components/user-info/user-profile";

interface UserProfilePageProps {
params: { username: string };
}

const UserProfilePage: React.FC<UserProfilePageProps> = (props) => {
const { username } = props.params;

const decodedUserName = decodeURIComponent(username);
const usernameWithoutAt = decodedUserName.slice(1);

const [user, setUser] = useState<any>(null);
const [articles, setArticles] = useState<any[]>([]);
const [offset, setOffset] = useState(0);
const [hasMore, setHasMore] = useState(true);

const loadUser = async () => {
const user = await getUserByUsername(usernameWithoutAt);
setUser(user);
};
const loadArticles = async () => {
const newArticles = await getArticlesByUsername(
usernameWithoutAt,
10,
offset,
);

if (!newArticles) {
return setHasMore(false);
}
setArticles((prevArticles) => [...prevArticles, ...newArticles]);
setOffset((prevOffset) => prevOffset + newArticles.length);
setHasMore(newArticles.length === 10);
};

useEffect(() => {
loadUser();
loadArticles();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (!user) {
return (
<main className="p-8">
<div className="text-center">User not found...</div>
</main>
);
}

return (
<main className="p-8">
<div className="max-w-4xl mx-auto">
<UserProfile user={user} />
<div className="article-container">
{articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
</div>
{hasMore && (
<button onClick={loadArticles} className="load-more-btn">
Load More
</button>
)}
</div>
</main>
);
};

export default UserProfilePage;
6 changes: 2 additions & 4 deletions src/app/api/sign-up/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

import { auth } from "@/lib/auth/lucia";
import { db } from "@/lib/db";
import { getUserByUsername } from "@/lib/query/user";

import { LuciaError } from "lucia";

Expand All @@ -28,9 +28,7 @@ export const POST = async (request: NextRequest) => {
return NextResponse.json({ error: "Invalid password" }, { status: 400 });
}

const existingUser = await db.query.users.findFirst({
where: (field, op) => op.eq(field.username, username.toLowerCase()),
});
const existingUser = await getUserByUsername(username);

if (existingUser) {
return NextResponse.json(
Expand Down
44 changes: 44 additions & 0 deletions src/components/articles/article-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable @next/next/no-img-element */
interface ArticleCardProps {
article: {
id: string;
createdAt: string;
updatedAt: string;
userId: string;
description: string | null;
title: string;
content: string;
cover: string | null;
};
}

export const ArticleCard = ({ article }: ArticleCardProps) => {
const formattedDate = article.createdAt
? article.createdAt.slice(0, 10)
: "Unknown Date";
return (
<div className="rounded-xl shadow-md overflow-hidden my-4 w-full">
<div className="md:flex">
<div className="flex-1 p-8">
<div className="uppercase tracking-wide text-sm text-indigo-500 font-semibold">
{formattedDate}
</div>
<a
href="#"
className="block mt-1 text-lg leading-tight font-medium text-white hover:underline"
>
{article.title}
</a>
<p className="mt-2 text-gray-500">{article.description}</p>
</div>
<div className="md:shrink-0">
<img
className="h-48 w-full object-cover md:w-48"
src={article.cover || "https://via.placeholder.com/150"}
alt={article.title}
/>
</div>
</div>
</div>
);
};
31 changes: 31 additions & 0 deletions src/components/user-info/user-profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable @next/next/no-img-element */
interface UserProfileProps {
user: {
id: string;
name: string | null;
email: string | null;
username: string;
avatar: string | null;
about: string | null;
createdAt: string;
updatedAt: string;
};
}

export const UserProfile = ({ user }: UserProfileProps) => {
return (
<div className="flex flex-col items-center p-4 bg-black shadow rounded-lg">
<img
className="size-24 rounded-full object-cover"
src={user.avatar || "https://via.placeholder.com/150"}
alt={user.name || "User"}
/>
<h2 className="mt-4 font-bold text-xl">{user.name}</h2>
<span className="text-gray-600">{10} Followers</span>
<p className="text-center text-gray-500 mt-2">{user.about}</p>
<button className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Follow
</button>
</div>
);
};
22 changes: 22 additions & 0 deletions src/lib/query/article.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { db } from "../db";
import { getUserByUsername } from "./user";

export type CompleteArticle = Awaited<
ReturnType<typeof getAllArticles>
Expand All @@ -16,3 +17,24 @@ export const getArticleById = async (id: string) => {
with: { author: true },
});
};

export const getArticlesByUsername = async (
username: string,
limit = 10,
offset = 0,
) => {
const user = await getUserByUsername(username);

if (!user) {
return null; // or throw an error here later ok, ave?
}

const articles = await db.query.articles.findMany({
where: (articles, { eq }) => eq(articles.userId, user.id),
limit,
offset,
orderBy: (articles, { desc }) => [desc(articles.createdAt)],
});

return articles;
};
14 changes: 14 additions & 0 deletions src/lib/query/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { db } from "../db";

/**
* Retrieves a user by their username.
*
* @param {string} username - the username of the user to retrieve
* @return {Promise<user>} the user object
*/
export const getUserByUsername = async (username: string) => {
const user = await db.query.users.findFirst({
where: (field, op) => op.eq(field.username, username.toLowerCase()),
});
return user;
};
Loading