Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ And that's it! Your `SkateHub Frontend` should now be up and running locally on

### 2025

- 2025-11-03 - Add UserProfile component to display user information [#126](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/126) _(v0.1.37)_
- 2025-11-03 - Standardize validation messages [#125](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/125) _(v0.1.36)_
- 2025-11-01 - Add category definition [#124](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/124) _(v0.1.35)_
- 2025-09-25 - Add ESLint packages to devDependencies for improved linting support [#121](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/121) _(v0.1.34)_
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skatehub-frontend",
"version": "0.1.36",
"version": "0.1.37",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
190 changes: 190 additions & 0 deletions src/features/user/profile/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React from "react";
import { FaGlobe, FaInstagram, FaMapMarkerAlt } from "react-icons/fa";
import { TbSkateboard } from "react-icons/tb";

import {
Avatar as ChakraAvatar,
Badge,
Box,
Divider,
Flex,
Grid,
Heading,
HStack,
Icon,
IconButton,
Image,
Link,
Spinner,
Text,
useColorModeValue,
VStack
} from "@chakra-ui/react";

import { useUser } from "@/hooks/useUser";
import { openInstagram, openWebsite } from "@/utils/socialMedia";

export function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useUser(userId);

// const bgColor = useColorModeValue('gray.100', '#1f2228');
const cardBg = useColorModeValue("gray.800", "rgba(31, 34, 40, 0.5)");
// const textColor = useColorModeValue("gray.800", "gray.200");
const mutedColor = useColorModeValue("gray.600", "gray.400");

const tricks = [
{
id: 1,
title: "Mega Ramp Air",
date: "2 days ago",
image:
"https://lh3.googleusercontent.com/aida-public/AB6AXuDENOIOjB4kTUp2fOYb-X-zPgslq-gZO6Uf81DIhoE3lP1EMnB53KPGctYvfpGTrELBSVKyoO_bY8CnxpJFZSOqe812L8ICN2JZvXopdcA8Ya3E0Vo-nRKwiXrTbcvqdcBFITDnKqehutmwPPhMxBKXl4CjoY-ARhLoSyfdJN1Nyl8aCGRn9IwQ2-4Ia9hDpwtxW8wfcapkmM9Gf_WpXBIdb2WQ4Rdlzytqs8GtpwSyd15VsfTqmbspgL3hbaas6AmUz5UtBHPzlyNU"
},
{
id: 2,
title: "Downtown Handrail",
date: "5 days ago",
image:
"https://lh3.googleusercontent.com/aida-public/AB6AXuB2WDXJP9yIB6jwDdLaohTRMQP-0fWewkdDvl9N0hCvGm3kRjRKAwgSgufxo04G-6g3VVmHUVLTBTFTpmAHV8hyuvxQObBau0otnaJzvDmuavoRq6ed3RvdxidG2By8HhgHRshLjXuAowJSxX9VzX3KF4z0QHwugDMsnT2cJr37QAdxcaoNU0H3-wluUxDMqe8yMDs1x6poK90egOEUD2AjacCUeBMJnaGa1Ve3SDOwOxzXPtvy6sefPjTaHPzEW9ymbsmOLijNjAzP"
},
{
id: 3,
title: "Vert Ramp Session",
date: "1 week ago",
image:
"https://lh3.googleusercontent.com/aida-public/AB6AXuAP6JOPzvX8G1Brhrr3uby06DF7evmMyrDo9boFsXCN5vb--uJyu0I8iI-RFaycYeQhMXL_3nQ_OzXL66b5jEgRFilJ51c3IqUtaPSMQORCG_OfBTzaeiP-dBOi9ZzP3tK2fclPnPD6JBrYT3drTsSZZJYfx3oWtJPgcTYEpWvsd5Ih3QA1QdGK3mSvRw_3qQ7OafT6B181vZRUFnEpjpdvBBRQZ0407I_kEb87MBCFoLR92fb6y3AsOYwD4TQK2VdnE6AEZEuEcT1Q"
}
];

if (isLoading) {
return <Spinner />;
}

if (error) {
return <Text>Error: {error.message}</Text>;
}

return (
<>
<Box mb={6}>
<Flex direction="row" alignItems="center" position="relative">
<Heading size="lg" bg="gray.900" py={0} pr={4}>
Perfil
</Heading>
<Divider my="0" borderColor="gray.700" position="absolute" left={0} right={0} zIndex={-1} />
</Flex>
</Box>
<Flex minH="100vh">
<Box flex={1}>
<Box>
<Box bg={cardBg} borderRadius="xl" shadow="sm" p={8}>
<Flex direction={{ base: "column", md: "row" }} align={{ base: "center", md: "start" }} gap={8}>
<Box position="relative" flexShrink={0}>
<Box bgGradient="linear(to-tr, green.100, purple.600)" borderRadius="full" p={0.5}>
<Link
display="block"
bg="gray.800"
borderRadius="full"
p={1}
transition="transform 0.2s"
_hover={{
transform: "rotate(-6deg)"
}}
>
<ChakraAvatar
size="2xl"
w="120px"
h="120px"
src={user?.avatar?.formats?.thumbnail.url}
name={user?.name}
border="none"
/>
</Link>
</Box>

<IconButton
icon={<TbSkateboard size={16} />}
aria-label="Online status"
position="absolute"
bottom={3}
right={2}
size="sm"
borderRadius="full"
color="gray.800"
bg="green.300"
border="2px solid"
borderColor="gray.800"
w={6}
h={6}
minW={6}
fontSize="lg"
_hover={{
bg: "green.300"
}}
/>
</Box>

<VStack align={{ base: "center", md: "start" }} w="full" spacing={4}>
<HStack>
<Heading size="lg">{user?.name}</Heading>
{user?.category && (
<Badge colorScheme="green" px={3} py={1} borderRadius="full">
{user?.category?.name}
</Badge>
)}
</HStack>

<HStack>
<Icon as={FaMapMarkerAlt} />
<Text>
{user?.address?.city}, {user?.address?.uf}, {user?.address?.country}
</Text>
</HStack>

<HStack spacing={4}>
{user?.instagram_url && (
<Link as="button" onClick={() => openInstagram(user.instagram_url)} _hover={{ opacity: 0.8 }}>
<Icon as={FaInstagram} w={6} h={6} />
</Link>
)}
{user?.website_url && (
<Link as="button" onClick={() => openWebsite(user.website_url)} _hover={{ opacity: 0.8 }}>
<Icon as={FaGlobe} w={6} h={6} />
</Link>
)}
</HStack>

<Text textAlign={{ base: "center", md: "left" }}>{user?.about}</Text>
</VStack>
</Flex>
</Box>

<Box mt={8}>
<Box mb={6}>
<Flex direction="row" alignItems="center" position="relative">
<Heading size="lg" bg="gray.900" py={0} pr={4}>
Últimas manobras
</Heading>
<Divider my="0" borderColor="gray.700" position="absolute" left={0} right={0} zIndex={-1} />
</Flex>
</Box>
<Grid templateColumns={{ base: "1fr", sm: "repeat(2, 1fr)", lg: "repeat(3, 1fr)" }} gap={6}>
{tricks.map(trick => (
<Box key={trick.id} bg={cardBg} borderRadius="xl" overflow="hidden" shadow="sm">
<Image src={trick.image} alt={trick.title} w="full" h="160px" objectFit="cover" />
<Box p={4}>
<Text fontWeight="semibold">{trick.title}</Text>
<Text fontSize="sm" color={mutedColor} mt={1}>
{trick.date}
</Text>
</Box>
</Box>
))}
</Grid>
</Box>
</Box>
</Box>
</Flex>
</>
);
}
22 changes: 22 additions & 0 deletions src/hooks/useUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";

import { getUser } from "@/services/getUser";
import type { UserBasics } from "@/types/usersBasics.type";

type UseUserOptions = Omit<UseQueryOptions<UserBasics, Error>, "queryKey" | "queryFn">;

export function useUser(userId: string | undefined, options?: UseUserOptions) {
return useQuery<UserBasics, Error>({
queryKey: ["user", userId],
queryFn: () => {
if (!userId) {
throw new Error("User ID is required");
}
return getUser(userId);
},
enabled: !!userId, // Only fetch when userId is defined
staleTime: 1000 * 60 * 5, // Consider data fresh for 5 minutes
refetchOnWindowFocus: false,
...options
});
}
13 changes: 13 additions & 0 deletions src/pages/user/[id]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useRouter } from "next/router";

import { UserProfile } from "@/features/user/profile";

export default function SkateHubProfilePage() {
const router = useRouter();

if (!router.isReady || typeof router.query.id !== "string") {
return null;
}

return <UserProfile userId={router.query.id} />;
}
13 changes: 13 additions & 0 deletions src/services/getUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axios from "axios";

import type { UserBasics } from "@/types/usersBasics.type";
import { API } from "@/utils/constant";

export async function getUser(id: string): Promise<UserBasics> {
try {
const res = await axios.get(`${API}/api/users/${id}?populate=avatar,address,category`);
return res.data;
} catch (error) {
throw new Error(`Failed to fetch user data: ${error}`);
}
}
19 changes: 19 additions & 0 deletions src/utils/socialMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function openInstagram(username: string | undefined | null): void {
if (!username) return;

const cleanUsername = username.replace("@", "").trim();
if (!cleanUsername) return;

window.open(`https://instagram.com/${cleanUsername}`, "_blank", "noopener,noreferrer");
}

export function openWebsite(url: string | undefined | null): void {
if (!url) return;

const cleanUrl = url.trim();
if (!cleanUrl) return;

const fullUrl = cleanUrl.startsWith("http://") || cleanUrl.startsWith("https://") ? cleanUrl : `https://${cleanUrl}`;

window.open(fullUrl, "_blank", "noopener,noreferrer");
}