= async values => {
+ if (recaptchaRef.current) {
+ try {
+ setIsExecutingRecaptcha(true);
+ // for v2 invisible, use executeAsync(). For v3, use execute()
+ const recaptchaValue =
+ (await recaptchaRef.current.executeAsync?.()) || (await recaptchaRef.current.execute?.());
+ const newValues = { ...values, recaptcha: recaptchaValue || undefined };
+
+ await signIn(newValues)
+ .then(_ => {
+ recaptchaRef.current?.reset();
+ })
+ .catch(error => {
+ recaptchaRef.current?.reset();
+
+ switch (error.response.data.error?.message) {
+ case "Your account email is not confirmed":
+ addToast({
+ title: "Erro de autenticação.",
+ message: "Confirme seu e-mail para acessar a plataforma.",
+ type: "warning"
+ });
+ break;
+ case "Your account has been blocked by an administrator":
+ addToast({
+ title: "Erro de autenticação.",
+ message:
+ "Sua conta está temporariamente bloqueada. Se você acabou de se cadastrar, por favor, aguarde enquanto suas informações estão sendo revisadas por nossa equipe.",
+ type: "error"
+ });
+ break;
+ default:
+ addToast({
+ title: "Erro de autenticação.",
+ message: "Verifique seus dados de login e tente novamente.",
+ type: "error"
+ });
+ break;
+ }
+ });
+ } catch (error) {
+ console.error("reCAPTCHA execution failed:", error);
+ addToast({
+ title: "Erro de verificação.",
+ message: "Falha na verificação de segurança. Tente novamente.",
+ type: "error"
+ });
+ } finally {
+ setIsExecutingRecaptcha(false);
+ }
+ } else {
+ addToast({
+ title: "Erro de verificação.",
+ message: "Sistema de verificação não está disponível.",
+ type: "error"
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Junte-se à comunidade! Faça parte da evolução do esporte.
+
+
+ Faça login ou cadastre-se para começar a explorar todas as funcionalidades.
+
+
+
+
+
+
+
+
+
+
+
+
+ : }
+ variant="unstyled"
+ aria-label={showPassword ? "Ocultar senha" : "Mostrar senha"}
+ onClick={() => setShowPassword(!showPassword)}
+ size="lg"
+ color="gray.600"
+ />
+
+
+
+
+ console.log("reCAPTCHA loaded")}
+ onError={error => console.error("reCAPTCHA error:", error)}
+ />
+
+
+
+ router.push("/auth/forgot-password")}
+ color="gray.600"
+ mt="2"
+ fontSize="xs"
+ textAlign="right"
+ textDecoration="underline"
+ fontWeight="medium"
+ >
+ Esqueci minha senha
+
+
+
+
+
+
+
+
+
+ versão {packageJson.version}
+
+
+
+
+
+ );
+}
+
+export const getServerSideProps = async (ctx: any) => {
+ return redirectIfAuthenticated(ctx);
+};
diff --git a/src/features/skatistas/home/index.tsx b/src/features/skatistas/home/index.tsx
new file mode 100644
index 0000000..c483220
--- /dev/null
+++ b/src/features/skatistas/home/index.tsx
@@ -0,0 +1,35 @@
+import { useState } from "react";
+
+import { Skatistas } from "@/features/skatistas";
+import { useUsers } from "@/hooks/useUsers";
+
+export function SkatistasHome() {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(20);
+
+ const { data: paginatedUsers, isPending, isFetching, isError } = useUsers(currentPage, pageSize);
+
+ const handlePageChange = (newPage: number) => {
+ setCurrentPage(newPage);
+ };
+
+ const handlePageSizeChange = (newPageSize: number) => {
+ setPageSize(newPageSize);
+ setCurrentPage(1); // Reset to first page when changing page size
+ };
+
+ if (isPending) return Loading...
;
+ if (isError) return Error loading users
;
+
+ return (
+
+ );
+}
diff --git a/src/features/skatistas/index.tsx b/src/features/skatistas/index.tsx
new file mode 100644
index 0000000..80b6f73
--- /dev/null
+++ b/src/features/skatistas/index.tsx
@@ -0,0 +1,174 @@
+import { TbChevronLeft, TbChevronRight } from "react-icons/tb";
+
+import {
+ Box,
+ Button,
+ Divider,
+ Flex,
+ HStack,
+ Select,
+ SimpleGrid,
+ Spinner,
+ Text,
+ useColorModeValue
+} from "@chakra-ui/react";
+
+import { UserCard } from "@/components/CardUser";
+import type { UserBasicsWithPagination } from "@/types/UserBasicsWithPagination.type";
+
+interface SkatistasProps {
+ users: UserBasicsWithPagination;
+ currentPage: number;
+ pageSize: number;
+ totalUsers: number;
+ isLoading: boolean;
+ onPageChange: (page: number) => void;
+ onPageSizeChange: (pageSize: number) => void;
+}
+
+export function Skatistas({
+ users,
+ currentPage,
+ pageSize,
+ totalUsers,
+ isLoading,
+ onPageChange,
+ onPageSizeChange
+}: SkatistasProps) {
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.800");
+ const totalPages = Math.ceil(totalUsers / pageSize);
+ // const startItem = currentPage;
+ // const endItem = Math.min(currentPage * pageSize, totalUsers);
+
+ const handlePreviousPage = () => {
+ if (currentPage > 1) {
+ onPageChange(currentPage - 1);
+ }
+ };
+
+ const handleNextPage = () => {
+ if (currentPage < totalPages) {
+ onPageChange(currentPage + 1);
+ }
+ };
+
+ const generatePageNumbers = () => {
+ const pages = [];
+ const maxVisiblePages = 5;
+ let startPage = Math.max(0, currentPage - Math.floor(maxVisiblePages / 2));
+ const endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1);
+
+ if (endPage - startPage < maxVisiblePages - 1) {
+ startPage = Math.max(0, endPage - maxVisiblePages + 1);
+ }
+
+ for (let i = startPage; i <= endPage; i++) {
+ pages.push(i);
+ }
+ return pages;
+ };
+
+ return (
+
+
+ {users.data.map(user => (
+
+ ))}
+
+
+
+
+
+ {isLoading ? (
+
+
+ Carregando...
+
+ ) : (
+ <>
+ Skatistas encontrados:{" "}
+
+ {totalUsers}
+
+ >
+ )}
+
+
+
+
+
+
+ Itens por página:
+
+
+
+ {totalPages > 1 && (
+ <>
+
+ }
+ variant="ghost"
+ color="green.400"
+ _hover={{
+ background: "transparent"
+ }}
+ >
+ Anterior
+
+
+ {generatePageNumbers().map(pageNum => (
+
+ ))}
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/src/features/stories/home/index.tsx b/src/features/stories/home/index.tsx
new file mode 100644
index 0000000..2eac75b
--- /dev/null
+++ b/src/features/stories/home/index.tsx
@@ -0,0 +1,48 @@
+import { Box, Divider, Flex, Heading, Spinner, Text, useColorModeValue } from "@chakra-ui/react";
+
+import { StoriesSwiper } from "@/components/StoriesSwiper";
+import { useStories } from "@/hooks/useStories";
+
+export function StoriesHome() {
+ const titleBgColor = useColorModeValue("white", "gray.900");
+ const { data, isLoading, isError } = useStories();
+
+ const stories =
+ data?.data?.map(story => ({
+ id: story.id,
+ storyAuthorId: story.attributes.author.data.id,
+ name: story.attributes.author.data.attributes.username,
+ image: "",
+ isUserOffline: false //TODO: implement logic to determine if the user is offline
+ })) || [];
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+ Erro ao carregar stories.
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+ Online agora
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/features/stories/modal/index.tsx b/src/features/stories/modal/index.tsx
new file mode 100644
index 0000000..19b24fb
--- /dev/null
+++ b/src/features/stories/modal/index.tsx
@@ -0,0 +1,99 @@
+import Stories from "react-insta-stories";
+
+import { Box, Center, Spinner, Text } from "@chakra-ui/react";
+
+import { useStoriesByUserId } from "@/hooks/useStoriesByUserId";
+
+interface StoryItem {
+ id?: number;
+ attributes: {
+ url: string;
+ duration: number;
+ see_more_enabled?: boolean;
+ see_more_text?: string | null;
+ see_more_link?: string | null;
+ createdAt?: string;
+ updatedAt?: string;
+ };
+}
+
+interface StoriesModalProps {
+ userId: string;
+ onClose: () => void;
+}
+
+export function StoriesModal({ userId, onClose }: StoriesModalProps) {
+ const { data, isLoading, isError, error } = useStoriesByUserId(userId);
+
+ console.warn(userId);
+ console.warn("useStoriesByUserId", data);
+
+ const stories = (() => {
+ try {
+ if (!data?.data || !Array.isArray(data.data)) {
+ return [];
+ }
+
+ return data.data.map((story: StoryItem) => ({
+ url: story.attributes.url,
+ duration: story.attributes.duration || 5000,
+ ...(story.attributes.see_more_enabled && {
+ seeMore: ({ close }: { close: () => void }) => {
+ return Hello, click to close this.
;
+ }
+ })
+ }));
+ } catch (error) {
+ console.error("Error transforming stories:", error);
+ return [];
+ }
+ })();
+
+ if (isLoading || !userId) {
+ return (
+
+
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+
+ Erro ao carregar stories
+
+ {error?.message || "Tente novamente mais tarde"}
+
+ Fechar
+
+
+ );
+ }
+
+ return (
+ {
+ console.warn("All stories ended");
+ onClose();
+ }}
+ onStoryEnd={(storyIndex: number) => {
+ console.warn("Story ended:", storyIndex);
+ }}
+ />
+ );
+}
diff --git a/src/features/user/edit/index.tsx b/src/features/user/edit/index.tsx
old mode 100644
new mode 100755
index 191b15b..931d226
--- a/src/features/user/edit/index.tsx
+++ b/src/features/user/edit/index.tsx
@@ -1,34 +1,40 @@
-import { z } from "zod";
+import { useContext, useEffect, useState } from "react";
+import { SubmitHandler, useForm } from "react-hook-form";
import { useRouter } from "next/router";
+
+import { Box, Button, Divider, Flex, Heading, HStack, SimpleGrid, useColorModeValue, VStack } from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
-import { SubmitHandler, useForm } from "react-hook-form";
-import { useContext, useEffect, useState } from "react";
-import { Box, Button, Flex, Heading, Divider, SimpleGrid, VStack, HStack } from "@chakra-ui/react";
+import { z } from "zod";
-import { Input } from "@/shared/components/Form/Input";
import { Toast } from "@/components/Toast";
-import { Layout } from "@/shared/components/Layout";
-import { Textarea } from "@/shared/components/Form/Textarea";
import { AuthContext } from "@/contexts/AuthContext";
+import { VALIDATION_MESSAGES, VALIDATION_RULES } from "@/lib/const";
+import { CATEGORIES } from "@/lib/const/categories";
+import { Input } from "@/shared/components/Form/Input";
+import { Select } from "@/shared/components/Form/Select";
+import { Textarea } from "@/shared/components/Form/Textarea";
type RegisterForm = {
name: string;
email: string;
+ username: string;
+ categoryValue: string;
about: string;
website_url: string;
instagram_url: string;
+ //TODO: implement password change
// password: string;
// password_confirmation: string;
};
const UserEditFormSchema = z.object({
- name: z.string().min(1, "Campo obrigatório."),
- username: z.string().min(1, "Campo obrigatório."),
- email: z.string().email("E-mail inválido.").min(1, "Campo obrigatório."),
- about: z.string().max(255, "Máximo de 255 caracteres."),
+ name: z.string().min(1, VALIDATION_MESSAGES.REQUIRED),
+ email: z.string().email(VALIDATION_MESSAGES.INVALID_EMAIL).min(1, VALIDATION_MESSAGES.REQUIRED),
+ username: z.string().min(1, VALIDATION_MESSAGES.REQUIRED),
+ categoryValue: z.string().nonempty(VALIDATION_MESSAGES.REQUIRED),
+ about: z.string().max(VALIDATION_RULES.ABOUT.MAX_LENGTH, VALIDATION_MESSAGES.ABOUT_MAX_LENGTH),
website_url: z.string(),
instagram_url: z.string()
- // .url("URL inválida.")
});
type UserEditFormSchema = z.infer;
@@ -39,6 +45,10 @@ export function UserEdit() {
const [isError, setIsError] = useState(false);
const { user, updateUser } = useContext(AuthContext);
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.800");
+ const titleBgColor = useColorModeValue("white", "gray.900");
+ const bgCancelButton = useColorModeValue("whiteAlpha.100", "blackAlpha.300");
+
const {
register,
handleSubmit,
@@ -51,16 +61,36 @@ export function UserEdit() {
useEffect(() => {
if (user) {
- reset(user);
+ // reset(user);
+ reset({
+ name: user.name || "",
+ email: user.email || "",
+ username: user.username || "",
+ categoryValue: user.category?.value || "",
+ about: user.about || "",
+ website_url: user.website_url || "",
+ instagram_url: user.instagram_url || ""
+ });
}
}, [user, reset]);
const handleEditUser: SubmitHandler = async values => {
try {
+ const selectedCategory = CATEGORIES.find(cat => cat.value === values.categoryValue);
+
+ if (!selectedCategory) {
+ throw new Error("Categoria inválida");
+ }
+
await updateUser({
id: user ? user.id : "",
name: values.name,
username: user ? user.username : "",
+ category: {
+ id: selectedCategory.id,
+ name: selectedCategory.name,
+ value: selectedCategory.value
+ },
email: values.email,
about: values.about,
website_url: values.website_url,
@@ -84,21 +114,46 @@ export function UserEdit() {
};
return (
-
-
-
-
+ <>
+
+
+
Editar
-
+
+
+
+
+
+
-
-
-
+
+
+
+
@@ -113,18 +168,20 @@ export function UserEdit() {
{...register("instagram_url")}
error={errors.instagram_url}
isInvalid={isError}
+ size="md"
/>
-
+ {/* TODO: implement password change */}
{/*
-
-
+ >
);
}
diff --git a/src/features/user/profile/index.tsx b/src/features/user/profile/index.tsx
new file mode 100644
index 0000000..e3cd904
--- /dev/null
+++ b/src/features/user/profile/index.tsx
@@ -0,0 +1,188 @@
+import React from "react";
+import { FaGlobe, FaInstagram, FaMapMarkerAlt } from "react-icons/fa";
+
+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 titleBgColor = useColorModeValue("white", "gray.900");
+ const cardBg = useColorModeValue("blackAlpha.100", "gray.800");
+ // 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 ;
+ }
+
+ if (error) {
+ return Error: {error.message};
+ }
+
+ return (
+ <>
+
+
+
+ Perfil
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {user?.name}
+ {user?.category && (
+
+ {user?.category?.name}
+
+ )}
+
+
+
+
+
+ {user?.address?.city}, {user?.address?.uf}, {user?.address?.country}
+
+
+
+
+ {user?.instagram_url && (
+ openInstagram(user.instagram_url)} _hover={{ opacity: 0.8 }}>
+
+
+ )}
+ {user?.website_url && (
+ openWebsite(user.website_url)} _hover={{ opacity: 0.8 }}>
+
+
+ )}
+
+
+ {user?.about}
+
+
+
+
+
+
+
+
+ Últimas
+
+
+
+
+
+ {tricks.map(trick => (
+
+
+
+ {trick.title}
+
+ {trick.date}
+
+
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/features/user/signInFormSchema.ts b/src/features/user/signInFormSchema.ts
new file mode 100644
index 0000000..9bd6bc2
--- /dev/null
+++ b/src/features/user/signInFormSchema.ts
@@ -0,0 +1,13 @@
+import { z } from "zod";
+
+import { VALIDATION_MESSAGES } from "@/lib/const/validation";
+
+export const signInFormSchema = z.object({
+ email: z
+ .string()
+ .email({ message: VALIDATION_MESSAGES.INVALID_EMAIL })
+ .min(1, { message: VALIDATION_MESSAGES.REQUIRED }),
+ password: z.string().min(1, { message: VALIDATION_MESSAGES.REQUIRED })
+});
+
+export type SignInFormSchema = z.infer;
diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/hooks/useStories.ts b/src/hooks/useStories.ts
new file mode 100644
index 0000000..0864aff
--- /dev/null
+++ b/src/hooks/useStories.ts
@@ -0,0 +1,15 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { getStories } from "@/services/getStories";
+import type { StoriesResponse } from "@/types/stories";
+
+export function useStories() {
+ return useQuery({
+ queryKey: ["stories"],
+ queryFn: () => {
+ return getStories();
+ },
+ staleTime: 1000 * 60 * 5, // consider data fresh for 5 minutes
+ refetchOnWindowFocus: false
+ });
+}
diff --git a/src/hooks/useStoriesByUserId.ts b/src/hooks/useStoriesByUserId.ts
new file mode 100644
index 0000000..fb8e661
--- /dev/null
+++ b/src/hooks/useStoriesByUserId.ts
@@ -0,0 +1,19 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { getStoriesByUserId } from "@/services/getStories";
+import type { StoriesResponse } from "@/types/stories";
+
+export function useStoriesByUserId(userId: string) {
+ return useQuery({
+ queryKey: ["stories", userId],
+ queryFn: () => {
+ if (!userId) {
+ throw new Error("User ID is required");
+ }
+ return getStoriesByUserId(userId);
+ },
+ enabled: !!userId, // only fetch when userId is defined
+ staleTime: 1000 * 60 * 5, // consider data fresh for 5 minutes
+ refetchOnWindowFocus: false
+ });
+}
diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts
new file mode 100644
index 0000000..75a7c00
--- /dev/null
+++ b/src/hooks/useUser.ts
@@ -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, "queryKey" | "queryFn">;
+
+export function useUser(userId: string | undefined, options?: UseUserOptions) {
+ return useQuery({
+ 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
+ });
+}
diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts
new file mode 100644
index 0000000..21b2a88
--- /dev/null
+++ b/src/hooks/useUsers.ts
@@ -0,0 +1,38 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { getCustomUsersWithPagination } from "@/services/getUsers";
+import { getUsersCount } from "@/services/getUsersCount";
+
+import { UserBasicsWithPagination } from "../types/UserBasicsWithPagination.type";
+
+const fetchUsers = async (
+ currentPage?: number,
+ pageSize?: number,
+ filter?: string
+): Promise<{ users: UserBasicsWithPagination; totalFetchedUsers: number }> => {
+ const [userResponsePaginated, totalCountResponse2] = await Promise.all([
+ getCustomUsersWithPagination(currentPage, pageSize),
+ getUsersCount()
+ ]);
+
+ if (!userResponsePaginated) {
+ throw new Error("Network response was not ok.");
+ }
+
+ return {
+ users: userResponsePaginated,
+ totalFetchedUsers: totalCountResponse2
+ };
+};
+
+const useUsers = (currentPage?: number, pageSize?: number, filter?: string) => {
+ return useQuery({
+ queryKey: ["Users", { currentPage, pageSize, filter }],
+ queryFn: () => fetchUsers(currentPage, pageSize, filter),
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ gcTime: 1000 * 60 * 10, // 10 minutes
+ refetchOnWindowFocus: false
+ });
+};
+
+export { fetchUsers, useUsers };
diff --git a/src/lib/const/categories.ts b/src/lib/const/categories.ts
new file mode 100644
index 0000000..5f2e929
--- /dev/null
+++ b/src/lib/const/categories.ts
@@ -0,0 +1,20 @@
+export const CATEGORIES = [
+ { id: 1, name: "Iniciante", value: "iniciante" },
+ { id: 2, name: "Amador", value: "amador" },
+ { id: 3, name: "Profissional", value: "profissional" },
+ { id: 4, name: "Pro Master", value: "pro-master" },
+ { id: 5, name: "Pro Legend", value: "pro-legend" },
+ { id: 6, name: "Master", value: "master" },
+ { id: 7, name: "Grand Master", value: "grand-master" },
+ { id: 8, name: "Grand Legend", value: "grand-legend" },
+ { id: 9, name: "Vintage", value: "vintage" },
+ { id: 10, name: "Open", value: "open" },
+ { id: 11, name: "Paraskatista", value: "paraskatista" }
+] as const;
+
+export type CategoryValue = (typeof CATEGORIES)[number]["value"];
+export type CategoryId = (typeof CATEGORIES)[number]["id"];
+
+export const getCategoryById = (id: CategoryId) => CATEGORIES.find(cat => cat.id === id);
+
+export const getCategoryByValue = (value: CategoryValue) => CATEGORIES.find(cat => cat.value === value);
diff --git a/src/lib/const/index.ts b/src/lib/const/index.ts
new file mode 100644
index 0000000..2c53872
--- /dev/null
+++ b/src/lib/const/index.ts
@@ -0,0 +1,2 @@
+export * from "./categories";
+export * from "./validation";
diff --git a/src/lib/const/validation.ts b/src/lib/const/validation.ts
new file mode 100644
index 0000000..225c9c9
--- /dev/null
+++ b/src/lib/const/validation.ts
@@ -0,0 +1,44 @@
+export const VALIDATION_RULES = {
+ PASSWORD: {
+ MIN_LENGTH: 6,
+ MAX_LENGTH: 128,
+ REQUIRE_UPPERCASE: true,
+ REQUIRE_NUMBER: true,
+ REQUIRE_SPECIAL_CHAR: false
+ },
+ USERNAME: {
+ MIN_LENGTH: 3,
+ MAX_LENGTH: 30,
+ PATTERN: /^[a-zA-Z0-9_-]+$/
+ },
+ NAME: {
+ MIN_LENGTH: 8,
+ MAX_LENGTH: 100
+ },
+ ABOUT: {
+ MAX_LENGTH: 255
+ },
+ UF: {
+ MAX_LENGTH: 2
+ }
+} as const;
+
+export const VALIDATION_MESSAGES = {
+ REQUIRED: "Campo obrigatório",
+ INVALID_EMAIL: "E-mail deve ser um e-mail válido",
+ USERNAME_IS_EMAIL: "Usuário não pode ser um e-mail",
+ PASSWORDS_DONT_MATCH: "Senhas não conferem",
+ PASSWORD_TOO_SHORT: `Senha deve ter no mínimo ${VALIDATION_RULES.PASSWORD.MIN_LENGTH} caracteres`,
+ PASSWORD_NEEDS_UPPERCASE: "Senha deve conter pelo menos uma letra maiúscula",
+ PASSWORD_NEEDS_NUMBER: "Senha deve conter pelo menos um número",
+ USERNAME_TOO_SHORT: `Usuário deve ter no mínimo ${VALIDATION_RULES.USERNAME.MIN_LENGTH} caracteres`,
+ NAME_TOO_SHORT: `Nome deve ter no mínimo ${VALIDATION_RULES.NAME.MIN_LENGTH} caracteres`,
+ ABOUT_MAX_LENGTH: `Máximo de ${VALIDATION_RULES.ABOUT.MAX_LENGTH} caracteres`
+} as const;
+
+export const REGEX_PATTERNS = {
+ EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+ UPPERCASE: /[A-Z]/,
+ NUMBER: /[0-9]/,
+ SPECIAL_CHAR: /[!@#$%^&*(),.?":{}|<>]/
+} as const;
diff --git a/src/lib/fonts.ts b/src/lib/fonts.ts
old mode 100644
new mode 100755
index 27dc994..5209ad5
--- a/src/lib/fonts.ts
+++ b/src/lib/fonts.ts
@@ -1,4 +1,4 @@
-import { Roboto, Raleway } from "next/font/google";
+import { Raleway, Roboto } from "next/font/google";
const roboto = Roboto({
subsets: ["latin"],
diff --git a/src/lib/theme.ts b/src/lib/theme.ts
new file mode 100644
index 0000000..0c710f1
--- /dev/null
+++ b/src/lib/theme.ts
@@ -0,0 +1,27 @@
+import { modalAnatomy as parts } from "@chakra-ui/anatomy";
+import { createMultiStyleConfigHelpers } from "@chakra-ui/react";
+
+const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(parts.keys);
+
+const baseStyle = definePartsStyle({
+ overlay: {
+ bg: "blackAlpha.200"
+ },
+ dialog: {
+ borderRadius: 0,
+ bg: "transparent",
+ padding: 0,
+ margin: 0,
+ shadow: "none"
+ },
+ body: {
+ padding: 0,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center"
+ }
+});
+
+export const modalTheme = defineMultiStyleConfig({
+ baseStyle
+});
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
old mode 100644
new mode 100755
index b6a5740..35f71ed
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,5 +1,6 @@
-import Link from "next/link";
import Image from "next/image";
+import Link from "next/link";
+
import { Container, Flex, Text } from "@chakra-ui/react";
export default function Error404() {
diff --git a/src/pages/500.tsx b/src/pages/500.tsx
old mode 100644
new mode 100755
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
old mode 100644
new mode 100755
index 3bfa740..1823f22
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,10 +1,13 @@
import { AppProps } from "next/app";
+
import { ChakraProvider } from "@chakra-ui/react";
-import { theme } from "@/styles/theme";
-import { fonts } from "@/lib/fonts";
+import QueryProvider from "@/components/QueryProvider";
import { AuthProvider } from "@/contexts/AuthContext";
import { SidebarDrawerProvider } from "@/contexts/SidebarDrawerContext";
+import { fonts } from "@/lib/fonts";
+import { Layout } from "@/shared/components/Layout";
+import { theme } from "@/styles/theme";
function MyApp({ Component, pageProps }: AppProps) {
return (
@@ -15,13 +18,22 @@ function MyApp({ Component, pageProps }: AppProps) {
--font-roboto: ${fonts.roboto.style.fontFamily};
--font-raleway: ${fonts.raleway.style.fontFamily};
}
+ #__next {
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ }
`}
-
+
+
+
+
+
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
old mode 100644
new mode 100755
diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts
old mode 100644
new mode 100755
index 5c1c984..b0ec1dc
--- a/src/pages/api/auth/[...nextauth].ts
+++ b/src/pages/api/auth/[...nextauth].ts
@@ -1,7 +1,8 @@
import NextAuth from "next-auth";
-import { signInRequest } from "../../../services/auth";
import CredentialsProvider from "next-auth/providers/credentials";
+import { signInRequest } from "../../../services/auth";
+
export default NextAuth({
providers: [
CredentialsProvider({
diff --git a/src/pages/api/sendConfirmationEmail.ts b/src/pages/api/sendConfirmationEmail.ts
old mode 100644
new mode 100755
index 361cfc9..ccc3a9f
--- a/src/pages/api/sendConfirmationEmail.ts
+++ b/src/pages/api/sendConfirmationEmail.ts
@@ -1,13 +1,14 @@
-import nodemailer from "nodemailer";
import { NextApiRequest, NextApiResponse } from "next";
+import nodemailer from "nodemailer";
+
import {
+ NODEMAILER_OPTIONS_FROM,
+ NODEMAILER_OPTIONS_TO,
NODEMAILER_REQUEST_SERVER,
- NODEMAILER_TRANSPORTER_SERVICE,
- NODEMAILER_TRANSPORTER_USER,
NODEMAILER_TRANSPORTER_PASS,
- NODEMAILER_OPTIONS_FROM,
- NODEMAILER_OPTIONS_TO
+ NODEMAILER_TRANSPORTER_SERVICE,
+ NODEMAILER_TRANSPORTER_USER
} from "@/utils/constant";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
diff --git a/src/pages/api/sitemap.ts b/src/pages/api/sitemap.ts
old mode 100644
new mode 100755
diff --git a/src/pages/auth/confirmation.tsx b/src/pages/auth/confirmation.tsx
old mode 100644
new mode 100755
index 5095d31..6cb9907
--- a/src/pages/auth/confirmation.tsx
+++ b/src/pages/auth/confirmation.tsx
@@ -1,9 +1,10 @@
+import { useEffect, useRef, useState } from "react";
import Head from "next/head";
-import Link from "next/link";
import Image from "next/image";
+import Link from "next/link";
import { useRouter } from "next/router";
+
import { Flex, Text } from "@chakra-ui/react";
-import { useEffect, useRef, useState } from "react";
import { Toast } from "@/components/Toast";
@@ -62,6 +63,7 @@ export default function Confirmation() {
route.push("/");
}, 3000);
} catch (error) {
+ console.error(error);
addToast({
title: "Erro de processamento",
message: "Ocorreu um erro inesperado. Tente novamente.",
diff --git a/src/pages/auth/forgot-password.tsx b/src/pages/auth/forgot-password.tsx
old mode 100644
new mode 100755
index 5ed57d6..bee180a
--- a/src/pages/auth/forgot-password.tsx
+++ b/src/pages/auth/forgot-password.tsx
@@ -1,20 +1,21 @@
+import { SubmitHandler, useForm } from "react-hook-form";
+import { RiAlertLine } from "react-icons/ri";
import Head from "next/head";
-import Link from "next/link";
-import Image from "next/image";
-import { z } from "zod";
import { useRouter } from "next/router";
+
+import { Box, Button, Flex, Stack, Text, useColorModeValue } from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
-import { RiAlertLine } from "react-icons/ri";
-import { SubmitHandler, useForm } from "react-hook-form";
-import { Box, Button, Divider, Flex, Stack, Text } from "@chakra-ui/react";
+import { z } from "zod";
-import { API } from "@/utils/constant";
+import { TitleSection } from "@/components/TitleSection";
import { Toast } from "@/components/Toast";
+import { VALIDATION_MESSAGES } from "@/lib/const/validation";
import { Input } from "@/shared/components/Form/Input";
import { redirectIfAuthenticated } from "@/utils/auth";
+import { API } from "@/utils/constant";
const forgotPasswordFormSchema = z.object({
- email: z.string().email("E-mail inválido.").nonempty("Campo obrigatório.")
+ email: z.string().email(VALIDATION_MESSAGES.INVALID_EMAIL)
});
type ForgotPasswordFormSchema = z.infer;
@@ -23,6 +24,8 @@ export default function ForgotPassword() {
const route = useRouter();
const { addToast } = Toast();
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.800");
+
const {
handleSubmit,
register,
@@ -86,41 +89,18 @@ export default function ForgotPassword() {
Esqueci minha senha - SkateHub
-
+
+
-
-
-
-
-
- Recuperar senha
-
-
-
-
-
-
-
-
- Enviaremos um link para você criar uma nova senha.
-
-
-
-
- Enviar link
-
+
+
+ Enviar link
+
+
+
+
+ Enviaremos um link para você criar uma nova senha.
+
+
+
>
diff --git a/src/pages/auth/index.tsx b/src/pages/auth/index.tsx
old mode 100644
new mode 100755
diff --git a/src/pages/auth/reset-password.tsx b/src/pages/auth/reset-password.tsx
old mode 100644
new mode 100755
index 93d7f69..f6730f0
--- a/src/pages/auth/reset-password.tsx
+++ b/src/pages/auth/reset-password.tsx
@@ -1,16 +1,17 @@
+import { SubmitHandler, useForm } from "react-hook-form";
import Head from "next/head";
-import Link from "next/link";
import Image from "next/image";
-import { z } from "zod";
+import Link from "next/link";
import { useRouter } from "next/router";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { SubmitHandler, useForm } from "react-hook-form";
+
import { Button, Divider, Flex, Stack, Text } from "@chakra-ui/react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
-import { API } from "@/utils/constant";
import { Toast } from "@/components/Toast";
import { Input } from "@/shared/components/Form/Input";
import { redirectIfAuthenticated } from "@/utils/auth";
+import { API } from "@/utils/constant";
const resetPasswordFormSchema = z
.object({
diff --git a/src/pages/auth/signin.tsx b/src/pages/auth/signin.tsx
old mode 100644
new mode 100755
index 9eec434..a97d073
--- a/src/pages/auth/signin.tsx
+++ b/src/pages/auth/signin.tsx
@@ -1,45 +1,40 @@
-import Head from "next/head";
-import Link from "next/link";
+import { useContext, useRef, useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
-import { z } from "zod";
-import { useRouter } from "next/router";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
import { SubmitHandler, useForm } from "react-hook-form";
-import { useContext, useRef, useState } from "react";
+import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
+import { RiAlertLine } from "react-icons/ri";
+import Head from "next/head";
+import { useRouter } from "next/router";
+
import {
Box,
Button,
Flex,
- Stack,
- Text,
- Link as ChakraLink,
+ IconButton,
InputGroup,
InputRightElement,
- IconButton
+ Stack,
+ Text,
+ useColorModeValue
} from "@chakra-ui/react";
+import { zodResolver } from "@hookform/resolvers/zod";
-import { Input } from "@/shared/components/Form/Input";
+import { TitleSection } from "@/components/TitleSection";
import { Toast } from "@/components/Toast";
import { AuthContext } from "@/contexts/AuthContext";
-import { LogoSkateHub } from "@/components/LogoSkateHub";
+import { SignInFormSchema, signInFormSchema } from "@/features/user/signInFormSchema";
+import { Input } from "@/shared/components/Form/Input";
import { redirectIfAuthenticated } from "@/utils/auth";
-const signInFormSchema = z.object({
- email: z.string().email({ message: "E-mail deve ser um e-mail válido." }).min(1, { message: "Campo obrigatório." }),
- password: z.string().min(1, { message: "Campo obrigatório." })
-});
-
-type SignInFormSchema = z.infer;
-
export default function SignIn() {
const router = useRouter();
const { signIn } = useContext(AuthContext);
const { addToast } = Toast();
- const [isVerified, setIsVerified] = useState(false);
- const [isVerifiedError, setIsVerifiedError] = useState(false);
const recaptchaRef = useRef(null);
const [showPassword, setShowPassword] = useState(false);
+ const [isExecutingRecaptcha, setIsExecutingRecaptcha] = useState(false);
+
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.800");
const {
handleSubmit,
@@ -52,95 +47,133 @@ export default function SignIn() {
});
const handleSignIn: SubmitHandler = async values => {
- if (isVerified) {
- const recaptchaValue = recaptchaRef.current?.getValue();
- const newValues = { ...values, recaptcha: recaptchaValue || undefined };
+ if (recaptchaRef.current) {
+ try {
+ setIsExecutingRecaptcha(true);
+ // for v2 invisible, use executeAsync(). For v3, use execute()
+ const recaptchaValue =
+ (await recaptchaRef.current.executeAsync?.()) || (await recaptchaRef.current.execute?.());
+ const newValues = { ...values, recaptcha: recaptchaValue || undefined };
- await signIn(newValues)
- .then(_ => {})
- .catch(error => {
- recaptchaRef.current?.reset();
- setIsVerified(false);
+ await signIn(newValues)
+ .then(_ => {
+ recaptchaRef.current?.reset();
+ })
+ .catch(error => {
+ recaptchaRef.current?.reset();
- switch (error.response.data.error?.message) {
- case "Your account email is not confirmed":
- addToast({
- title: "Erro de autenticação.",
- message: "Confirme seu e-mail para acessar a plataforma.",
- type: "warning"
- });
- break;
- case "Your account has been blocked by an administrator":
- addToast({
- title: "Erro de autenticação.",
- message:
- "Sua conta está temporariamente bloqueada. Se você acabou de se cadastrar, por favor, aguarde enquanto suas informações estão sendo revisadas por nossa equipe.",
- type: "error"
- });
- break;
- default:
- addToast({
- title: "Erro de autenticação.",
- message: "Verifique seus dados de login e tente novamente.",
- type: "error"
- });
- break;
- }
+ switch (error.response.data.error?.message) {
+ case "Your account email is not confirmed":
+ addToast({
+ title: "Erro de autenticação.",
+ message: "Confirme seu e-mail para acessar a plataforma.",
+ type: "warning"
+ });
+ break;
+ case "Your account has been blocked by an administrator":
+ addToast({
+ title: "Erro de autenticação.",
+ message:
+ "Sua conta está temporariamente bloqueada. Se você acabou de se cadastrar, por favor, aguarde enquanto suas informações estão sendo revisadas por nossa equipe.",
+ type: "error"
+ });
+ break;
+ default:
+ addToast({
+ title: "Erro de autenticação.",
+ message: "Verifique seus dados de login e tente novamente.",
+ type: "error"
+ });
+ break;
+ }
+ });
+ } catch (error) {
+ console.error("reCAPTCHA execution failed:", error);
+ addToast({
+ title: "Erro de verificação.",
+ message: "Falha na verificação de segurança. Tente novamente.",
+ type: "error"
});
+ } finally {
+ setIsExecutingRecaptcha(false);
+ }
} else {
- setIsVerifiedError(true);
- console.log("Please verify the reCAPTCHA.");
+ addToast({
+ title: "Erro de verificação.",
+ message: "Sistema de verificação não está disponível.",
+ type: "error"
+ });
}
};
+ // if (isVerified) {
+ // const recaptchaValue = recaptchaRef.current?.getValue();
+ // const newValues = { ...values, recaptcha: recaptchaValue || undefined };
- const onVerify = (token: string | null) => {
- if (!token) return;
+ // await signIn(newValues)
+ // .then(_ => { })
+ // .catch(error => {
+ // recaptchaRef.current?.reset();
+ // setIsVerified(false);
- if (token) {
- setIsVerified(true);
- setIsVerifiedError(false);
- }
- };
+ // switch (error.response.data.error?.message) {
+ // case "Your account email is not confirmed":
+ // addToast({
+ // title: "Erro de autenticação.",
+ // message: "Confirme seu e-mail para acessar a plataforma.",
+ // type: "warning"
+ // });
+ // break;
+ // case "Your account has been blocked by an administrator":
+ // addToast({
+ // title: "Erro de autenticação.",
+ // message:
+ // "Sua conta está temporariamente bloqueada. Se você acabou de se cadastrar, por favor, aguarde enquanto suas informações estão sendo revisadas por nossa equipe.",
+ // type: "error"
+ // });
+ // break;
+ // default:
+ // addToast({
+ // title: "Erro de autenticação.",
+ // message: "Verifique seus dados de login e tente novamente.",
+ // type: "error"
+ // });
+ // break;
+ // }
+ // });
+ // } else {
+ // setIsVerifiedError(true);
+ // console.log("Please verify the reCAPTCHA.");
+ // }
+ // };
+
+ // const onVerify = (token: string | null) => {
+ // if (!token) return;
+
+ // if (token) {
+ // setIsVerified(true);
+ // setIsVerifiedError(false);
+ // }
+ // };
return (
<>
Login - SkateHub
-
+
+
-
-
-
-
-
-
+
-
-
-
-
-
- Se precisar de ajuda, entre em{" "}
-
- contato conosco
-
- .
-
-
-
+
-
-
- {isVerifiedError && (
-
- Please verify that you are not a robot.
-
- )}
+
+
+
+ Entrar
+
+ router.push("/auth/forgot-password")}
+ >
+ Esqueci minha senha
+
-
-
- Entrar
-
- router.push("/auth/forgot-password")}
- color="gray.600"
- mt="4"
- textAlign="center"
- textDecoration="underline"
- fontWeight="medium"
- >
- Esqueci minha senha
-
+
+
+
+ Se precisar de ajuda, entre em contato conosco.
+
+
+
+ console.log("reCAPTCHA loaded")}
+ onError={error => console.error("reCAPTCHA error:", error)}
+ />
>
);
}
diff --git a/src/pages/auth/signup.tsx b/src/pages/auth/signup.tsx
old mode 100644
new mode 100755
index 25e2e05..7288f72
--- a/src/pages/auth/signup.tsx
+++ b/src/pages/auth/signup.tsx
@@ -1,40 +1,78 @@
-import Head from "next/head";
-import Link from "next/link";
-import Image from "next/image";
+import { useRef, useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
-import { z } from "zod";
-import { useRouter } from "next/router";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { RiAlertLine } from "react-icons/ri";
-import { useState, useRef } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
-import { Button, Flex, Text, Stack, Box, Divider, InputGroup, InputRightElement, IconButton } from "@chakra-ui/react";
+import { RiAlertLine } from "react-icons/ri";
+import Head from "next/head";
+import { useRouter } from "next/router";
-import { API } from "@/utils/constant";
-import { Input } from "@/shared/components/Form/Input";
+import {
+ Box,
+ Button,
+ Flex,
+ IconButton,
+ InputGroup,
+ InputRightElement,
+ Stack,
+ Text,
+ useColorModeValue
+} from "@chakra-ui/react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { TitleSection } from "@/components/TitleSection";
import { Toast } from "@/components/Toast";
+import { CATEGORIES, getCategoryByValue } from "@/lib/const/categories";
+import { REGEX_PATTERNS, VALIDATION_MESSAGES, VALIDATION_RULES } from "@/lib/const/validation";
+import { Input } from "@/shared/components/Form/Input";
+import { Select } from "@/shared/components/Form/Select";
import { redirectIfAuthenticated } from "@/utils/auth";
+import { API } from "@/utils/constant";
const signUpSchema = z
.object({
- name: z.string().nonempty("Campo obrigatório.").min(8, { message: "Nome deve ter no mínimo 8 caracteres." }),
+ name: z
+ .string()
+ .nonempty(VALIDATION_MESSAGES.REQUIRED)
+ .min(VALIDATION_RULES.NAME.MIN_LENGTH, { message: VALIDATION_MESSAGES.NAME_TOO_SHORT }),
+ email: z.string().nonempty(VALIDATION_MESSAGES.REQUIRED).email({ message: VALIDATION_MESSAGES.INVALID_EMAIL }),
username: z
.string()
- .nonempty("Campo obrigatório.")
- .min(3, { message: "Usuário deve ter no mínimo 3 caracteres." })
- .refine(value => !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), {
- message: "Usuário não pode ser um e-mail."
+ .nonempty(VALIDATION_MESSAGES.REQUIRED)
+ .min(VALIDATION_RULES.USERNAME.MIN_LENGTH, { message: VALIDATION_MESSAGES.USERNAME_TOO_SHORT })
+ .refine(value => !REGEX_PATTERNS.EMAIL.test(value), {
+ message: VALIDATION_MESSAGES.USERNAME_IS_EMAIL
}),
- email: z.string().nonempty("Campo obrigatório.").email({ message: "E-mail deve ser um e-mail válido." }),
- password: z.string().nonempty("Campo obrigatório.").min(6, { message: "Senha deve ter no mínimo 6 caracteres." }),
- confirmPassword: z.string().nonempty("Campo obrigatório.")
+ category: z.enum(
+ [
+ "iniciante",
+ "amador",
+ "profissional",
+ "pro-master",
+ "pro-legend",
+ "master",
+ "grand-master",
+ "grand-legend",
+ "vintage",
+ "open",
+ "paraskatista"
+ ],
+ { errorMap: () => ({ message: VALIDATION_MESSAGES.REQUIRED }) }
+ ),
+ city: z.string().nonempty(VALIDATION_MESSAGES.REQUIRED),
+ uf: z.string().nonempty(VALIDATION_MESSAGES.REQUIRED),
+ country: z.string().nonempty(VALIDATION_MESSAGES.REQUIRED),
+ password: z
+ .string()
+ .nonempty(VALIDATION_MESSAGES.REQUIRED)
+ .min(VALIDATION_RULES.PASSWORD.MIN_LENGTH, { message: VALIDATION_MESSAGES.PASSWORD_TOO_SHORT }),
+ confirmPassword: z.string().nonempty(VALIDATION_MESSAGES.REQUIRED)
})
.superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: "custom",
- message: "Senhas não conferem.",
+ message: VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH,
path: ["confirmPassword"]
});
}
@@ -45,11 +83,12 @@ type SignUpSchema = z.infer;
export default function SignUp() {
const route = useRouter();
const { addToast } = Toast();
- const [isVerified, setIsVerified] = useState(false);
- const [isVerifiedError, setIsVerifiedError] = useState(false);
const recaptchaRef = useRef(null);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [isExecutingRecaptcha, setIsExecutingRecaptcha] = useState(false);
+
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.800");
const {
handleSubmit,
@@ -62,71 +101,69 @@ export default function SignUp() {
mode: "onChange"
});
- const onFinish: SubmitHandler = async values => {
- if (!isVerified) {
- setIsVerifiedError(true);
- console.log("Please verify the reCAPTCHA.");
- return;
- }
-
- try {
- const recaptchaValue = recaptchaRef.current?.getValue();
+ const handleSignUp: SubmitHandler = async values => {
+ if (recaptchaRef.current) {
+ try {
+ setIsExecutingRecaptcha(true);
+ // for v2 invisible, use executeAsync(). For v3, use execute()
+ const recaptchaValue =
+ (await recaptchaRef.current.executeAsync?.()) || (await recaptchaRef.current.execute?.());
- const response = await fetch(`${API}/api/auth/local/register`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({ ...values, recaptcha: recaptchaValue })
- });
+ const selectedCategory2 = getCategoryByValue(values.category);
- const data = await response.json();
+ const newValues = {
+ ...values,
+ category: selectedCategory2?.id,
+ recaptcha: recaptchaValue || undefined
+ };
- if (data.error) {
- recaptchaRef.current?.reset();
- reset();
- setIsVerified(false);
- throw data.error;
- } else {
- addToast({
- title: "Cadastro efetuado com sucesso.",
- message:
- "Você receberá um e-mail para confirmar o seu endereço de e-mail. Por favor, verifique sua caixa de entrada.",
- type: "success"
+ const response = await fetch(`${API}/api/auth/local/register`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(newValues)
});
- setTimeout(() => {
- route.push("/");
- }, 3000);
- }
- } catch (error: any) {
- if (error.status === 400) {
- addToast({
- title: "Usuário já cadastrado.",
- message:
- "Este usuário já está registrado em nosso sistema. Por favor, faça login ou recupere sua senha, se necessário.",
- type: "error"
- });
- } else {
- addToast({
- title: "Erro ao processar solicitação.",
- message: "Houve um erro ao tentar criar sua conta. Por favor, verifique seus dados e tente novamente.",
- type: "error"
- });
- }
+ const data = await response.json();
- recaptchaRef.current?.reset();
- reset();
- setIsVerified(false);
- }
- };
+ if (data.error) {
+ recaptchaRef.current?.reset();
+ reset();
+ setIsExecutingRecaptcha(false);
+ throw data.error;
+ } else {
+ addToast({
+ title: "Cadastro efetuado com sucesso.",
+ message:
+ "Você receberá um e-mail para confirmar o seu endereço de e-mail. Por favor, verifique sua caixa de entrada.",
+ type: "success"
+ });
- const onVerify = (token: string | null) => {
- if (!token) return;
+ setTimeout(() => {
+ route.push("/");
+ }, 3000);
+ }
+ } catch (error: any) {
+ if (error.status === 400) {
+ addToast({
+ title: "Usuário já cadastrado.",
+ message:
+ "Este usuário já está registrado em nosso sistema. Por favor, faça login ou recupere sua senha, se necessário.",
+ type: "error"
+ });
+ } else {
+ addToast({
+ title: "Erro ao processar solicitação.",
+ message: "Houve um erro ao tentar criar sua conta. Por favor, verifique seus dados e tente novamente.",
+ type: "error"
+ });
+ }
- if (token) {
- setIsVerified(true);
- setIsVerifiedError(false);
+ recaptchaRef.current?.reset();
+ reset();
+ setIsExecutingRecaptcha(false);
+ }
}
};
@@ -135,42 +172,19 @@ export default function SignUp() {
Cadastrar - SkateHub
-
+
+
-
-
-
-
-
- Criar uma conta
-
-
-
-
-
+
+
+
+
+
+
+
@@ -240,43 +292,41 @@ export default function SignUp() {
-
-
-
-
- Verifique sua caixa de entrada para o e-mail de confirmação.
- {/* Assim que seu cadastro for aprovado, você receberá um e-mail de confirmação para realizar o login na plataforma e preencher seu cadastro completo. */}
-
-
-
console.log("reCAPTCHA loaded")}
+ onError={error => console.error("reCAPTCHA error:", error)}
/>
- {isVerifiedError && (
-
- Please verify that you are not a robot.
-
- )}
-
- Cadastrar
-
+
+
+
+ Cadastrar
+
+
+
+
+ Verifique sua caixa de entrada para o e-mail de confirmação.
+
+
+
>
diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx
old mode 100644
new mode 100755
index bd007c7..8e2349e
--- a/src/pages/dashboard.tsx
+++ b/src/pages/dashboard.tsx
@@ -1,25 +1,36 @@
import { useContext } from "react";
-import { parseCookies } from "nookies";
+
+import { Box } from "@chakra-ui/react";
+
+import { TitleSection } from "@/components/TitleSection";
+// import { parseCookies } from "nookies";
import { AuthContext } from "@/contexts/AuthContext";
import { Dashboard } from "@/features/dashboard";
export default function DashboardPage() {
const { user } = useContext(AuthContext);
- return ;
+ return (
+ <>
+
+
+
+
+ >
+ );
}
-export const getServerSideProps = async (ctx: any) => {
- const { ["auth.token"]: token } = parseCookies(ctx);
+// export const getServerSideProps = async (ctx: any) => {
+// const { ["auth.token"]: token } = parseCookies(ctx);
- if (!token) {
- return {
- redirect: {
- destination: "/",
- permanent: false
- }
- };
- }
- return {
- props: {}
- };
-};
+// if (!token) {
+// return {
+// redirect: {
+// destination: "/",
+// permanent: false
+// }
+// };
+// }
+// return {
+// props: {}
+// };
+// };
diff --git a/src/pages/general.tsx b/src/pages/general.tsx
old mode 100644
new mode 100755
index cf762b6..a57473a
--- a/src/pages/general.tsx
+++ b/src/pages/general.tsx
@@ -1,10 +1,21 @@
-import { Layout } from "@/shared/components/Layout";
-export default function GeneralPage() {
+import { Box, Flex, Grid, useColorModeValue } from "@chakra-ui/react";
+
+import { TitleSection } from "@/components/TitleSection";
+
+export default function AthleteRegistrationPage() {
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.800");
return (
-
-
-
General Page
-
-
+ <>
+
+
+
+
+ Image 1
+ Image 2
+ Image 3
+
+
+
+ >
);
}
diff --git a/src/pages/index-bkp.tsx b/src/pages/index-bkp.tsx
new file mode 100755
index 0000000..6cbf0ba
--- /dev/null
+++ b/src/pages/index-bkp.tsx
@@ -0,0 +1,123 @@
+import Head from "next/head";
+import { useRouter } from "next/router";
+
+import { Button, Flex, Grid, GridItem, Stack, Text } from "@chakra-ui/react";
+
+import { LogoSkateHub } from "@/components/LogoSkateHub";
+// import { useEffect } from "react";
+// import { destroyCookie, parseCookies } from "nookies";
+import { redirectIfAuthenticated } from "@/utils/auth";
+
+import packageJson from "../../package.json";
+
+export default function Home() {
+ const router = useRouter();
+
+ return (
+ <>
+
+ Home - SkateHub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Junte-se à comunidade! Faça parte da evolução do esporte.
+
+
+ Faça login ou cadastre-se para começar a explorar todas as funcionalidades.
+
+
+
+ router.push("/auth/signin")}
+ >
+ Login
+
+ router.push("/auth/signup")}
+ >
+ Criar uma conta
+
+
+
+
+
+ Termos de uso
+
+
+ |
+
+
+ Política de privacidade
+
+
+
+
+ versão {packageJson.version}
+
+
+
+
+
+ >
+ );
+}
+
+export const getServerSideProps = async (ctx: any) => {
+ return redirectIfAuthenticated(ctx);
+};
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 1357cf9..c06bc99 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,121 +1,13 @@
-import Head from "next/head";
-import { useRouter } from "next/router";
-import { Button, Flex, Grid, GridItem, Stack, Text } from "@chakra-ui/react";
+import { Box } from "@chakra-ui/react";
-import packageJson from "../../package.json";
-import { LogoSkateHub } from "@/components/LogoSkateHub";
-// import { useEffect } from "react";
-// import { destroyCookie, parseCookies } from "nookies";
-import { redirectIfAuthenticated } from "@/utils/auth";
-
-export default function Home() {
- const router = useRouter();
+import { SkatistasHome } from "@/features/skatistas/home";
+import { StoriesHome } from "@/features/stories/home";
+export default function DashboardPage() {
return (
- <>
-
- Home - SkateHub
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Junte-se à comunidade! Faça parte da evolução do esporte.
-
-
- Faça login ou cadastre-se para começar a explorar todas as funcionalidades.
-
-
-
- router.push("/auth/signin")}
- >
- Login
-
- router.push("/auth/signup")}
- >
- Criar uma conta
-
-
-
-
-
- Termos de uso
-
-
- |
-
-
- Política de privacidade
-
-
-
-
- versão {packageJson.version}
-
-
-
-
-
- >
+
+
+
+
);
}
-
-export const getServerSideProps = async (ctx: any) => {
- return redirectIfAuthenticated(ctx);
-};
diff --git a/src/pages/skatistas.tsx b/src/pages/skatistas.tsx
new file mode 100644
index 0000000..aa80bed
--- /dev/null
+++ b/src/pages/skatistas.tsx
@@ -0,0 +1,39 @@
+import { useState } from "react";
+
+import { TitleSection } from "@/components/TitleSection";
+import { Skatistas } from "@/features/skatistas";
+import { useUsers } from "@/hooks/useUsers";
+
+export default function SkatistasPage() {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+
+ const { data: paginatedUsers, isPending, isFetching, isError } = useUsers(currentPage, pageSize);
+
+ const handlePageChange = (newPage: number) => {
+ setCurrentPage(newPage);
+ };
+
+ const handlePageSizeChange = (newPageSize: number) => {
+ setPageSize(newPageSize);
+ setCurrentPage(1); // Reset to first page when changing page size
+ };
+
+ if (isPending) return Loading...
;
+ if (isError) return Error loading users
;
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/pages/spots/index.tsx b/src/pages/spots/index.tsx
new file mode 100644
index 0000000..4a98440
--- /dev/null
+++ b/src/pages/spots/index.tsx
@@ -0,0 +1,21 @@
+import { Box, Flex, Grid, useColorModeValue } from "@chakra-ui/react";
+
+import { TitleSection } from "@/components/TitleSection";
+
+export default function SpotsPage() {
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.800");
+ return (
+ <>
+
+
+
+
+ Image 1
+ Image 2
+ Image 3
+
+
+
+ >
+ );
+}
diff --git a/src/pages/user/[id]/index.tsx b/src/pages/user/[id]/index.tsx
new file mode 100644
index 0000000..00aaf4f
--- /dev/null
+++ b/src/pages/user/[id]/index.tsx
@@ -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 ;
+}
diff --git a/src/pages/user/edit.tsx b/src/pages/user/edit.tsx
old mode 100644
new mode 100755
index 6c1043e..aeb9357
--- a/src/pages/user/edit.tsx
+++ b/src/pages/user/edit.tsx
@@ -1,4 +1,5 @@
import { parseCookies } from "nookies";
+
import { UserEdit } from "@/features/user/edit";
export default function UserEditPage() {
diff --git a/src/services/auth.ts b/src/services/auth.ts
old mode 100644
new mode 100755
index 86fbb11..278c47f
--- a/src/services/auth.ts
+++ b/src/services/auth.ts
@@ -1,4 +1,5 @@
import axios from "axios";
+
import { API } from "@/utils/constant";
type SignInData = {
@@ -6,12 +7,19 @@ type SignInData = {
password: string;
};
+type Category = {
+ id: number;
+ name: string;
+ value: string;
+};
+
type UpdateUserData = {
id: string;
name: string;
email: string;
- about: string;
username: string;
+ category: Category;
+ about: string;
website_url?: string;
instagram_url?: string;
};
@@ -41,17 +49,29 @@ export async function userMe(token: string) {
}
export async function updateUserProfile(token: string, data: UpdateUserData) {
- const formData = new FormData();
- formData.append("name", data.name);
- formData.append("email", data.email);
- formData.append("about", data.about);
- formData.append("website_url", data.website_url || "");
- formData.append("instagram_url", data.instagram_url || "");
-
- const res = await axios.put(`${API}/api/users/${data.id}`, formData, {
+ // const formData = new FormData();
+ // formData.append("name", data.name);
+ // formData.append("email", data.email);
+ // formData.append("category.name", data.category.name);
+ // formData.append("category.value", data.category.value);
+ // formData.append("about", data.about);
+ // formData.append("website_url", data.website_url || "");
+ // formData.append("instagram_url", data.instagram_url || "");
+
+ const payload = {
+ name: data.name,
+ email: data.email,
+ username: data.username,
+ category: data.category.id,
+ about: data.about,
+ website_url: data.website_url || "",
+ instagram_url: data.instagram_url || ""
+ };
+
+ const res = await axios.put(`${API}/api/users/${data.id}`, payload, {
headers: {
Authorization: `Bearer ${token}`,
- "Content-Type": "multipart/form-data"
+ "Content-Type": "application/json"
}
});
diff --git a/src/services/getStories.ts b/src/services/getStories.ts
new file mode 100644
index 0000000..5e65bc8
--- /dev/null
+++ b/src/services/getStories.ts
@@ -0,0 +1,28 @@
+import axios from "axios";
+
+import type { StoriesResponse } from "@/types/stories";
+import { API } from "@/utils/constant";
+
+export async function getStories(): Promise {
+ try {
+ const res = await axios.get(
+ `${API}/api/stories?populate[author][fields][0]=username&populate[author][fields][1]=avatar`
+ );
+ return res.data;
+ } catch (error) {
+ console.error("Error fetching stories:", error);
+ throw error;
+ }
+}
+
+export async function getStoriesByUserId(userId: string): Promise {
+ try {
+ const res = await axios.get(
+ `${API}/api/stories?filters[author][id][$eq]=${userId}&populate[author][fields][0]=username&populate[author][fields][1]=avatar`
+ );
+ return res.data;
+ } catch (error) {
+ console.error("Error fetching stories by user ID:", error);
+ throw error;
+ }
+}
diff --git a/src/services/getUser.ts b/src/services/getUser.ts
new file mode 100644
index 0000000..96cc95d
--- /dev/null
+++ b/src/services/getUser.ts
@@ -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 {
+ 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}`);
+ }
+}
diff --git a/src/services/getUsers.ts b/src/services/getUsers.ts
new file mode 100644
index 0000000..b0b1be2
--- /dev/null
+++ b/src/services/getUsers.ts
@@ -0,0 +1,28 @@
+import axios from "axios";
+
+import { API } from "@/utils/constant";
+
+export async function getUsers() {
+ const res = await axios.get(`${API}/api/users?populate=avatar,address`);
+
+ return res.data;
+}
+
+export async function getUsers2(currentPage?: number, pageSize?: number) {
+ if (currentPage === undefined) currentPage = 1;
+ if (pageSize === undefined) pageSize = 10;
+
+ const res = await axios.get(
+ `${API}/api/users?populate=avatar,address&start=${currentPage * pageSize}&limit=${pageSize}`
+ );
+
+ return res.data;
+}
+
+export async function getCustomUsersWithPagination(currentPage?: number, pageSize?: number) {
+ const res = await axios.get(
+ `${API}/api/custom-users?populate[0]=address&populate[1]=avatar&populate[2]=category&pagination[page]=${currentPage}&pagination[pageSize]=${pageSize}&pagination[withCount]=true`
+ );
+
+ return res.data;
+}
diff --git a/src/services/getUsersCount.ts b/src/services/getUsersCount.ts
new file mode 100644
index 0000000..d8b24ff
--- /dev/null
+++ b/src/services/getUsersCount.ts
@@ -0,0 +1,9 @@
+import axios from "axios";
+
+import { API } from "@/utils/constant";
+
+export async function getUsersCount() {
+ const res = await axios.get(`${API}/api/users/count`);
+
+ return res.data;
+}
diff --git a/src/shared/components/Form/Input.tsx b/src/shared/components/Form/Input.tsx
old mode 100644
new mode 100755
index 5d37413..951b1aa
--- a/src/shared/components/Form/Input.tsx
+++ b/src/shared/components/Form/Input.tsx
@@ -1,15 +1,15 @@
-import { FieldError } from "react-hook-form";
import { forwardRef, ForwardRefRenderFunction } from "react";
+import { FieldError } from "react-hook-form";
+
import {
- Text,
- Input as ChakraInput,
- FormLabel,
FormControl,
- InputProps as ChakraInputProps,
FormErrorMessage,
+ FormLabel,
+ Input as ChakraInput,
InputGroup,
InputLeftAddon,
- InputRightAddon
+ InputProps as ChakraInputProps,
+ useColorModeValue
} from "@chakra-ui/react";
interface InputProps extends ChakraInputProps {
@@ -23,6 +23,10 @@ const InputBase: ForwardRefRenderFunction = (
{ name, label, error = null, isInputGroup, InputLeftAddonText, ...rest },
ref
) => {
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.900");
+ const bgInputGroupColor = useColorModeValue("blackAlpha.200", "blackAlpha.500");
+ const textColor = useColorModeValue("gray.800", "gray.100");
+ const bgHoverColor = useColorModeValue("blackAlpha.200", "blackAlpha.300");
return (
{!!label && {label}}
@@ -30,27 +34,28 @@ const InputBase: ForwardRefRenderFunction = (
) : (
-
-
+
+
{InputLeftAddonText}
diff --git a/src/shared/components/Form/Select.tsx b/src/shared/components/Form/Select.tsx
new file mode 100644
index 0000000..fd0cb57
--- /dev/null
+++ b/src/shared/components/Form/Select.tsx
@@ -0,0 +1,55 @@
+import { forwardRef, ForwardRefRenderFunction, ReactNode } from "react";
+import { FieldError } from "react-hook-form";
+
+import {
+ FormControl,
+ FormErrorMessage,
+ FormLabel,
+ Select as ChakraSelect,
+ SelectProps as ChakraSelectProps,
+ useColorModeValue
+} from "@chakra-ui/react";
+
+interface SelectProps extends ChakraSelectProps {
+ label?: string;
+ error?: FieldError;
+ children: ReactNode;
+ placeholder?: string;
+}
+
+const SelectBase: ForwardRefRenderFunction = (
+ { name, label, error = null, children, placeholder, ...rest },
+ ref
+) => {
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.900");
+ const textColor = useColorModeValue("gray.800", "gray.100");
+ const bgHoverColor = useColorModeValue("blackAlpha.200", "blackAlpha.300");
+ return (
+
+ {!!label && {label}}
+
+ {children}
+
+
+ {!!error && (
+
+ {error?.message}
+
+ )}
+
+ );
+};
+
+export const Select = forwardRef(SelectBase);
diff --git a/src/shared/components/Form/Textarea.tsx b/src/shared/components/Form/Textarea.tsx
old mode 100644
new mode 100755
index 8740344..1a99d53
--- a/src/shared/components/Form/Textarea.tsx
+++ b/src/shared/components/Form/Textarea.tsx
@@ -1,11 +1,13 @@
-import { FieldError } from "react-hook-form";
import { forwardRef, ForwardRefRenderFunction } from "react";
+import { FieldError } from "react-hook-form";
+
import {
- Textarea as ChakraTextarea,
- FormLabel,
FormControl,
+ FormErrorMessage,
+ FormLabel,
+ Textarea as ChakraTextarea,
TextareaProps as ChakraTextareaProps,
- FormErrorMessage
+ useColorModeValue
} from "@chakra-ui/react";
interface TextareaProps extends ChakraTextareaProps {
@@ -17,17 +19,21 @@ const TextareaBase: ForwardRefRenderFunction
{ name, label, error = null, ...rest },
ref
) => {
+ const bgColor = useColorModeValue("blackAlpha.100", "gray.900");
+ const bgHoverColor = useColorModeValue("blackAlpha.200", "blackAlpha.300");
+ const textColor = useColorModeValue("gray.800", "gray.100");
return (
{!!label && {label}}
diff --git a/src/shared/components/Layout/index.tsx b/src/shared/components/Layout/index.tsx
old mode 100644
new mode 100755
index ac431bb..4abd7d7
--- a/src/shared/components/Layout/index.tsx
+++ b/src/shared/components/Layout/index.tsx
@@ -1,4 +1,6 @@
import { Flex } from "@chakra-ui/react";
+
+import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
import { Sidebar } from "@/components/Sidebar";
@@ -8,12 +10,15 @@ type LayoutProps = {
export function Layout({ children }: LayoutProps) {
return (
-
-
-
+ <>
+
- {children}
+
+
+ {children}
+
-
+
+ >
);
}
diff --git a/src/styles/modal.ts b/src/styles/modal.ts
new file mode 100644
index 0000000..4bc36bb
--- /dev/null
+++ b/src/styles/modal.ts
@@ -0,0 +1,13 @@
+const parts = ["overlay", "dialogContainer", "dialog", "header", "closeButton", "body", "footer"];
+
+const modal = {
+ parts,
+ baseStyle: {
+ // dialog: {},
+ body: {
+ padding: 0
+ }
+ }
+};
+
+export default modal;
diff --git a/src/styles/theme.ts b/src/styles/theme.ts
old mode 100644
new mode 100755
index b88e5d3..e0965a2
--- a/src/styles/theme.ts
+++ b/src/styles/theme.ts
@@ -1,6 +1,17 @@
-import { extendTheme } from "@chakra-ui/react";
+import { extendTheme, type ThemeConfig } from "@chakra-ui/react";
+
+import { modalTheme } from "@/lib/theme";
+
+const config: ThemeConfig = {
+ initialColorMode: "dark",
+ useSystemColorMode: false
+};
export const theme = extendTheme({
+ config,
+ components: {
+ Modal: modalTheme
+ },
colors: {
gray: {
"900": "#262829",
@@ -15,14 +26,14 @@ export const theme = extendTheme({
},
fonts: {
heading: "var(--font-raleway)",
- body: "var(--font-raleway)"
+ body: "var(--font-roboto)"
},
styles: {
- global: {
+ global: (props: { colorMode: string }) => ({
body: {
- bg: "gray.900",
- color: "gray.50"
+ bg: props.colorMode === "light" ? "gray.50" : "gray.900",
+ color: props.colorMode === "light" ? "gray.900" : "gray.50"
}
- }
+ })
}
});
diff --git a/src/types/UserBasicsWithPagination.type.ts b/src/types/UserBasicsWithPagination.type.ts
new file mode 100644
index 0000000..833b80e
--- /dev/null
+++ b/src/types/UserBasicsWithPagination.type.ts
@@ -0,0 +1,37 @@
+export type UserBasicsWithPagination = {
+ data: {
+ id: string;
+ name: string;
+ email: string;
+ username: string;
+ category: {
+ id: string;
+ name: string;
+ value?: string;
+ };
+ about?: string;
+ website_url?: string;
+ instagram_url?: string;
+ avatar: {
+ url?: string;
+ formats: {
+ thumbnail: {
+ url?: string;
+ };
+ };
+ };
+ address: {
+ country?: string;
+ uf?: string;
+ city?: string;
+ };
+ }[];
+ meta: {
+ pagination: {
+ page: number;
+ pageSize: number;
+ pageCount: number;
+ total: number;
+ };
+ };
+};
diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts
old mode 100644
new mode 100755
index 674ed60..eadffb1
--- a/src/types/next-auth.d.ts
+++ b/src/types/next-auth.d.ts
@@ -1,4 +1,4 @@
-import NextAuth, { DefaultSession } from "next-auth";
+import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
diff --git a/src/types/stories.ts b/src/types/stories.ts
new file mode 100644
index 0000000..107dad0
--- /dev/null
+++ b/src/types/stories.ts
@@ -0,0 +1,37 @@
+type StoryAttributes = {
+ url: string;
+ duration: number;
+ see_more_enabled: boolean;
+ see_more_text: string | null;
+ see_more_link: string | null;
+ createdAt: string;
+ updatedAt: string;
+ author: {
+ data: {
+ id: number;
+ attributes: {
+ username: string;
+ };
+ };
+ };
+};
+
+type StoryItem = {
+ id: number;
+ attributes: StoryAttributes;
+};
+
+type StoriesResponseMeta = {
+ pagination?: {
+ page: number;
+ pageSize: number;
+ pageCount: number;
+ total: number;
+ };
+ [key: string]: unknown;
+};
+
+export type StoriesResponse = {
+ data: StoryItem[];
+ meta: StoriesResponseMeta;
+};
diff --git a/src/types/usersBasics.type.ts b/src/types/usersBasics.type.ts
new file mode 100644
index 0000000..a30b26e
--- /dev/null
+++ b/src/types/usersBasics.type.ts
@@ -0,0 +1,27 @@
+export type UserBasics = {
+ id: string;
+ name: string;
+ email: string;
+ username: string;
+ category: {
+ id: string;
+ name: string;
+ value?: string;
+ };
+ about?: string;
+ website_url?: string;
+ instagram_url?: string;
+ avatar: {
+ url?: string;
+ formats: {
+ thumbnail: {
+ url?: string;
+ };
+ };
+ };
+ address: {
+ country?: string;
+ uf?: string;
+ city?: string;
+ };
+};
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
old mode 100644
new mode 100755
index 6dd6e47..fc9a4f7
--- a/src/utils/auth.ts
+++ b/src/utils/auth.ts
@@ -1,4 +1,5 @@
import { GetServerSidePropsContext } from "next";
+
import { parseCookies } from "nookies";
export const redirectIfAuthenticated = (ctx: GetServerSidePropsContext) => {
@@ -7,7 +8,7 @@ export const redirectIfAuthenticated = (ctx: GetServerSidePropsContext) => {
if (token) {
return {
redirect: {
- destination: "/dashboard",
+ destination: "/",
permanent: false
}
};
diff --git a/src/utils/constant.ts b/src/utils/constant.ts
old mode 100644
new mode 100755
diff --git a/src/utils/socialMedia.ts b/src/utils/socialMedia.ts
new file mode 100644
index 0000000..f070335
--- /dev/null
+++ b/src/utils/socialMedia.ts
@@ -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");
+}
diff --git a/tsconfig.json b/tsconfig.json
old mode 100644
new mode 100755
index e59724b..7bacdc1
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
- "lib": ["dom", "dom.iterable", "esnext"],
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -19,9 +23,18 @@
}
],
"paths": {
- "@/*": ["./src/*"]
+ "@/*": [
+ "./src/*"
+ ]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
}