diff --git a/client/app/app/explore/page.jsx b/client/app/app/explore/page.jsx index f03ab2a..93c2abb 100644 --- a/client/app/app/explore/page.jsx +++ b/client/app/app/explore/page.jsx @@ -1,10 +1,16 @@ 'use client'; import PostList from '@/components/app/Post/List'; +import UserList from '@/components/app/User/List'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { getPosts } from '@/lib/api/posts'; +import { getUsers } from '@/lib/api/users'; export default function Page() { + const fetchUsers = async (offset) => { + return await getUsers(11, offset); + }; + const fetchPosts = async (offset) => { return await getPosts(11, offset); }; @@ -21,7 +27,9 @@ export default function Page() { 2 - 3 + + + ); diff --git a/client/components/app/Comment/index.jsx b/client/components/app/Comment/index.jsx index b60d5fd..22c883b 100644 --- a/client/components/app/Comment/index.jsx +++ b/client/components/app/Comment/index.jsx @@ -8,7 +8,7 @@ import { } from '@/components/ui/tooltip'; import Dropdown from '@/components/app/Comment/Dropdown'; import LikeButton from '@/components/app/Comment/LikeButton'; -import UserCard from '@/components/app/UserCard'; +import UserInfo from '@/components/app/User/Info'; export default function Comment({ comment: initialComment, onDelete }) { const [comment, setComment] = useState(initialComment); @@ -16,7 +16,7 @@ export default function Comment({ comment: initialComment, onDelete }) { return (
- + {user != 'loading' ? ( - + ) : (
diff --git a/client/components/app/Post/List.jsx b/client/components/app/Post/List.jsx index 2c8d0de..82b67cb 100644 --- a/client/components/app/Post/List.jsx +++ b/client/components/app/Post/List.jsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { LoaderCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; import Post from '@/components/app/Post'; @@ -7,6 +8,7 @@ export default function PostList({ fetchPosts }) { const [posts, setPosts] = useState([]); const [offset, setOffset] = useState(10); const [hasMorePost, setHasMorePost] = useState(true); + const [loading, setLoading] = useState(false); const { toast } = useToast(); const handleDelete = (postId) => { @@ -39,39 +41,41 @@ export default function PostList({ fetchPosts }) { setOffset((prevOffset) => prevOffset + 10); }; - useEffect( - () => { - const fetchInitialPosts = async () => { - const response = await fetchPosts(0); - if (!response) { - toast({ - title: 'hay aksi, bir şeyler ters gitti!', - description: - 'sunucudan yanıt alınamadı. lütfen daha sonra tekrar deneyin.', - duration: 3000 - }); - return; - } + useEffect(() => { + const fetchInitialPosts = async () => { + setLoading(true); + const response = await fetchPosts(0); + setLoading(false); - const initialPosts = response.data.posts || []; + if (!response) { + toast({ + title: 'hay aksi, bir şeyler ters gitti!', + description: + 'sunucudan yanıt alınamadı. Lütfen daha sonra tekrar deneyin.', + duration: 3000 + }); + return; + } - if (initialPosts.length > 10) { - setPosts(initialPosts.slice(0, 10)); - } else { - setPosts(initialPosts); - setHasMorePost(false); - } - }; + const initialPosts = response.data.posts || []; - fetchInitialPosts(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + if (initialPosts.length > 10) { + setPosts(initialPosts.slice(0, 10)); + } else { + setPosts(initialPosts); + setHasMorePost(false); + } + }; + + fetchInitialPosts(); + }, [fetchPosts, toast]); return (
- {posts.length > 0 ? ( + {loading && ( + + )} + {!loading && posts.length > 0 ? ( <> {posts.map((post) => ( @@ -83,9 +87,11 @@ export default function PostList({ fetchPosts }) { )} ) : ( -
- buralar şimdilik sessiz. -
+ !loading && ( +
+ buralar şimdilik sessiz. +
+ ) )}
); diff --git a/client/components/app/Post/index.jsx b/client/components/app/Post/index.jsx index a42b2e0..493ac83 100644 --- a/client/components/app/Post/index.jsx +++ b/client/components/app/Post/index.jsx @@ -10,7 +10,7 @@ import { import Dropdown from '@/components/app/Post/Dropdown'; import LikeButton from '@/components/app/Post/LikeButton'; import CommentButton from '@/components/app/Post/CommentButton'; -import UserCard from '@/components/app/UserCard'; +import UserInfo from '@/components/app/User/Info'; export default function Post({ post: initialPost, onDelete, onNewComment }) { const [post, setPost] = useState(initialPost); @@ -19,7 +19,7 @@ export default function Post({ post: initialPost, onDelete, onNewComment }) { return (
- +
diff --git a/client/components/app/User/Card.jsx b/client/components/app/User/Card.jsx new file mode 100644 index 0000000..42e3cf9 --- /dev/null +++ b/client/components/app/User/Card.jsx @@ -0,0 +1,53 @@ +import { CalendarFold, Trash, SquareArrowOutUpRight } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import Hover from '@/components/app/User/Hover'; +import Link from 'next/link'; + +export default function Card({ index, user }) { + return ( +
+
+
+
+
+
+ +
@{user.username}
+
+
+ +
+
{user.about}
+
+
+ + {new Date(user.created_at.T * 1000).toLocaleDateString('tr-TR', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+ {' · '} +
+ +
{user.points} çöp puanı
+
+
+
+
+ ); +} diff --git a/client/components/app/UserCard/Hover.jsx b/client/components/app/User/Hover.jsx similarity index 99% rename from client/components/app/UserCard/Hover.jsx rename to client/components/app/User/Hover.jsx index 87f26fa..58bf91b 100644 --- a/client/components/app/UserCard/Hover.jsx +++ b/client/components/app/User/Hover.jsx @@ -36,4 +36,4 @@ export default function Hover({ user }) { ); -} \ No newline at end of file +} diff --git a/client/components/app/UserCard/index.jsx b/client/components/app/User/Info.jsx similarity index 83% rename from client/components/app/UserCard/index.jsx rename to client/components/app/User/Info.jsx index 94e28e7..309bd69 100644 --- a/client/components/app/UserCard/index.jsx +++ b/client/components/app/User/Info.jsx @@ -1,6 +1,6 @@ -import Hover from '@/components/app/UserCard/Hover'; +import Hover from '@/components/app/User/Hover'; -export default function UserCard({ user }) { +export default function UserInfo({ user }) { return (
diff --git a/client/components/app/User/List.jsx b/client/components/app/User/List.jsx new file mode 100644 index 0000000..331adc5 --- /dev/null +++ b/client/components/app/User/List.jsx @@ -0,0 +1,87 @@ +import { useState, useEffect } from 'react'; +import { LoaderCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import Card from '@/components/app/User/Card'; + +export default function UserList({ fetchUsers }) { + const [users, setUsers] = useState([]); + const [offset, setOffset] = useState(10); + const [hasMoreUser, setHasMoreUser] = useState(true); + const { toast } = useToast(); + + const loadMoreUsers = async () => { + if (!hasMoreUser) return; + + const response = await fetchUsers(offset); + if (!response) { + toast({ + title: 'hay aksi, bir şeyler ters gitti!', + description: + 'sunucudan yanıt alınamadı. lütfen daha sonra tekrar deneyin.', + duration: 3000 + }); + return; + } + + const newUsers = response.data.users || []; + + if (newUsers.length > 10) { + setUsers((prevUsers) => [...prevUsers, ...newUsers.slice(0, 10)]); + } else { + setUsers((prevUsers) => [...prevUsers, ...newUsers]); + setHasMoreUser(false); + } + + setOffset((prevOffset) => prevOffset + 10); + }; + + useEffect( + () => { + const fetchInitialUsers = async () => { + const response = await fetchUsers(0); + if (!response) { + toast({ + title: 'hay aksi, bir şeyler ters gitti!', + description: + 'sunucudan yanıt alınamadı. lütfen daha sonra tekrar deneyin.', + duration: 3000 + }); + return; + } + + const initialUsers = response.data.users || []; + + if (initialUsers.length > 10) { + setUsers(initialUsers.slice(0, 10)); + } else { + setUsers(initialUsers); + setHasMoreUser(false); + } + }; + + fetchInitialUsers(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( +
+ {users.length > 0 ? ( + <> + {users.map((user, index) => ( + + ))} + {hasMoreUser && ( + + )} + + ) : ( + + )} +
+ ); +} diff --git a/client/lib/api/users/index.js b/client/lib/api/users/index.js index e9b6626..24083ee 100644 --- a/client/lib/api/users/index.js +++ b/client/lib/api/users/index.js @@ -9,6 +9,15 @@ export async function getUser({ slug }) { } } +export async function getUsers(limit, offset) { + try { + const response = await api.get('/users', { params: { limit, offset } }); + return response; + } catch (error) { + return error.response; + } +} + export async function getUserPosts(slug, limit, offset) { try { const response = await api.get(`/users/${slug}/posts`, { diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 05b38f7..f2041a1 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -4,83 +4,83 @@ module.exports = { './pages/**/*.{js,jsx}', './components/**/*.{js,jsx}', './app/**/*.{js,jsx}', - './src/**/*.{js,jsx}', + './src/**/*.{js,jsx}' ], - prefix: "", + prefix: '', theme: { container: { center: true, - padding: "2rem", + padding: '2rem', screens: { - "2xl": "1400px", - }, + '2xl': '1400px' + } }, extend: { colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' }, secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' }, destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' }, muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' }, accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' }, popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' }, card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + } }, borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' }, keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' } }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' } }, sparkle: { '0%': { transform: 'scale(1.2) rotate(0deg)' }, '25%': { transform: 'scale(0.7) rotate(10deg)' }, '50%': { transform: 'scale(1.2) rotate(0deg)' }, '75%': { transform: 'scale(0.7) rotate(-10deg)' }, - '100%': { transform: 'scale(1.2) rotate(0deg)' }, - }, + '100%': { transform: 'scale(1.2) rotate(0deg)' } + } }, animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', sparkle1: 'sparkle 2s ease-in-out infinite', sparkle2: 'sparkle 2.5s ease-in-out infinite', - sparkle3: 'sparkle 3s ease-in-out infinite', - }, - }, + sparkle3: 'sparkle 3s ease-in-out infinite' + } + } }, - plugins: [require("tailwindcss-animate")], -} \ No newline at end of file + plugins: [require('tailwindcss-animate')] +}; diff --git a/server/handlers/users/get_users.go b/server/handlers/users/get_users.go new file mode 100644 index 0000000..8b9f05c --- /dev/null +++ b/server/handlers/users/get_users.go @@ -0,0 +1,31 @@ +package users + +import ( + "github.com/gofiber/fiber/v2" + "github.com/lareii/copl.uk/server/models" +) + +func GetUsers(c *fiber.Ctx) error { + limit := c.QueryInt("limit", 10) + offset := c.QueryInt("offset", 0) + + users, err := models.GetUsers(int64(limit), int64(offset)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "message": "Error fetching users.", + }) + } + + var responseUsers []models.User + for _, user := range users { + user.Email = "" + user.Password = "" + + responseUsers = append(responseUsers, user) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "Users found.", + "users": responseUsers, + }) +} diff --git a/server/models/user.go b/server/models/user.go index d3d6b4a..1c56ed0 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -12,6 +12,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) type User struct { @@ -51,6 +52,25 @@ func GetUserByUsername(username string) (user User, err error) { return user, err } +func GetUsers(limit, offset int64) ([]User, error) { + var users []User + cursor, err := database.Users.Find(context.Background(), bson.M{}, &options.FindOptions{ + Limit: &limit, + Skip: &offset, + Sort: bson.M{"points": -1}, + }) + if err != nil { + return users, fmt.Errorf("error fetching users: %v", err) + } + + err = cursor.All(context.Background(), &users) + if err != nil { + return users, fmt.Errorf("error decoding users: %v", err) + } + + return users, nil +} + func ValidateUser(c *fiber.Ctx) AuthStatus { cookie := c.Cookies("jwt") token, err := jwt.ParseWithClaims(cookie, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { diff --git a/server/router/router.go b/server/router/router.go index f9524f5..c17d89e 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -20,6 +20,7 @@ func SetupRouter(app *fiber.App) { authGroup.Get("/me", middlewares.AuthMiddleware(), auth.User) userGroup := app.Group("/users") + userGroup.Get("/", middlewares.AuthMiddleware(), users.GetUsers) userGroup.Get("/:slug", middlewares.AuthMiddleware(), users.GetUser) userGroup.Get("/:slug/posts", middlewares.AuthMiddleware(), users.GetUserPosts)