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 && ( + Tweet image + )} +
+ +
+ + + +
+
+ {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 ( +
+
+ + + + {user?.name?.charAt?.(0)?.toUpperCase?.() ?? "U"} + + +
+