diff --git a/crazystacknextjs/next.config.ts b/crazystacknextjs/next.config.ts
index a9194aa..eb74c8b 100644
--- a/crazystacknextjs/next.config.ts
+++ b/crazystacknextjs/next.config.ts
@@ -7,6 +7,11 @@ const nextConfig: NextConfig = {
protocol: "https",
hostname: "images.unsplash.com",
},
+ {
+ protocol: "https",
+ hostname:
+ "crazystack.571527a8590aad1eb25dc5b3338c4271.r2.cloudflarestorage.com",
+ },
],
},
};
diff --git a/crazystacknextjs/package.json b/crazystacknextjs/package.json
index cec7b6a..bfc07d9 100644
--- a/crazystacknextjs/package.json
+++ b/crazystacknextjs/package.json
@@ -9,6 +9,8 @@
"lint": "next lint"
},
"dependencies": {
+ "@emoji-mart/data": "^1.2.1",
+ "@emoji-mart/react": "^1.1.1",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
@@ -26,16 +28,21 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
+ "@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.3",
+ "@react-input/mask": "^1.2.15",
"@tanstack/react-query": "^5.59.16",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
+ "emoji-mart": "^5.6.0",
+ "framer-motion": "^12.0.11",
"i18next": "^23.16.4",
"i18next-chained-backend": "^4.6.2",
"i18next-http-backend": "^2.6.2",
"i18next-resources-to-backend": "^1.2.1",
+ "libphonenumber-js": "^1.11.15",
"lucide-react": "^0.454.0",
"next": "15.0.2",
"next-themes": "^0.3.0",
diff --git a/crazystacknextjs/public/banner-01.png b/crazystacknextjs/public/banner-01.png
deleted file mode 100644
index afbbaa8..0000000
Binary files a/crazystacknextjs/public/banner-01.png and /dev/null differ
diff --git a/crazystacknextjs/src/app/login/_components/side-div.tsx b/crazystacknextjs/src/app/(auth)/_components/side-div.tsx
similarity index 100%
rename from crazystacknextjs/src/app/login/_components/side-div.tsx
rename to crazystacknextjs/src/app/(auth)/_components/side-div.tsx
diff --git a/crazystacknextjs/src/app/(auth)/_components/terms-and-conditions.tsx b/crazystacknextjs/src/app/(auth)/_components/terms-and-conditions.tsx
new file mode 100644
index 0000000..a0436e6
--- /dev/null
+++ b/crazystacknextjs/src/app/(auth)/_components/terms-and-conditions.tsx
@@ -0,0 +1,22 @@
+import Link from "next/link";
+
+export const TermsAndConditions = () => {
+ return (
+
+ Clicando em continuar, você concorda com nossos{" "}
+
+ Termos de serviço
+ {" "}
+ e{" "}
+
+ Política de privacidade
+
+
+ );
+};
diff --git a/crazystacknextjs/src/app/login/_components/signin-div.tsx b/crazystacknextjs/src/app/(auth)/login/_components/signin-div.tsx
similarity index 65%
rename from crazystacknextjs/src/app/login/_components/signin-div.tsx
rename to crazystacknextjs/src/app/(auth)/login/_components/signin-div.tsx
index ea6f90c..56ba575 100644
--- a/crazystacknextjs/src/app/login/_components/signin-div.tsx
+++ b/crazystacknextjs/src/app/(auth)/login/_components/signin-div.tsx
@@ -1,5 +1,6 @@
import { LoginForm } from "@/slices/general/features/login";
import Link from "next/link";
+import { TermsAndConditions } from "../../_components/terms-and-conditions";
export const SignInDiv = () => {
return (
@@ -18,22 +19,7 @@ export const SignInDiv = () => {
-
- Clicando em continuar, você concorda com nossos{" "}
-
- Termos de serviço
- {" "}
- e{" "}
-
- Política de privacidade
-
-
+
Ainda não tem uma conta?{" "}
{
+ return (
+
+
+
+
+ Criar conta
+
+
+ Digite seu email e senha para criar sua conta
+
+
+
+
+
+
+ Já tem uma conta?{" "}
+
+ Entre aqui
+
+
+
+ );
+};
diff --git a/crazystacknextjs/src/app/(auth)/signup/page.tsx b/crazystacknextjs/src/app/(auth)/signup/page.tsx
new file mode 100644
index 0000000..51df895
--- /dev/null
+++ b/crazystacknextjs/src/app/(auth)/signup/page.tsx
@@ -0,0 +1,21 @@
+import type { Metadata } from "next";
+import { whitelabel } from "@/application/whitelabel";
+import { SignUpDiv } from "./_components/signup-div";
+import { SideDiv } from "../_components/side-div";
+
+export const metadata: Metadata = {
+ title: `${whitelabel.systemName} | Entrar`,
+ description: `Página de login do ${whitelabel.systemName}. Faça login para acessar o sistema.`,
+};
+export default function Login() {
+ return (
+
+
+
+
+ );
+}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/emoji-picker.tsx b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/emoji-picker.tsx
new file mode 100644
index 0000000..4114e6a
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/emoji-picker.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import data from "@emoji-mart/data";
+import Picker from "@emoji-mart/react";
+
+interface EmojiPickerProps {
+ onEmojiSelect: (emoji: string) => void;
+}
+
+const EmojiPicker: React.FC = ({ onEmojiSelect }) => {
+ return (
+ onEmojiSelect(emoji.native)}
+ theme="light"
+ showPreview={false}
+ showSkinTones={false}
+ emojiSize={20}
+ emojiButtonSize={28}
+ maxFrequentRows={4}
+ />
+ );
+};
+
+export default EmojiPicker;
diff --git a/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-card.tsx b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-card.tsx
new file mode 100644
index 0000000..4f0becf
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-card.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import { useState, useCallback, useEffect } from "react";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+} from "@/components/ui/card";
+import { Heart, MessageCircle, Repeat2 } from "lucide-react";
+import { TweetForm } from "./tweet-form";
+import { toggleLike } from "@/slices/belezix/entidades/tweet/tweet.api";
+import { useAuth } from "@/shared/libs/contexts/AuthContext";
+import { parseCookies } from "nookies";
+import { useToast } from "@/hooks/use-toast";
+
+export const TweetCard = ({
+ tweet,
+ canReply = false,
+ handleChangeCanReply,
+ ref,
+}: any) => {
+ const { user } = useAuth();
+ const { toast } = useToast();
+ const isReply = !!tweet?.tweetId;
+ const [likeCount, setLikeCount] = useState(
+ Number(tweet?.tweetlike?.total ?? 0),
+ );
+ const [isLiked, setIsLiked] = useState(
+ tweet?.tweetlike?.find?.((item: any) => item?.createdById === user?._id),
+ );
+ const [isLikeLoading, setIsLikeLoading] = useState(false);
+
+ useEffect(() => {
+ setLikeCount(Number(tweet?.tweetlike?.total ?? 0));
+ setIsLiked(
+ tweet?.tweetlike?.find?.((item: any) => item?.createdById === user?._id),
+ );
+ }, [tweet, user]);
+
+ const handleLikeClick = useCallback(async () => {
+ if (!user) {
+ toast({
+ title: "Authentication required",
+ description: "Please log in to like tweets.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ setIsLikeLoading(true);
+ try {
+ await toggleLike({
+ tweetlike: { tweetId: tweet?._id, userSlug: user?.slug },
+ cookies: parseCookies(),
+ });
+ setLikeCount((prev: any) => (isLiked ? prev - 1 : prev + 1));
+ setIsLiked(!isLiked);
+ } catch (error) {
+ console.error("Error toggling like:", error);
+ toast({
+ title: "Error",
+ description: "Failed to update like status. Please try again.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLikeLoading(false);
+ }
+ }, [isLiked, tweet?._id, user, toast]);
+
+ return (
+
+
+
+
+ {tweet?.createdBy?.slug?.charAt?.(0)}
+
+
+
+
+
@{tweet?.createdBy?.slug}
+
{tweet?.createdBy?.name}
+
+
{tweet?.distanceNow}
+
+
+
+
+
+ {tweet?.body}
+
+ {tweet?.image && (
+
+ )}
+
+
+
+ {
+ if (!user) {
+ toast({
+ title: "Authentication required",
+ description: "Please log in to reply tweets.",
+ variant: "destructive",
+ });
+ return;
+ }
+ handleChangeCanReply({ tweet });
+ }}
+ >
+
+ {tweet?.replies}
+
+ {}}
+ >
+
+ {tweet?.retweets}
+
+
+
+ {likeCount}
+
+
+
+ {canReply && (
+
+
+
+ )}
+
+ );
+};
diff --git a/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-form.tsx b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-form.tsx
new file mode 100644
index 0000000..4b4686c
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-form.tsx
@@ -0,0 +1,266 @@
+"use client";
+
+import { useState, useRef, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Loader2, ImageIcon, Smile, X } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+import { parseCookies } from "nookies";
+import { uploadPhoto } from "@/slices/belezix/entidades/photo/photo.api";
+import { addTweet } from "@/slices/belezix/entidades/tweet/tweet.api";
+import { useAuth } from "@/shared/libs/contexts/AuthContext";
+import EmojiPicker from "./emoji-picker";
+import { toast } from "sonner";
+
+const MAX_TWEET_LENGTH = 280;
+
+export function TweetFormContainer({ tweetId }: { tweetId?: string }) {
+ return (
+
+
+
+ );
+}
+export function TweetForm({ tweetId }: { tweetId?: string }) {
+ const { user } = useAuth();
+ const [tweet, setTweet] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [image, setImage] = useState(null);
+ const fileInputRef = useRef(null);
+ const [formDataImage, setFormDataImage] = useState(null);
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
+ const emojiPickerRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ emojiPickerRef.current &&
+ !emojiPickerRef.current.contains(event.target as Node)
+ ) {
+ setShowEmojiPicker(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, []);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ setError("");
+ try {
+ const cookies = parseCookies();
+ try {
+ let photo;
+ if (formDataImage) {
+ photo = await uploadPhoto({ formDataImage, cookies });
+ }
+ const response = await addTweet({
+ tweet: {
+ userSlug: user?.name,
+ body: tweet,
+ image: photo?._id,
+ tweetId,
+ },
+ cookies,
+ });
+ if (response?._id) {
+ toast.success(
+ tweetId
+ ? "Resposta enviada com sucesso!"
+ : "Post enviado com sucesso!",
+ );
+ }
+ } catch (err) {}
+ setTweet("");
+ setImage(null);
+ } catch (err) {
+ setError("Failed to submit tweet. Please try again.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleImageUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ const formData = new FormData();
+
+ if (file) {
+ formData.append("file", file);
+ setFormDataImage(formData);
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setImage(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const removeImage = () => {
+ setImage(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ };
+
+ const handleEmojiSelect = (emoji: string) => {
+ setTweet(tweet + emoji);
+ setShowEmojiPicker(false);
+ };
+
+ const remainingChars = MAX_TWEET_LENGTH - tweet.length;
+ const isOverLimit = remainingChars < 0;
+
+ return (
+
+ );
+}
+export const ImageTweet = ({ image, removeImage }: any) => {
+ return (
+ <>
+ {image && (
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-list.tsx b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-list.tsx
new file mode 100644
index 0000000..bc833d4
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/(home)/_components/molecules/tweet-list.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { useState, useEffect, useRef, useCallback } from "react";
+import { TweetCard } from "./tweet-card";
+import type { TweetProps } from "@/slices/belezix/entidades/tweet/tweet.model";
+import { getTweets } from "@/slices/belezix/entidades/tweet/tweet.api";
+import { parseCookies } from "nookies";
+import { motion, AnimatePresence } from "framer-motion";
+import { Loader2 } from "lucide-react";
+
+export function TweetList({
+ initialTweets,
+ countTweets,
+}: {
+ initialTweets: any[];
+ countTweets: number;
+}) {
+ const [tweets, setTweets] = useState(initialTweets);
+ const [canReply, setCanReply] = useState(null);
+ const [page, setPage] = useState(1);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const observer = useRef(null);
+
+ const lastTweetElementRef = useCallback(
+ (node: HTMLDivElement | null) => {
+ if (loading) return;
+ if (observer.current) observer.current.disconnect();
+ observer.current = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && tweets.length < countTweets) {
+ setPage((prevPage) => prevPage + 1);
+ }
+ });
+ if (node) observer.current.observe(node);
+ },
+ [loading, tweets.length, countTweets],
+ );
+
+ const handleChangeCanReply = ({ tweet }: { tweet: TweetProps }) => {
+ if (canReply?._id === tweet?._id) {
+ setCanReply(null);
+ return;
+ }
+ setCanReply(tweet);
+ };
+
+ useEffect(() => {
+ const loadMoreTweets = async () => {
+ if (loading || tweets.length >= countTweets) return;
+ setLoading(true);
+ setError(null);
+ try {
+ const cookies = parseCookies();
+ const { tweets: newTweets } = await getTweets(page, cookies, {
+ sortBy: "createdAt",
+ typeSort: "desc",
+ tweetId: "null",
+ });
+
+ setTweets((prevTweets) => [
+ ...new Set(prevTweets?.concat?.(newTweets)),
+ ]);
+ } catch (error) {
+ console.error("Error loading more tweets:", error);
+ setError("Failed to load tweets. Please try again.");
+ } finally {
+ setLoading(false);
+ }
+ };
+ if (page > 1) {
+ loadMoreTweets();
+ }
+ }, [page, loading, tweets.length, countTweets]);
+
+ return (
+
+ {tweets.map((tweet: TweetProps, index: number) => (
+
+ ))}
+ {loading && (
+
+
+ Loading more tweets...
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
+ {!loading && tweets.length >= countTweets && (
+
+ No more tweets to load.
+
+ )}
+
+ );
+}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/(home)/page.tsx b/crazystacknextjs/src/app/(pageswithheader)/(home)/page.tsx
index 75b3070..bd2dcfe 100644
--- a/crazystacknextjs/src/app/(pageswithheader)/(home)/page.tsx
+++ b/crazystacknextjs/src/app/(pageswithheader)/(home)/page.tsx
@@ -1,12 +1,15 @@
import { SubHeader } from "./_components/organisms/subheader";
import type { Metadata } from "next";
import { whitelabel } from "@/application/whitelabel";
-import { getCookies, parseCookies } from "@/shared/libs/utils";
+import { parseCookies } from "@/shared/libs/utils";
import { getOwnersPublic } from "@/slices/belezix/entidades/owner/owner.api";
import { getRequests } from "@/slices/belezix/entidades/request/request.api";
import { startOfDay } from "date-fns";
import { HorizontalList } from "../_components/templates/horizontal-list";
-
+import { TweetFormContainer } from "./_components/molecules/tweet-form";
+import { TweetList } from "./_components/molecules/tweet-list";
+import { getTweets } from "@/slices/belezix/entidades/tweet/tweet.api";
+import { getCookies } from "@/shared/libs/utils/cookies";
export const metadata: Metadata = {
title: `${whitelabel.systemName} | Agendamentos Online`,
description: `Página de inicial do ${whitelabel.systemName}. Aqui você pode agendar com os melhores estabelecimentos da cidade.`,
@@ -37,6 +40,18 @@ async function handleRequests(cookies: any) {
return { requests: [], totalCount: 0 };
}
}
+async function handleTweets(cookies: any) {
+ try {
+ const { tweets = [], totalCount = 0 } = await getTweets(1, cookies, {
+ sortBy: "createdAt",
+ typeSort: "desc",
+ tweetId: "null",
+ });
+ return { tweets, totalCount };
+ } catch (error: any) {
+ return { tweets: [], totalCount: 0 };
+ }
+}
export default async function Page() {
const cookies = await getParsedCookies();
const popularOwners = await getOwnersPublic(1, cookies, {
@@ -47,36 +62,57 @@ export default async function Page() {
sortBy: "createdAt",
typeSort: "desc",
});
+
const owners = Array.isArray(popularOwners?.owners)
? popularOwners?.owners
: [];
const newOwners = Array.isArray(lastOwners?.owners) ? lastOwners?.owners : [];
const { requests, totalCount } = await handleRequests(cookies);
+ const { tweets, totalCount: countTweets } = await handleTweets(cookies);
return (
-
+ <>
+
+
+
+
+
Feed
+
+ Veja o que as pessoas estão falando sobre o Belezix e participe
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/(home)/tweets/page.tsx b/crazystacknextjs/src/app/(pageswithheader)/(home)/tweets/page.tsx
new file mode 100644
index 0000000..31028fa
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/(home)/tweets/page.tsx
@@ -0,0 +1,64 @@
+import type { Metadata } from "next";
+import { whitelabel } from "@/application/whitelabel";
+import { parseCookies } from "@/shared/libs/utils";
+import { getCookies } from "@/shared/libs/utils/cookies";
+
+import { getTweets } from "@/slices/belezix/entidades/tweet/tweet.api";
+import { TweetFormContainer } from "../_components/molecules/tweet-form";
+import { TweetList } from "../_components/molecules/tweet-list";
+
+export const metadata: Metadata = {
+ title: `${whitelabel.systemName} | Agendamentos Online`,
+ description: `Página de inicial do ${whitelabel.systemName}. Aqui você pode agendar com os melhores estabelecimentos da cidade.`,
+};
+
+async function getParsedCookies() {
+ const cookies = await getCookies();
+ if (!cookies) {
+ return null;
+ }
+ const parsedCookies = parseCookies(cookies);
+ if (!parsedCookies?.["belezixclient.token"]) {
+ return null;
+ }
+ return parsedCookies;
+}
+
+async function handleTweets(cookies: any) {
+ try {
+ const { tweets, totalCount = 0 } = await getTweets(1, cookies, {
+ sortBy: "createdAt",
+ typeSort: "desc",
+ tweetId: "null",
+ });
+ return { tweets, totalCount };
+ } catch (error: any) {
+ return { tweets: [], totalCount: 0 };
+ }
+}
+
+export default async function Page() {
+ const cookies = await getParsedCookies();
+
+ const { tweets, totalCount: countTweets } = await handleTweets(cookies);
+
+ return (
+ <>
+
+
+
+
Feed
+
+ Veja o que as pessoas estão falando sobre o Belezix e participe
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/_components/molecules/sidebar-sheet.tsx b/crazystacknextjs/src/app/(pageswithheader)/_components/molecules/sidebar-sheet.tsx
index 8679465..b024981 100644
--- a/crazystacknextjs/src/app/(pageswithheader)/_components/molecules/sidebar-sheet.tsx
+++ b/crazystacknextjs/src/app/(pageswithheader)/_components/molecules/sidebar-sheet.tsx
@@ -1,5 +1,4 @@
"use client";
-import { SignInDiv } from "@/app/login/_components/signin-div";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
@@ -24,6 +23,7 @@ import {
import Link from "next/link";
import { useRouter } from "next/navigation";
import Image from "next/image";
+import { SignInDiv } from "@/app/(auth)/login/_components/signin-div";
type Props = {
quickSearchOptions: any[];
diff --git a/crazystacknextjs/src/app/(pageswithheader)/_components/templates/horizontal-list.tsx b/crazystacknextjs/src/app/(pageswithheader)/_components/templates/horizontal-list.tsx
index aa84ee4..3544c8f 100644
--- a/crazystacknextjs/src/app/(pageswithheader)/_components/templates/horizontal-list.tsx
+++ b/crazystacknextjs/src/app/(pageswithheader)/_components/templates/horizontal-list.tsx
@@ -22,7 +22,7 @@ export const HorizontalList = ({
return (
<>
{array.length > 0 && (
-
+
{title}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/appointments/page.tsx b/crazystacknextjs/src/app/(pageswithheader)/appointments/page.tsx
index 5d91f93..ec86b1c 100644
--- a/crazystacknextjs/src/app/(pageswithheader)/appointments/page.tsx
+++ b/crazystacknextjs/src/app/(pageswithheader)/appointments/page.tsx
@@ -1,9 +1,10 @@
import type { Metadata } from "next";
import { whitelabel } from "@/application/whitelabel";
-import { getCookies, parseCookies } from "@/shared/libs/utils";
+import { parseCookies } from "@/shared/libs/utils";
import { getRequests } from "@/slices/belezix/entidades/request/request.api";
import { startOfDay } from "date-fns";
import AppointmentsList from "./AppointmentsList";
+import { getCookies } from "@/shared/libs/utils/cookies";
export const metadata: Metadata = {
title: `${whitelabel.systemName} | Meus Agendamentos`,
@@ -53,13 +54,15 @@ export default async function Page() {
cookies,
} = appointments;
return (
-
+
+
+
);
}
async function getParsedCookies() {
diff --git a/crazystacknextjs/src/app/(pageswithheader)/forgot-password/page.tsx b/crazystacknextjs/src/app/(pageswithheader)/forgot-password/page.tsx
new file mode 100644
index 0000000..fd4044d
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/forgot-password/page.tsx
@@ -0,0 +1,167 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Loader2, Mail, ArrowLeft } from "lucide-react";
+
+const formSchema = z.object({
+ email: z.string().email({
+ message: "Please enter a valid email address.",
+ }),
+});
+
+export default function ForgotPasswordPage() {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState
(null);
+ const [success, setSuccess] = useState(false);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ },
+ });
+
+ async function onSubmit(values: z.infer) {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const response = await fetch("/api/auth/forgot-password", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(values),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || "Something went wrong");
+ }
+
+ setSuccess(true);
+ form.reset();
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : "An unexpected error occurred",
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+
+
+
+ Reset your password
+
+ Enter your email address and we will send you a link to reset your
+ password
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {success ? (
+
+
+
+ Check your email for a reset link. It will expire in 1 hour.
+
+
+
+
+
+
+ If you don t see the email in your inbox, check your spam
+ folder.
+
+
+
+
setSuccess(false)}
+ >
+ Try another email
+
+
+ ) : (
+
+
+ (
+
+ Email
+
+
+
+
+
+ )}
+ />
+
+ {isLoading ? (
+ <>
+
+ Sending reset link...
+ >
+ ) : (
+ "Send reset link"
+ )}
+
+
+
+ )}
+
+
+
+
+ Back to sign in
+
+
+
+
+ );
+}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/owners/[id]/page.tsx b/crazystacknextjs/src/app/(pageswithheader)/owners/[id]/page.tsx
index 067f2fe..be1f337 100644
--- a/crazystacknextjs/src/app/(pageswithheader)/owners/[id]/page.tsx
+++ b/crazystacknextjs/src/app/(pageswithheader)/owners/[id]/page.tsx
@@ -4,16 +4,19 @@ import Link from "next/link";
import type { Metadata } from "next";
import { ChevronLeftIcon, MapPinIcon, StarIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { PhoneItem } from "@/shared/ui/molecules";
import { getOwnerByIdPublic } from "@/slices/belezix/entidades/owner/owner.api";
import { whitelabel } from "@/application/whitelabel";
-import { getCookies, parseCookies } from "@/shared/libs/utils";
+import { parseCookies } from "@/shared/libs/utils";
import { AppointmentService } from "../_components/appointment-service";
+import { getCookies } from "@/shared/libs/utils/cookies";
export const metadata: Metadata = {
title: `${whitelabel.systemName} | Detalhes do Estabelecimento`,
description: `Página de detalhes de estabelecimentos do ${whitelabel.systemName}. Aqui você pode agendar com os melhores estabelecimentos da cidade.`,
};
+
async function getParsedCookies() {
const cookies = await getCookies();
if (!cookies) {
@@ -25,6 +28,7 @@ async function getParsedCookies() {
}
return parsedCookies;
}
+
export default async function Page({
params,
}: {
@@ -40,13 +44,15 @@ export default async function Page({
if (!owner) {
return null;
}
+
return (
+ //
@@ -62,12 +68,18 @@ export default async function Page({
+
+
+
+ {owner.name.charAt(0)}
+
+
-
+
{owner.name}
-
{owner?.address ?? "Sem endereço"}
+
{owner?.place?.address ?? "Sem endereço"}
@@ -76,11 +88,18 @@ export default async function Page({
Sobre nós
-
{owner?.description ?? "Não tem descrição"}
+
+ {owner?.description ??
+ owner?.place?.description ??
+ "Não tem descrição"}
+
+
+
+ {owner?.place?.phone &&
}
Serviços
-
+
{owner?.services?.map?.((service: any, index: number) => (
- {owner?.phones?.length > 0 && (
-
- {owner?.phones?.map?.((phone: any) => (
-
- ))}
-
- )}
);
}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/owners/_components/appointment-service.tsx b/crazystacknextjs/src/app/(pageswithheader)/owners/_components/appointment-service.tsx
index 483fa84..c266b4e 100644
--- a/crazystacknextjs/src/app/(pageswithheader)/owners/_components/appointment-service.tsx
+++ b/crazystacknextjs/src/app/(pageswithheader)/owners/_components/appointment-service.tsx
@@ -1,18 +1,9 @@
"use client";
-import { SignInDiv } from "@/app/login/_components/signin-div";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
+import { SignInDiv } from "@/app/(auth)/login/_components/signin-div";
+import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { useAuth } from "@/shared/libs/contexts/AuthContext";
import { AppointmentDialog } from "@/slices/belezix/entidades/appointment/ui/organisms";
import { ServiceItem } from "@/slices/belezix/entidades/service/ui";
-import { LogInIcon } from "lucide-react";
import { useState } from "react";
interface AppointmentServiceProps {
service: any;
diff --git a/crazystacknextjs/src/app/(pageswithheader)/owners/page.tsx b/crazystacknextjs/src/app/(pageswithheader)/owners/page.tsx
index 4760079..f5b0a99 100644
--- a/crazystacknextjs/src/app/(pageswithheader)/owners/page.tsx
+++ b/crazystacknextjs/src/app/(pageswithheader)/owners/page.tsx
@@ -1,9 +1,10 @@
import { Search } from "../_components/molecules/search";
-import { getCookies, parseCookies } from "@/shared/libs/utils";
+import { parseCookies } from "@/shared/libs/utils";
import { getOwnersPublic } from "@/slices/belezix/entidades/owner/owner.api";
import type { Metadata } from "next";
import { whitelabel } from "@/application/whitelabel";
import { OwnerItem } from "@/slices/belezix/entidades/owner/ui/owner-item";
+import { getCookies } from "@/shared/libs/utils/cookies";
export const metadata: Metadata = {
title: `${whitelabel.systemName} | Estabelecimentos`,
@@ -29,6 +30,7 @@ async function handleOwners(filter: any) {
cookies,
filter,
);
+
return { owners, totalCount };
} catch (error) {
return null;
diff --git a/crazystacknextjs/src/app/(pageswithheader)/verify-email/_components/FallbackEmailVerified.tsx b/crazystacknextjs/src/app/(pageswithheader)/verify-email/_components/FallbackEmailVerified.tsx
new file mode 100644
index 0000000..c3cde40
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/verify-email/_components/FallbackEmailVerified.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Loader2, Mail } from "lucide-react";
+import Link from "next/link";
+import { api } from "@/shared/api";
+export function FallbackEmailVerified({ email = "" }) {
+ const [isResending, setIsResending] = useState(false);
+ const [error, setError] = useState
(null);
+ const [resendSuccess, setResendSuccess] = useState(false);
+
+ async function resendVerificationEmail() {
+ setIsResending(true);
+ setError(null);
+ setResendSuccess(false);
+
+ try {
+ if (!api) {
+ throw new Error("API instance is not available");
+ }
+ const response = await api.post("/auth/resend-email", { email });
+ setResendSuccess(!!response?.data);
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : "An unexpected error occurred",
+ );
+ } finally {
+ setIsResending(false);
+ }
+ }
+
+ return (
+
+
+
+ Check your email
+
+ We ve sent a verification link to {email || "your email address"}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {resendSuccess && (
+
+
+ Verification email sent successfully!
+
+
+ )}
+
+
+
+
+ Click the link in the email to verify your account. If you see it,
+ check your spam folder.
+
+
+
+
+
+ {isResending ? (
+ <>
+
+ Resending...
+ >
+ ) : (
+ "Resend verification email"
+ )}
+
+
+ Already verified?{" "}
+
+ Sign in
+
+
+
+
+
+ );
+}
diff --git a/crazystacknextjs/src/app/(pageswithheader)/verify-email/page.tsx b/crazystacknextjs/src/app/(pageswithheader)/verify-email/page.tsx
new file mode 100644
index 0000000..5760290
--- /dev/null
+++ b/crazystacknextjs/src/app/(pageswithheader)/verify-email/page.tsx
@@ -0,0 +1,93 @@
+import { whitelabel } from "@/application/whitelabel";
+import { parseCookies } from "@/shared/libs/utils";
+import type { Metadata } from "next";
+import { FallbackEmailVerified } from "./_components/FallbackEmailVerified";
+import { CheckCircle } from "lucide-react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { verifyEmail } from "@/shared/libs/contexts/verify-email";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import { getCookies } from "@/shared/libs/utils/cookies";
+
+export const metadata: Metadata = {
+ title: `${whitelabel.systemName} | Verificar email`,
+ description: `Página de verificar email do ${whitelabel.systemName}. Aqui você pode verificar seu email.`,
+};
+async function getParsedCookies() {
+ const cookies = await getCookies();
+ if (!cookies) {
+ return null;
+ }
+ const parsedCookies = parseCookies(cookies);
+ if (!parsedCookies?.["belezixclient.token"]) {
+ return null;
+ }
+ return parsedCookies;
+}
+
+export default async function Page({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const emailParam = (await searchParams).email;
+ const tokenParam = (await searchParams).token;
+ const email = Array.isArray(emailParam) ? emailParam?.[0] : emailParam;
+ const token = Array.isArray(tokenParam) ? tokenParam?.[0] : tokenParam;
+ const result = await handleEmailVerification({ email, token });
+ if (!result) return ;
+ console.log({ result });
+ return (
+
+
+
+ Email Verification
+ Were verifying your email address
+
+
+ {!result && (
+
+
+ An error occurred while verifying your email address
+
+
+ )}
+ {result ? (
+
+
+
+ Email verified successfully!
+
+
+ Continue to Home
+
+
+ ) : null}{" "}
+
+
+
+ );
+}
+async function handleEmailVerification({
+ email,
+ token,
+}: {
+ email: string | undefined;
+ token: string | undefined;
+}) {
+ try {
+ if (!email || !token) return null;
+ const result = await verifyEmail({ email, token });
+ return result;
+ } catch (error) {
+ console.log(error);
+ return null;
+ }
+}
diff --git a/crazystacknextjs/src/app/providers.tsx b/crazystacknextjs/src/app/providers.tsx
index 9a68c98..5ca7a1b 100644
--- a/crazystacknextjs/src/app/providers.tsx
+++ b/crazystacknextjs/src/app/providers.tsx
@@ -5,6 +5,7 @@ import { WebSocketProvider } from "@/application/providers/webSocketProvider";
import { AuthProvider } from "@/shared/libs/contexts/AuthContext";
import { UiProvider } from "@/shared/libs/contexts/UiContext";
import { Toaster } from "sonner";
+import { Toaster as ToasterShadcn } from "@/components/ui/toaster";
export type AllProviderProps = {
children: any;
@@ -27,6 +28,7 @@ export const AllProviders = ({ children }: AllProviderProps) => {
{children}
+
diff --git a/crazystacknextjs/src/components/ui/alert.tsx b/crazystacknextjs/src/components/ui/alert.tsx
new file mode 100644
index 0000000..5afd41d
--- /dev/null
+++ b/crazystacknextjs/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/crazystacknextjs/src/components/ui/toast.tsx b/crazystacknextjs/src/components/ui/toast.tsx
new file mode 100644
index 0000000..a10fd03
--- /dev/null
+++ b/crazystacknextjs/src/components/ui/toast.tsx
@@ -0,0 +1,128 @@
+"use client"
+
+import * as React from "react"
+import * as ToastPrimitives from "@radix-ui/react-toast"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "@/lib/utils"
+import { Cross2Icon } from "@radix-ui/react-icons"
+
+const ToastProvider = ToastPrimitives.Provider
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ )
+})
+Toast.displayName = ToastPrimitives.Root.displayName
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastAction.displayName = ToastPrimitives.Action.displayName
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ToastClose.displayName = ToastPrimitives.Close.displayName
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastTitle.displayName = ToastPrimitives.Title.displayName
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastDescription.displayName = ToastPrimitives.Description.displayName
+
+type ToastProps = React.ComponentPropsWithoutRef
+
+type ToastActionElement = React.ReactElement
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+}
diff --git a/crazystacknextjs/src/components/ui/toaster.tsx b/crazystacknextjs/src/components/ui/toaster.tsx
new file mode 100644
index 0000000..171beb4
--- /dev/null
+++ b/crazystacknextjs/src/components/ui/toaster.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import { useToast } from "@/hooks/use-toast"
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from "@/components/ui/toast"
+
+export function Toaster() {
+ const { toasts } = useToast()
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title} }
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/crazystacknextjs/src/hooks/use-toast.ts b/crazystacknextjs/src/hooks/use-toast.ts
new file mode 100644
index 0000000..02e111d
--- /dev/null
+++ b/crazystacknextjs/src/hooks/use-toast.ts
@@ -0,0 +1,194 @@
+"use client"
+
+// Inspired by react-hot-toast library
+import * as React from "react"
+
+import type {
+ ToastActionElement,
+ ToastProps,
+} from "@/components/ui/toast"
+
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
+
+type ToasterToast = ToastProps & {
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const
+
+let count = 0
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
+}
+
+type ActionType = typeof actionTypes
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+
+interface State {
+ toasts: ToasterToast[]
+}
+
+const toastTimeouts = new Map>()
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId)
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ })
+ }, TOAST_REMOVE_DELAY)
+
+ toastTimeouts.set(toastId, timeout)
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ }
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ }
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId)
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id)
+ })
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ }
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ }
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ }
+ }
+}
+
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
+
+function toast({ ...props }: Toast) {
+ const id = genId()
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss()
+ },
+ },
+ })
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ }
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState)
+
+ React.useEffect(() => {
+ listeners.push(setState)
+ return () => {
+ const index = listeners.indexOf(setState)
+ if (index > -1) {
+ listeners.splice(index, 1)
+ }
+ }
+ }, [state])
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ }
+}
+
+export { useToast, toast }
diff --git a/crazystacknextjs/src/shared/libs/contexts/AuthContext.tsx b/crazystacknextjs/src/shared/libs/contexts/AuthContext.tsx
index 0187950..15be414 100644
--- a/crazystacknextjs/src/shared/libs/contexts/AuthContext.tsx
+++ b/crazystacknextjs/src/shared/libs/contexts/AuthContext.tsx
@@ -28,6 +28,7 @@ type User = {
serviceIds?: string[];
daysSinceRegister?: number;
phone?: string;
+ slug?: string;
};
type AuthProviderProps = {
@@ -177,6 +178,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
cpf,
cnpj,
cnpjActive,
+ coord = { type: "Point", coordinates: [0, 0] },
}: SignupCredentials) => {
try {
setLoading(true);
@@ -190,10 +192,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
passwordConfirmation: password,
name,
phone,
- coord: {
- lat: 0,
- lng: 0,
- },
+ coord,
role: "client",
cpf: cnpjActive ? null : cpf,
cnpj: cnpjActive ? cnpj : null,
diff --git a/crazystacknextjs/src/shared/libs/contexts/verify-email.tsx b/crazystacknextjs/src/shared/libs/contexts/verify-email.tsx
new file mode 100644
index 0000000..f0b8b81
--- /dev/null
+++ b/crazystacknextjs/src/shared/libs/contexts/verify-email.tsx
@@ -0,0 +1,23 @@
+export const verifyEmail = async ({
+ email,
+ token,
+}: {
+ email: string;
+ token: string;
+}) => {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_API_URL}/auth/verify-email?email=${email}&code=${token}`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email, code: token }),
+ },
+ );
+ if (!response.ok) {
+ return;
+ }
+ const data = await response.json();
+ return data;
+};
diff --git a/crazystacknextjs/src/shared/libs/utils/geolocation.ts b/crazystacknextjs/src/shared/libs/utils/geolocation.ts
index 6b77e6b..27c183e 100644
--- a/crazystacknextjs/src/shared/libs/utils/geolocation.ts
+++ b/crazystacknextjs/src/shared/libs/utils/geolocation.ts
@@ -1,5 +1,5 @@
export function getCurrentPosition(
- options?: PositionOptions
+ options?: PositionOptions,
): Promise<{ lat: number; lng: number }> {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
@@ -9,7 +9,7 @@ export function getCurrentPosition(
lng: position.coords.longitude,
}),
(error) => reject(error),
- options
+ options,
);
});
}
diff --git a/crazystacknextjs/src/shared/libs/utils/index.ts b/crazystacknextjs/src/shared/libs/utils/index.ts
index 01ec06b..db0aa0d 100644
--- a/crazystacknextjs/src/shared/libs/utils/index.ts
+++ b/crazystacknextjs/src/shared/libs/utils/index.ts
@@ -1,4 +1,3 @@
export * from "./geolocation";
-export * from "./cookies";
export * from "./format-relative";
export * from "./parseCookies";
diff --git a/crazystacknextjs/src/shared/ui/atoms/form-field/form-field.tsx b/crazystacknextjs/src/shared/ui/atoms/form-field/form-field.tsx
index d26f70c..a1a96d7 100644
--- a/crazystacknextjs/src/shared/ui/atoms/form-field/form-field.tsx
+++ b/crazystacknextjs/src/shared/ui/atoms/form-field/form-field.tsx
@@ -1,7 +1,6 @@
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
-// Molecules
interface FormFieldProps {
label: string;
id: string;
@@ -22,15 +21,13 @@ export const FormField = ({
error,
}: FormFieldProps) => (
-
- {label}
-
+
{label}
{
return (
-
+
-
-
+
+ {item?.place?.profilephoto && (
+
+ )}
+
{
{item?.name}
- {item?.address ?? "Sem endereço"}
+ {item?.place?.address ?? "Sem endereço"}
Agendar
diff --git a/crazystacknextjs/src/slices/belezix/entidades/photo/photo.api.ts b/crazystacknextjs/src/slices/belezix/entidades/photo/photo.api.ts
new file mode 100644
index 0000000..526d183
--- /dev/null
+++ b/crazystacknextjs/src/slices/belezix/entidades/photo/photo.api.ts
@@ -0,0 +1,87 @@
+import { setupAPIClient } from "@/shared/api";
+import { PhotoProps, photoModel } from "./photo.model";
+export type GetPhotosResponse = {
+ photos: PhotoProps[];
+ totalCount: number;
+ next?: number;
+ prev?: number;
+};
+const registerByPage = 10;
+export const uploadPhoto = async ({ formDataImage, cookies }: any) => {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_API_URL}/uploadPhoto`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${cookies["belezixclient.token"]}`,
+ },
+ body: formDataImage,
+ },
+ );
+ if (response.ok) {
+ const data = await response.json();
+ console.log("Image uploaded:", data);
+ return data;
+ }
+ return null;
+};
+
+export const getPhotos = async (
+ page: number,
+ ctx: any,
+ params: any = {},
+): Promise => {
+ const apiClient = setupAPIClient(ctx);
+ if (!apiClient) {
+ throw new Error("API client is null");
+ }
+ const { data } = await apiClient.get("/photo/loadByPage", {
+ params: { page, sortBy: "createdAt", typeSort: "desc", ...params },
+ });
+ const { photos, total } = data || {};
+ const totalCount = Number(total ?? 0);
+ const lastPage = Number.isInteger(totalCount / registerByPage)
+ ? totalCount / registerByPage
+ : Math.floor(totalCount / registerByPage) + 1;
+ const response = {
+ photos: photos?.map?.((props: PhotoProps) => photoModel(props).format()),
+ totalCount,
+ };
+ if (lastPage > page) {
+ Object.assign(response, { next: page + 1 });
+ }
+ if (page > 1) {
+ Object.assign(response, { prev: page - 1 });
+ }
+ return response;
+};
+
+export const getInfinitePhotos = async (
+ page: number,
+ params: any,
+): Promise => {
+ return getPhotos(page, null, params);
+};
+export const getPhotoById = async (
+ id: string,
+ ctx: any,
+): Promise => {
+ try {
+ if (!ctx) {
+ throw new Error("Context is null");
+ }
+ const apiClient = setupAPIClient(ctx);
+ if (!apiClient) {
+ throw new Error("API client is null");
+ }
+ const { data } = await apiClient.get("/photo/load", {
+ params: { _id: id },
+ });
+ if (!data) {
+ return null;
+ }
+ return photoModel(data).format();
+ } catch (error) {
+ return null;
+ }
+};
diff --git a/crazystacknextjs/src/slices/belezix/entidades/photo/photo.model.ts b/crazystacknextjs/src/slices/belezix/entidades/photo/photo.model.ts
new file mode 100644
index 0000000..1a4bcb5
--- /dev/null
+++ b/crazystacknextjs/src/slices/belezix/entidades/photo/photo.model.ts
@@ -0,0 +1,65 @@
+export type PhotoProps = {
+ _id: string;
+ key: string;
+ createdById: string;
+ image: string;
+ url: string;
+ provider: string;
+ expiresIn: string;
+ createdAt: string;
+ updatedAt: string;
+ active: boolean;
+};
+
+class Photo {
+ protected props: PhotoProps;
+ constructor(props: PhotoProps) {
+ this.props = props;
+ }
+ public static build(props: PhotoProps) {
+ return new Photo(props);
+ }
+ get _id(): string {
+ return this.props._id;
+ }
+ get createdAt(): string {
+ return this.props.createdAt;
+ }
+ get active(): boolean | undefined {
+ return this.props.active;
+ }
+ get key(): string {
+ return this.props.key;
+ }
+ get image(): string {
+ return this.props.image;
+ }
+ get url(): string {
+ return this.props.url;
+ }
+ get provider(): string {
+ return this.props.provider;
+ }
+ get expiresIn(): string {
+ return this.props.expiresIn;
+ }
+ get createdById(): string {
+ return this.props.createdById;
+ }
+ get updatedAt(): string {
+ return this.props.updatedAt;
+ }
+
+ format(): PhotoProps {
+ return {
+ ...this.props,
+ _id: this.props._id,
+ createdAt: new Date(this.props.createdAt).toLocaleDateString("pt-BR", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ }),
+ };
+ }
+}
+export const photoModel = (props: PhotoProps) => Photo.build(props);
diff --git a/crazystacknextjs/src/slices/belezix/entidades/tweet/tweet.api.ts b/crazystacknextjs/src/slices/belezix/entidades/tweet/tweet.api.ts
new file mode 100644
index 0000000..60367a6
--- /dev/null
+++ b/crazystacknextjs/src/slices/belezix/entidades/tweet/tweet.api.ts
@@ -0,0 +1,105 @@
+import { api, getAxios, setupAPIClient } from "@/shared/api";
+import { TweetProps, tweetModel } from "./tweet.model";
+import axios from "axios";
+export type GetTweetsResponse = {
+ tweets: TweetProps[];
+ totalCount: number;
+ next?: number;
+ prev?: number;
+};
+const registerByPage = 10;
+
+export const addTweet = async ({ tweet, cookies }: any) => {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/tweet/add`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${cookies["belezixclient.token"]}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(tweet),
+ });
+ if (response.ok) {
+ const data = await response.json();
+ return data;
+ }
+ return null;
+};
+export const toggleLike = async ({ tweetlike, cookies }: any) => {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_API_URL}/tweetlike/toggleLike`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${cookies["belezixclient.token"]}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(tweetlike),
+ },
+ );
+ if (response.ok) {
+ const data = await response.json();
+ return data;
+ }
+ return null;
+};
+
+export const getTweets = async (
+ page: number,
+ ctx: any,
+ params: any = {},
+): Promise => {
+ const { data } = await getAxios(ctx?.["belezixclient.token"]).get(
+ !ctx?.["belezixclient.token"]
+ ? "publictweet/loadByPage"
+ : "/tweet/loadByPage",
+ {
+ params: { page, sortBy: "createdAt", typeSort: "desc", ...params },
+ },
+ );
+ const { tweets, total } = data || {};
+ const totalCount = Number(total ?? 0);
+ const lastPage = Number.isInteger(totalCount / registerByPage)
+ ? totalCount / registerByPage
+ : Math.floor(totalCount / registerByPage) + 1;
+ const response = {
+ tweets: tweets?.map?.((props: TweetProps) => tweetModel(props).format()),
+ totalCount,
+ };
+ if (lastPage > page) {
+ Object.assign(response, { next: page + 1 });
+ }
+ if (page > 1) {
+ Object.assign(response, { prev: page - 1 });
+ }
+ return response;
+};
+
+export const getInfiniteTweets = async (
+ page: number,
+ params: any,
+): Promise => {
+ return getTweets(page, null, params);
+};
+export const getTweetById = async (
+ id: string,
+ ctx: any,
+): Promise => {
+ try {
+ if (!ctx) {
+ throw new Error("Context is null");
+ }
+ const apiClient = setupAPIClient(ctx);
+ if (!apiClient) {
+ throw new Error("API client is null");
+ }
+ const { data } = await apiClient.get("/tweet/load", {
+ params: { _id: id },
+ });
+ if (!data) {
+ return null;
+ }
+ return tweetModel(data).format();
+ } catch (error) {
+ return null;
+ }
+};
diff --git a/crazystacknextjs/src/slices/belezix/entidades/tweet/tweet.model.ts b/crazystacknextjs/src/slices/belezix/entidades/tweet/tweet.model.ts
new file mode 100644
index 0000000..23e0c8f
--- /dev/null
+++ b/crazystacknextjs/src/slices/belezix/entidades/tweet/tweet.model.ts
@@ -0,0 +1,109 @@
+import { formatDistanceToNow } from "date-fns";
+
+export type TweetProps = {
+ _id: string;
+ userSlug: string;
+ body: string;
+ image: string;
+ tweetId: string;
+ createdAt: string;
+ updatedAt: string;
+ active: boolean;
+ createdById: string;
+ retweets: number;
+ distanceNow: string;
+ createdBy: {
+ slug: string;
+ photo: string;
+ name: string;
+ };
+ tweetlike: {
+ tweetlikes: {
+ _id: string;
+ createdById: string;
+ userSlug: string;
+ createdAt: string;
+ updatedAt: string;
+ }[];
+ total: number;
+ };
+};
+
+class Tweet {
+ protected props: TweetProps;
+ constructor(props: TweetProps) {
+ this.props = props;
+ }
+ public static build(props: TweetProps) {
+ return new Tweet(props);
+ }
+ get _id(): string {
+ return this.props._id;
+ }
+ get userSlug(): string {
+ return this.props.userSlug;
+ }
+ get body(): string {
+ return this.props.body;
+ }
+ get image(): string {
+ return this.props.image;
+ }
+ get tweetId(): string {
+ return this.props.tweetId;
+ }
+ get updatedAt(): string {
+ return this.props.updatedAt;
+ }
+ get createdById(): string {
+ return this.props.createdById;
+ }
+ get retweets(): number {
+ return this.props.retweets;
+ }
+ get distanceNow(): string {
+ return this.props.distanceNow;
+ }
+
+ get createdBy(): {
+ slug: string;
+ photo: string;
+ name: string;
+ } {
+ return this.props.createdBy;
+ }
+ get tweetlike(): {
+ tweetlikes: {
+ _id: string;
+ createdById: string;
+ userSlug: string;
+ createdAt: string;
+ updatedAt: string;
+ }[];
+ total: number;
+ } {
+ return this.props.tweetlike;
+ }
+
+ get createdAt(): string {
+ return this.props.createdAt;
+ }
+ get active(): boolean | undefined {
+ return this.props.active;
+ }
+ format(): TweetProps {
+ return {
+ ...this.props,
+ _id: this.props._id,
+ createdAt: new Date(this.props.createdAt).toLocaleDateString("pt-BR", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ }),
+ distanceNow: formatDistanceToNow(new Date(this.props.createdAt), {
+ addSuffix: true,
+ }),
+ };
+ }
+}
+export const tweetModel = (props: TweetProps) => Tweet.build(props);
diff --git a/crazystacknextjs/src/slices/general/entidades/user/user.model.ts b/crazystacknextjs/src/slices/general/entidades/user/user.model.ts
index b34c84c..a27b2c2 100644
--- a/crazystacknextjs/src/slices/general/entidades/user/user.model.ts
+++ b/crazystacknextjs/src/slices/general/entidades/user/user.model.ts
@@ -12,6 +12,7 @@ export type UserProps = {
serviceIds?: string[];
daysSinceRegister?: number;
phone?: string;
+ slug?: string;
};
class User {
@@ -25,6 +26,9 @@ class User {
get _id(): string {
return this.props._id;
}
+ get slug(): string {
+ return this.props.slug ?? "";
+ }
get name(): string {
return this.props.name;
}
@@ -72,7 +76,7 @@ class User {
daysSinceRegister: this.props.createdAt
? Math.floor(
(new Date().getTime() - new Date(this.props.createdAt).getTime()) /
- (1000 * 60 * 60 * 24)
+ (1000 * 60 * 60 * 24),
)
: 0,
};
@@ -81,7 +85,8 @@ class User {
export const calculateDaysSinceRegister = (createdAt: string) => {
if (!createdAt) return 9999;
return Math.floor(
- (new Date().getTime() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24)
+ (new Date().getTime() - new Date(createdAt).getTime()) /
+ (1000 * 60 * 60 * 24),
);
};
export const userModel = (props: UserProps) => User.build(props);
diff --git a/crazystacknextjs/src/slices/general/features/signup/components/country-selector.tsx b/crazystacknextjs/src/slices/general/features/signup/components/country-selector.tsx
new file mode 100644
index 0000000..5b5b57b
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/components/country-selector.tsx
@@ -0,0 +1,108 @@
+/* eslint-disable @next/next/no-img-element */
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { ChevronDown, Check, Search } from "lucide-react";
+export type Country = {
+ name: string;
+ code: string;
+ phone: string;
+ flag: string;
+ format: string;
+ mask: string;
+};
+
+interface CountrySelectorProps {
+ countries: Country[];
+ selectedCountry: Country;
+ setSelectedCountry: (country: Country) => void;
+ setSearchTerm: (value: string) => void;
+ searchTerm: string;
+ setValue: (field: string, value: any) => void;
+ setPhone: (value: string) => void;
+}
+
+export const CountrySelector = ({
+ countries,
+ selectedCountry,
+ setSelectedCountry,
+ setSearchTerm,
+ searchTerm,
+ setValue,
+ setPhone,
+}: CountrySelectorProps) => {
+ const filteredCountries = countries.filter(
+ (country) =>
+ country.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ country.code.toLowerCase().includes(searchTerm.toLowerCase()),
+ );
+
+ return (
+
+
+
+
+
+ (+{selectedCountry.code})
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="border-0 bg-transparent"
+ />
+
+
+ {filteredCountries.map((country) => (
+ {
+ setValue("country", {
+ code: country.code,
+ phone: country.phone,
+ });
+ setSelectedCountry(country);
+ setValue("phone", "");
+ setPhone("");
+ }}
+ className="flex items-center gap-2"
+ >
+
+
+ (+{country.code}) {country.name}
+
+ {selectedCountry.code === country.code && (
+
+ )}
+
+ ))}
+
+
+
+ );
+};
diff --git a/crazystacknextjs/src/slices/general/features/signup/components/email-password-form.tsx b/crazystacknextjs/src/slices/general/features/signup/components/email-password-form.tsx
new file mode 100644
index 0000000..2c7f619
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/components/email-password-form.tsx
@@ -0,0 +1,101 @@
+import { Button } from "@/components/ui/button";
+import { Icons } from "@/components/ui/icons";
+import { FormField } from "@/shared/ui/atoms/form-field";
+import { FieldValues, FormState, SubmitHandler } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import { PhoneInput } from "./phone-input";
+
+interface EmailPasswordFormProps {
+ formState: FormState;
+ handleSubmit: (
+ callback: SubmitHandler,
+ ) => (e?: React.BaseSyntheticEvent) => Promise;
+ register: any;
+ handleSignUp: any;
+ formProps: any;
+}
+
+export const EmailPasswordForm = ({
+ formState,
+ handleSubmit,
+ register,
+ handleSignUp,
+ formProps,
+}: EmailPasswordFormProps) => {
+ const { t } = useTranslation(["PAGES"]);
+ return (
+ {
+ console.log(e);
+ })}
+ >
+
+
+
+
+
+
+
+
+ {formState.isSubmitting && (
+
+ )}
+ {t("PAGES:AUTH_PAGE.signUp", { defaultValue: "Criar conta" })}
+
+
+
+ );
+};
diff --git a/crazystacknextjs/src/slices/general/features/signup/components/phone-input-field.tsx b/crazystacknextjs/src/slices/general/features/signup/components/phone-input-field.tsx
new file mode 100644
index 0000000..b6d467e
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/components/phone-input-field.tsx
@@ -0,0 +1,51 @@
+import { InputMask } from "@react-input/mask";
+import { AlertCircle } from "lucide-react";
+import { Input } from "@/components/ui/input";
+
+interface PhoneInputFieldProps {
+ id: string;
+ type: string;
+ placeholder: string;
+ phoneError: string;
+ isPhoneValid: boolean;
+ mask: string;
+ setPhone: (value: string) => void;
+ phone: string;
+}
+
+export const PhoneInputField = ({
+ id,
+ type,
+ placeholder,
+ phoneError,
+ isPhoneValid,
+ mask,
+ setPhone,
+ phone,
+}: PhoneInputFieldProps) => {
+ return (
+
+
{
+ setPhone(e.target.value);
+ }}
+ replacement={{ _: /\d/ }}
+ component={Input}
+ value={phone}
+ />
+ {phoneError && (
+
+ )}
+
+ );
+};
diff --git a/crazystacknextjs/src/slices/general/features/signup/components/phone-input.tsx b/crazystacknextjs/src/slices/general/features/signup/components/phone-input.tsx
new file mode 100644
index 0000000..bbe63b9
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/components/phone-input.tsx
@@ -0,0 +1,246 @@
+import { useState } from "react";
+import { Label } from "@/components/ui/label";
+import { CountrySelector, type Country } from "./country-selector";
+import { PhoneInputField } from "./phone-input-field";
+import parsePhoneNumberFromString, {
+ AsYouType,
+ isValidPhoneNumber,
+ type CountryCode,
+} from "libphonenumber-js";
+
+export const PhoneInput = ({ label, id, type, formProps }: any) => {
+ const { setValue } = formProps;
+ const [phone, setPhone] = useState("");
+ const [selectedCountry, setSelectedCountry] = useState(countries[0]);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [phoneError, setPhoneError] = useState("");
+ const [isPhoneValid, setIsPhoneValid] = useState(false);
+
+ const validateAndFormatPhone = (value: string) => {
+ try {
+ const digitsOnly = value.replace(/\D/g, "");
+ const formatter = new AsYouType(selectedCountry.code as CountryCode);
+ const formattedNumber = formatter.input(digitsOnly);
+ const fullNumber = `+${selectedCountry.phone}${digitsOnly}`;
+ const isValid = isValidPhoneNumber(
+ fullNumber,
+ selectedCountry.code as CountryCode,
+ );
+
+ const phoneNumber = parsePhoneNumberFromString(
+ fullNumber,
+ selectedCountry.code as CountryCode,
+ );
+
+ setIsPhoneValid(isValid);
+ setPhoneError(isValid ? "" : "Invalid phone number");
+ setValue("phone", formattedNumber);
+ setPhone(value);
+ return { formatted: formattedNumber, isValid, phoneNumber };
+ } catch (error) {
+ setIsPhoneValid(false);
+ setPhoneError("Invalid format");
+ setValue("phone", value);
+ setPhone(value);
+ return { formatted: value, isValid: false, phoneNumber: null };
+ }
+ };
+
+ return (
+
+ );
+};
+
+const countries = [
+ {
+ name: "Brazil",
+ code: "BR",
+ phone: "55",
+ flag: "🇧🇷",
+ format: "(99) 99999-9999",
+ mask: "(__) _____-____",
+ },
+ {
+ name: "United States",
+ code: "US",
+ phone: "1",
+ flag: "🇺🇸",
+ format: "(999) 999-9999",
+ mask: "(___) ___-____",
+ },
+ {
+ name: "Afghanistan",
+ code: "AF",
+ phone: "93",
+ flag: "🇦🇫",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "Albania",
+ code: "AL",
+ phone: "355",
+ flag: "🇦🇱",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "Algeria",
+ code: "DZ",
+ phone: "213",
+ flag: "🇩🇿",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "Argentina",
+ code: "AR",
+ phone: "54",
+ flag: "🇦🇷",
+ format: "(999) 999-9999",
+ mask: "(___) ___-____",
+ },
+ {
+ name: "Australia",
+ code: "AU",
+ phone: "61",
+ flag: "🇦🇺",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "Austria",
+ code: "AT",
+ phone: "43",
+ flag: "🇦🇹",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "Belgium",
+ code: "BE",
+ phone: "32",
+ flag: "🇧🇪",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "Canada",
+ code: "CA",
+ phone: "1",
+ flag: "🇨🇦",
+ format: "(999) 999-9999",
+ mask: "(___) ___-____",
+ },
+ {
+ name: "China",
+ code: "CN",
+ phone: "86",
+ flag: "🇨🇳",
+ format: "999 999 9999",
+ mask: "___ ___ ____",
+ },
+ {
+ name: "Colombia",
+ code: "CO",
+ phone: "57",
+ flag: "🇨🇴",
+ format: "999 999 9999",
+ mask: "___ ___ ____",
+ },
+ {
+ name: "France",
+ code: "FR",
+ phone: "33",
+ flag: "🇫🇷",
+ format: "99 99 99 99 99",
+ mask: "__ __ __ __ __",
+ },
+ {
+ name: "Germany",
+ code: "DE",
+ phone: "49",
+ flag: "🇩🇪",
+ format: "999 999 9999",
+ mask: "___ ___ ____",
+ },
+ {
+ name: "India",
+ code: "IN",
+ phone: "91",
+ flag: "🇮🇳",
+ format: "99999 99999",
+ mask: "_____ _____",
+ },
+ {
+ name: "Italy",
+ code: "IT",
+ phone: "39",
+ flag: "🇮🇹",
+ format: "999 999 9999",
+ mask: "___ ___ ____",
+ },
+ {
+ name: "Japan",
+ code: "JP",
+ phone: "81",
+ flag: "🇯🇵",
+ format: "999 999 9999",
+ mask: "___ ___ ____",
+ },
+ {
+ name: "Mexico",
+ code: "MX",
+ phone: "52",
+ flag: "🇲🇽",
+ format: "999 999 9999",
+ mask: "___ ___ ____",
+ },
+ {
+ name: "Portugal",
+ code: "PT",
+ phone: "351",
+ flag: "🇵🇹",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "Spain",
+ code: "ES",
+ phone: "34",
+ flag: "🇪🇸",
+ format: "999 999 999",
+ mask: "___ ___ ___",
+ },
+ {
+ name: "United Kingdom",
+ code: "GB",
+ phone: "44",
+ flag: "🇬🇧",
+ format: "9999 999999",
+ mask: "____ ______",
+ },
+] as const as Country[];
diff --git a/crazystacknextjs/src/slices/general/features/signup/index.ts b/crazystacknextjs/src/slices/general/features/signup/index.ts
new file mode 100644
index 0000000..0c1ce9b
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/index.ts
@@ -0,0 +1 @@
+export { SignUpForm } from "./signup-form";
diff --git a/crazystacknextjs/src/slices/general/features/signup/signup-form.tsx b/crazystacknextjs/src/slices/general/features/signup/signup-form.tsx
new file mode 100644
index 0000000..38f9562
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/signup-form.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { useTranslation } from "react-i18next";
+import { useSignUp } from "./signup.hook";
+import { cn } from "@/lib/utils";
+import { EmailPasswordForm } from "./components/email-password-form";
+import { SocialButton } from "@/shared/ui/atoms/social-button/social-button";
+import { Icons } from "@/components/ui/icons";
+
+interface SignUpFormProps extends React.HTMLAttributes {}
+
+export function SignUpForm({ className, ...props }: SignUpFormProps) {
+ const { formState, handleSubmit, register, handleSignUp, formProps } =
+ useSignUp();
+ return (
+
+
+
+
+ );
+}
+
+const GoogleSignUpSection = ({ formState }: { formState: any }) => {
+ const { t } = useTranslation(["PAGES"]);
+
+ return (
+ <>
+
+
+
+
+
+
+ {t("PAGES:AUTH_PAGE.orContinueWith", {
+ defaultValue: "Or continue with",
+ })}
+
+
+
+
+ Google
+
+ >
+ );
+};
diff --git a/crazystacknextjs/src/slices/general/features/signup/signup.hook.ts b/crazystacknextjs/src/slices/general/features/signup/signup.hook.ts
new file mode 100644
index 0000000..09059f3
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/signup.hook.ts
@@ -0,0 +1,26 @@
+import { useAuth } from "@/shared/libs/contexts/AuthContext";
+import { useSignUpLib, type SubmitSignUpHandler } from "./signup.lib";
+import { useEffect } from "react";
+import { getCurrentPosition } from "@/shared/libs/utils";
+
+export const useSignUp = () => {
+ const { signup } = useAuth();
+ const { handleSubmit, register, formState, ...formProps } = useSignUpLib();
+ useEffect(() => {
+ getCurrentPosition()
+ .then((position) => {
+ const { lat, lng } = position;
+ formProps?.setValue("coord", {
+ type: "Point",
+ coordinates: [lng, lat],
+ });
+ })
+ .catch((error) => {
+ console.error("Error getting location", error);
+ });
+ }, []);
+ const handleSignUp: SubmitSignUpHandler = async (data) => {
+ await signup(data);
+ };
+ return { handleSubmit, register, formState, handleSignUp, formProps };
+};
diff --git a/crazystacknextjs/src/slices/general/features/signup/signup.lib.ts b/crazystacknextjs/src/slices/general/features/signup/signup.lib.ts
new file mode 100644
index 0000000..94bd47c
--- /dev/null
+++ b/crazystacknextjs/src/slices/general/features/signup/signup.lib.ts
@@ -0,0 +1,69 @@
+import * as yup from "yup";
+import { yupResolver } from "@hookform/resolvers/yup";
+import { SubmitHandler, useForm } from "react-hook-form";
+import { isValidPhoneNumber, CountryCode } from "libphonenumber-js";
+
+export interface SignUpFormData {
+ email: string;
+ password: string;
+ passwordConfirmation: string;
+ name: string;
+ phone: string;
+ country: {
+ code?: string;
+ phone?: string;
+ };
+ coord: any;
+}
+
+export type SubmitSignUpHandler = SubmitHandler;
+
+export const signupSchema = yup.object({
+ email: yup.string().email("Email inválido").required("Email é obrigatório"),
+ password: yup.string().required("Senha é obrigatória"),
+ passwordConfirmation: yup
+ .string()
+ .oneOf([yup.ref("password")], "As senhas não correspondem")
+ .required("Confirmação de senha é obrigatória"),
+ name: yup.string().required("Nome é obrigatório"),
+ country: yup.object({
+ code: yup.string(),
+ phone: yup.string(),
+ }),
+ coord: yup.object({
+ type: yup.string(),
+ coordinates: yup.array().of(yup.number()),
+ }),
+ phone: yup
+ .string()
+ .required("Telefone é obrigatório")
+ .test("phone", "Telefone inválido", function (value) {
+ const digitsOnly = value?.replace?.(/\D/g, "");
+ const country = this.parent.country;
+ const fullNumber = `+${country.phone}${digitsOnly}`;
+ const isValid = isValidPhoneNumber(
+ fullNumber,
+ country.code as CountryCode,
+ );
+ return isValid;
+ }),
+});
+
+export type YupSchema = yup.InferType;
+
+export const useSignUpLib = () => {
+ const formProps = useForm({
+ resolver: yupResolver(signupSchema),
+ defaultValues: {
+ country: {
+ code: "BR",
+ phone: "55",
+ },
+ coord: {
+ type: "Point",
+ coordinates: [0, 0],
+ },
+ },
+ });
+ return formProps;
+};