diff --git a/backend/src/entities/Group.ts b/backend/src/entities/Group.ts index 34126df..5327063 100644 --- a/backend/src/entities/Group.ts +++ b/backend/src/entities/Group.ts @@ -45,7 +45,7 @@ export class Group extends BaseEntity { @Field() event_type: string; - @Column() + @Column({ default: 0 }) @Field() piggy_bank: number; diff --git a/backend/src/resolvers/GroupResolver.ts b/backend/src/resolvers/GroupResolver.ts index 0dcba1f..7851212 100644 --- a/backend/src/resolvers/GroupResolver.ts +++ b/backend/src/resolvers/GroupResolver.ts @@ -28,8 +28,8 @@ class CreateGroupInput { @Field() event_type!: string; - @Field() - piggy_bank!: number; + @Field({ nullable: true, defaultValue: 0 }) + piggy_bank?: number; @Field() deadline!: Date; @@ -40,6 +40,15 @@ class CreateGroupInput { user_beneficiary?: string; } +@InputType() +class AddFundsInput { + @Field() + groupId!: number; + + @Field() + amount!: number; +} + @ObjectType() export class MyGroupsResponse { @Field(() => [Group]) @@ -121,7 +130,7 @@ export default class GroupResolver { user_admin: userAdmin, name: data.name, event_type: data.event_type, - piggy_bank: data.piggy_bank, + piggy_bank: data.piggy_bank ?? 0, deadline: data.deadline, user_beneficiary: beneficiaryUser ?? undefined, }); @@ -156,4 +165,34 @@ export default class GroupResolver { return group; } + + @UseMiddleware(RoleMiddleware()) + @Mutation(() => Group) + async addFundsToGroup(@Arg("data") data: AddFundsInput, @Ctx() ctx: ContextType) { + if (!ctx.user) throw new Error("Utilisateur non connecté"); + + // Vérifier que le groupe existe + const group = await Group.findOne({ + where: { id: data.groupId }, + relations: { groupMember: true }, + }); + + if (!group) throw new Error("Groupe introuvable"); + + // Vérifier que l'utilisateur est membre du groupe + const isMember = await GroupMember.findOne({ + where: { groupId: data.groupId, userId: ctx.user.id }, + }); + + if (!isMember) throw new Error("Vous n'êtes pas membre de ce groupe"); + + // Vérifier que le montant est positif + if (data.amount <= 0) throw new Error("Le montant doit être positif"); + + // Ajouter les fonds à la cagnotte + group.piggy_bank += data.amount; + await group.save(); + + return group; + } } diff --git a/frontend/src/components/Wishlist.tsx b/frontend/src/components/Wishlist.tsx index 5cffffb..3d27144 100644 --- a/frontend/src/components/Wishlist.tsx +++ b/frontend/src/components/Wishlist.tsx @@ -151,14 +151,6 @@ export default function Wishlist() {

Aucune idée pour l'instant.

-
) : ( diff --git a/frontend/src/components/auth/AuthFormTemplate.tsx b/frontend/src/components/auth/AuthFormTemplate.tsx index dec1d93..1f21098 100644 --- a/frontend/src/components/auth/AuthFormTemplate.tsx +++ b/frontend/src/components/auth/AuthFormTemplate.tsx @@ -15,7 +15,7 @@ export default function AuthFormTemplate({ title, children, onSubmit, footer }:
{children} diff --git a/frontend/src/components/auth/auth.css b/frontend/src/components/auth/auth.css index 22b2c8a..c1ab0ac 100644 --- a/frontend/src/components/auth/auth.css +++ b/frontend/src/components/auth/auth.css @@ -55,7 +55,8 @@ align-items: center; justify-content: center; text-align: center; - font-size: 60px; + font-size: 75px; + line-height: 1; font-weight: 400; color: #fdfbf6; } diff --git a/frontend/src/components/forms/CreateGroupForm.tsx b/frontend/src/components/forms/CreateGroupForm.tsx index 35c8c0b..d9f05f9 100644 --- a/frontend/src/components/forms/CreateGroupForm.tsx +++ b/frontend/src/components/forms/CreateGroupForm.tsx @@ -4,7 +4,6 @@ import { useCreateGroupMutation } from "../../graphql/generated/graphql-types"; import { GET_ALL_MY_GROUPS } from "../../graphql/operations/groupOperations"; import { groupCreationFormValidation } from "../../hooks/formValidationRules"; import { useSanitizedForm } from "../../hooks/useSanitizedForm"; -import Button from "../utils/Button"; import Icon from "../utils/Icon"; import Input from "../utils/Input"; import InputWithToggle from "../utils/InputWithToggle"; @@ -15,9 +14,10 @@ import GroupLink from "./GroupLink"; type CreateGroupFormProps = { onSuccess?: () => void; + onCancel?: () => void; }; -export default function CreateGroupForm({ onSuccess }: CreateGroupFormProps) { +export default function CreateGroupForm({ onSuccess, onCancel }: CreateGroupFormProps) { const options = [ { label: "Anniversaire", @@ -121,14 +121,18 @@ export default function CreateGroupForm({ onSuccess }: CreateGroupFormProps) { } return ( - -
+ +
{/* Form to create a new group */} - Créer un groupe -
+ Créer un groupe +
-
+
- - { @@ -185,20 +179,28 @@ export default function CreateGroupForm({ onSuccess }: CreateGroupFormProps) { error={errors.deadline} />
- +
+ + {onCancel && ( + + )} +
{error &&

{error}

} {errors.main &&

{errors.main}

}
-
+
{/* Adding users can go here */}
diff --git a/frontend/src/components/groups/AddFundsModal.tsx b/frontend/src/components/groups/AddFundsModal.tsx new file mode 100644 index 0000000..44d7bb6 --- /dev/null +++ b/frontend/src/components/groups/AddFundsModal.tsx @@ -0,0 +1,105 @@ +import { useState } from "react"; +import { useAddFundsToGroupMutation } from "../../graphql/generated/graphql-types"; +import { GET_ALL_MY_GROUPS } from "../../graphql/operations/groupOperations"; +import Button from "../utils/Button"; +import Input from "../utils/Input"; +import Modal from "../utils/Modal"; + +type AddFundsModalProps = { + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; + groupId: number; + currentAmount: number; +}; + +export default function AddFundsModal({ + isOpen, + onClose, + onSuccess, + groupId, + currentAmount, +}: AddFundsModalProps) { + const [amount, setAmount] = useState(""); + const [error, setError] = useState(""); + + const [addFunds, { loading }] = useAddFundsToGroupMutation({ + refetchQueries: [{ query: GET_ALL_MY_GROUPS }], + awaitRefetchQueries: true, + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + const numAmount = Number(amount); + if (!amount || numAmount <= 0) { + setError("Veuillez entrer un montant valide"); + return; + } + + try { + await addFunds({ + variables: { + data: { + groupId, + amount: numAmount, + }, + }, + }); + setAmount(""); + onSuccess?.(); + onClose(); + } catch (err) { + if (err instanceof Error) { + setError(err.message); + } else { + setError("Une erreur est survenue"); + } + } + }; + + const handleClose = () => { + setAmount(""); + setError(""); + onClose(); + }; + + return ( + + +

Ajouter des fonds

+ +
+

Cagnotte actuelle

+

{currentAmount}€

+
+ +
+ setAmount(e.target.value)} + placeholder="Montant à ajouter (€)" + icon="piggyBank" + error={error} + /> +
+ +
+ + +
+ +
+ ); +} diff --git a/frontend/src/components/groups/Groups.tsx b/frontend/src/components/groups/Groups.tsx index ee9bd31..b666753 100644 --- a/frontend/src/components/groups/Groups.tsx +++ b/frontend/src/components/groups/Groups.tsx @@ -17,6 +17,8 @@ type GroupsProps = { messages: Record; getNbNewMessages: (groupId: number, messages: Message[]) => number; updateLastVu: (groupId: number, date: Date | string, serveurSyconization?: boolean) => void; + onGroupClick?: (group: GetAllMyGroupsQuery["getAllMyGroups"]["groups"][number]) => void; + activeGroupId?: number; }; export default function Groups({ @@ -25,6 +27,8 @@ export default function Groups({ loading, error, messages, + onGroupClick, + activeGroupId, getNbNewMessages, updateLastVu, }: GroupsProps) { @@ -37,7 +41,8 @@ export default function Groups({ <> } @@ -51,13 +56,15 @@ export default function Groups({ key={group.id} id={Number(group.id)} title={group.name} + active={activeGroupId === Number(group.id)} onClick={() => { setActiveGroup?.(group); updateLastVu(Number(group.id), messages[Number(group.id)][0].createdAt); + onGroupClick?.(group); }} nbNewMessages={getNbNewMessages(Number(group.id), messages[Number(group.id)] || [])} > -

+

Date limite: {formatDate(new Date(group.deadline))}
{group.groupMember?.length}{" "} diff --git a/frontend/src/components/groups/Messaging/Message.tsx b/frontend/src/components/groups/Messaging/Message.tsx index c3941eb..e8c0ebd 100644 --- a/frontend/src/components/groups/Messaging/Message.tsx +++ b/frontend/src/components/groups/Messaging/Message.tsx @@ -49,7 +49,7 @@ export default function Message({ regroupement, userId }: MessageProps) { {regroupement.map((message, index) => { return (

{message.content}

@@ -68,7 +68,7 @@ export default function Message({ regroupement, userId }: MessageProps) { profile utilisateur
diff --git a/frontend/src/components/groups/Messaging/Messaging.tsx b/frontend/src/components/groups/Messaging/Messaging.tsx index 7d2fa99..9f2ec1b 100644 --- a/frontend/src/components/groups/Messaging/Messaging.tsx +++ b/frontend/src/components/groups/Messaging/Messaging.tsx @@ -17,12 +17,14 @@ type MessagingProps = { date: Date; groupId: number; messages: GetAllMessageMyGroupsQuery["getAllMessageMyGroups"][number]["messages"]; - addMessages: (message: GetAllMessageMyGroupsQuery["getAllMessageMyGroups"][number]["messages"]) => void; + addMessages?: (message: GetAllMessageMyGroupsQuery["getAllMessageMyGroups"][number]["messages"]) => void; calbackSendMessage: (groupId: number, message: string) => void; contenairMessageRef: RefObject; - updateLastVu: (groupId: number, date: Date | string, serveurSyconization?: boolean) => void; - getLastVu: (groupId: number) => Date | undefined; - getNbNewMessages: (groupId: number, messages: MessageType[]) => number; + updateLastVu?: (groupId: number, date: Date | string, serveurSyconization?: boolean) => void; + getLastVu?: (groupId: number) => Date | undefined; + getNbNewMessages?: (groupId: number, messages: MessageType[]) => number; + isMobile?: boolean; + hideHeader?: boolean; }; export default function Messaging({ @@ -37,6 +39,8 @@ export default function Messaging({ updateLastVu, getLastVu, getNbNewMessages, + isMobile = false, + hideHeader = false, }: MessagingProps) { const id = useId(); const [messageInput, setMessageInput] = useState(""); @@ -116,7 +120,7 @@ export default function Messaging({ }, }); - addMessages(olderMessages.data?.getLazyMessages.messages || []); + addMessages?.(olderMessages.data?.getLazyMessages.messages || []); setOldMessagesPending(false); setIsMaximumMessages(olderMessages.data?.getLazyMessages.isMaximumMessages || false); }; @@ -174,7 +178,7 @@ export default function Messaging({ useEffect(() => { const el = contenairMessageRef.current; - if (!el) return; + if (!el || !getLastVu || !updateLastVu) return; const onScroll = () => { if (el.scrollHeight - (el.scrollTop + el.clientHeight) <= 5) { @@ -191,36 +195,46 @@ export default function Messaging({ el.addEventListener("scroll", onScroll); return () => el.removeEventListener("scroll", onScroll); - }, [messages, groupId]); + }, [messages, groupId, getLastVu, updateLastVu]); return ( -
-
-
- {title} -

- - {expired - ? `Ce groupe a expiré depuis ${Math.abs(daysLeft)} jour(s)` - : `${daysLeft} jour(s) restant(s)`}{" "} - {" "} - -{" "} - - {" "} - {participants} {participants === 1 ? "participant" : "participants"}{" "} - -

-
-
- +
+ {!hideHeader && ( +
+
+ {title} +

+ + {expired + ? `Ce groupe a expiré depuis ${Math.abs(daysLeft)} jour(s)` + : `${daysLeft} jour(s) restant(s)`}{" "} + {" "} + -{" "} + + {" "} + {participants} {participants === 1 ? "participant" : "participants"}{" "} + +

+
+
+ +
-
-
-
+ )} +
+
{orderedMessages.map((message, index) => { return ( @@ -234,7 +248,7 @@ export default function Messaging({ ); })}
- {getNbNewMessages(groupId, messages) > 0 && ( + {getNbNewMessages && getNbNewMessages(groupId, messages) > 0 && ( + ); + } + return ( + + ); + }; + + const renderRightButton = () => { + if (currentView === "cagnotte") { + return ( + + ); + } + return ( + + ); + }; + + return ( +
+ {renderLeftButton()} + {renderRightButton()} +
+ ); +} diff --git a/frontend/src/components/groups/Messaging/MobileChatHeader.tsx b/frontend/src/components/groups/Messaging/MobileChatHeader.tsx new file mode 100644 index 0000000..bdd1692 --- /dev/null +++ b/frontend/src/components/groups/Messaging/MobileChatHeader.tsx @@ -0,0 +1,38 @@ +import Icon from "../../utils/Icon"; +import Subtitle from "../../utils/Subtitle"; + +type MobileChatHeaderProps = { + title: string; + subtitle?: string; + onBack: () => void; + onMenuClick?: () => void; +}; + +export default function MobileChatHeader({ title, subtitle, onBack, onMenuClick }: MobileChatHeaderProps) { + return ( +
+ + +
+ {title} + {subtitle &&

{subtitle}

} +
+ + +
+ ); +} diff --git a/frontend/src/components/groups/PiggyBank.tsx b/frontend/src/components/groups/PiggyBank.tsx index 123b7f2..79f1a13 100644 --- a/frontend/src/components/groups/PiggyBank.tsx +++ b/frontend/src/components/groups/PiggyBank.tsx @@ -3,17 +3,19 @@ import Container from "../utils/Container"; type PiggyBankProps = { pot: number; + onAddFunds?: () => void; }; -export default function PiggyBank({ pot }: PiggyBankProps) { +export default function PiggyBank({ pot, onAddFunds }: PiggyBankProps) { return ( } + title="Ma cagnotte" + button={
-

{title}

+

{title}

{children}
diff --git a/frontend/src/components/utils/Container.tsx b/frontend/src/components/utils/Container.tsx index 5fec1b3..831e760 100644 --- a/frontend/src/components/utils/Container.tsx +++ b/frontend/src/components/utils/Container.tsx @@ -5,23 +5,28 @@ import Subtitle from "./Subtitle"; type ContainerProps = { colour: ColourScheme["colour"]; title: string; + icon?: React.ReactNode; button?: React.ReactNode; children?: React.ReactNode; + classNameTitle?: string; }; -export default function Container({ colour, title, button, children }: ContainerProps) { +export default function Container({ colour, title, icon, button, children, classNameTitle }: ContainerProps) { return (
{/* Header */}
- {title} +
+ {icon} + {title} +
{button}
{/* Scrollable content */} -
{children}
+
{children}
); } diff --git a/frontend/src/components/utils/Icon.tsx b/frontend/src/components/utils/Icon.tsx index ab76ee1..6e77d96 100644 --- a/frontend/src/components/utils/Icon.tsx +++ b/frontend/src/components/utils/Icon.tsx @@ -1,11 +1,11 @@ -import { FaArrowCircleRight, FaRegUser } from "react-icons/fa"; +import { FaArrowCircleRight, FaArrowLeft, FaRegUser } from "react-icons/fa"; import { FiLogOut } from "react-icons/fi"; import { HiDotsVertical, HiOutlineCurrencyDollar } from "react-icons/hi"; import { HiOutlineChatBubbleLeftRight } from "react-icons/hi2"; import { ImCancelCircle } from "react-icons/im"; import { IoIosClose } from "react-icons/io"; import { IoChatboxEllipsesOutline, IoSearch } from "react-icons/io5"; -import { LuCirclePlus, LuGift, LuHeart } from "react-icons/lu"; +import { LuCirclePlus, LuGift, LuHeart, LuPiggyBank } from "react-icons/lu"; import { RiImageCircleLine } from "react-icons/ri"; export type IconTypes = @@ -13,7 +13,9 @@ export type IconTypes = | "plus" | "heart" | "dollar" + | "piggyBank" | "arrow" + | "arrowLeft" | "logout" | "user" | "gift" @@ -34,7 +36,9 @@ const iconMap = { plus: LuCirclePlus, heart: LuHeart, dollar: HiOutlineCurrencyDollar, + piggyBank: LuPiggyBank, arrow: FaArrowCircleRight, + arrowLeft: FaArrowLeft, logout: FiLogOut, user: FaRegUser, gift: LuGift, @@ -49,9 +53,17 @@ const iconMap = { export default function Icon({ icon, text, className }: IconProps) { const IconComponent = iconMap[icon]; + const isPiggyBank = icon === "piggyBank"; + const isClose = icon === "close"; + const iconSize = isPiggyBank ? "text-3xl" : "text-2xl"; + const iconStroke = isPiggyBank ? 2 : isClose ? undefined : 3; + return ( -
- +
+ {text && {text}}
); diff --git a/frontend/src/components/utils/Input.tsx b/frontend/src/components/utils/Input.tsx index 16478c3..cc2d45c 100644 --- a/frontend/src/components/utils/Input.tsx +++ b/frontend/src/components/utils/Input.tsx @@ -28,12 +28,12 @@ export default function Input({ ...props }: InputProps) { const baseStyles = - "w-full p-2 border-2 rounded-lg font-bold text-lg outline-none transition-colors duration-200"; + "w-full p-2 rounded-lg font-inter font-bold text-lg outline-none transition-colors duration-200 placeholder:font-bold"; const themeStyles = theme === "dark" - ? "border-dark border-[3.5px] text-dark focus:border-blue" - : "bg-transparent border-white border-[3.5px] text-white placeholder-white-100 focus:placeholder-white"; + ? "border-dark border-[3.5px] text-dark focus:border-blue placeholder:text-dark/50" + : "bg-transparent border-white border-[3.5px] text-white placeholder:text-white/70"; const errorStyles = error ? "border-orange focus:border-orange" : ""; diff --git a/frontend/src/components/utils/InputWithToggle.tsx b/frontend/src/components/utils/InputWithToggle.tsx index e5413bb..f4d1da4 100644 --- a/frontend/src/components/utils/InputWithToggle.tsx +++ b/frontend/src/components/utils/InputWithToggle.tsx @@ -21,7 +21,7 @@ export default function InputWithToggle({ return (
{checked && } - {!checked && {question}} + {!checked && {question}}
); diff --git a/frontend/src/components/utils/Modal.tsx b/frontend/src/components/utils/Modal.tsx index 47ef4cc..1321fc6 100644 --- a/frontend/src/components/utils/Modal.tsx +++ b/frontend/src/components/utils/Modal.tsx @@ -13,6 +13,7 @@ type ModalProps = { closeOnOverlayClick?: boolean; showCloseButton?: boolean; withPadding?: boolean; + hideCloseButton?: boolean; }; const sizeClasses: Record = { diff --git a/frontend/src/components/utils/SearchSelectInput.tsx b/frontend/src/components/utils/SearchSelectInput.tsx index 7293921..b445277 100644 --- a/frontend/src/components/utils/SearchSelectInput.tsx +++ b/frontend/src/components/utils/SearchSelectInput.tsx @@ -77,12 +77,12 @@ export default function SearchSelectInput({ // Styles (copied from your Input component) const baseInput = - "w-full px-4 py-2 border-2 rounded-lg font-inter font-bold text-md outline-none transition-colors duration-200 focus:border-4 cursor-pointer"; + "w-full p-2 rounded-lg font-inter font-bold text-lg outline-none transition-colors duration-200 cursor-pointer placeholder:font-bold"; const themeInput = theme === "dark" - ? "border-dark text-dark focus:border-dark bg-white" - : "bg-transparent border-white text-white placeholder-white-100 focus:placeholder-white"; + ? "border-dark border-[3.5px] text-dark focus:border-blue bg-white placeholder:text-dark/50" + : "bg-transparent border-white border-[3.5px] text-white placeholder:text-white/70"; const errorInput = error ? "border-orange focus:border-orange" : ""; @@ -123,7 +123,7 @@ export default function SearchSelectInput({ } }} > - + {value ? options.find((o) => o.value === value)?.label : placeholder} diff --git a/frontend/src/components/utils/ToggleSwitch.tsx b/frontend/src/components/utils/ToggleSwitch.tsx index 0fa3ca0..18b12c8 100644 --- a/frontend/src/components/utils/ToggleSwitch.tsx +++ b/frontend/src/components/utils/ToggleSwitch.tsx @@ -18,8 +18,17 @@ interface ToggleProps { // }; export default function ToggleSwitch({ checked, onChange, mode = "light" }: ToggleProps) { - const borderColour = mode === "dark" ? "border-gray-300" : "border-white"; - const bulletColour = mode === "dark" ? "bg-gray-200" : "bg-white"; + const borderColour = mode === "dark" ? "border-dark" : "border-white"; + // When checked: solid fill, when unchecked: transparent (outline only) + const bgColour = checked ? (mode === "dark" ? "bg-dark" : "bg-white") : "bg-transparent"; + // Bullet border color: when checked on light mode, use green so it's visible against white bg + const bulletBorderColour = checked + ? mode === "dark" + ? "border-white" + : "border-green" + : mode === "dark" + ? "border-dark" + : "border-white"; return (
+ {/* Hollow bullet - just border, transparent inside */}
diff --git a/frontend/src/components/wishlist/Wishlist.css b/frontend/src/components/wishlist/Wishlist.css index ad317df..552ba7a 100644 --- a/frontend/src/components/wishlist/Wishlist.css +++ b/frontend/src/components/wishlist/Wishlist.css @@ -24,6 +24,31 @@ } /* Modal desktop styles */ +.modal-mobile-content { + padding: 40px; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; +} + +.modal-mobile-content h2 { + font-weight: 900; + text-align: center; + font-size: 28px; + margin-top: 0; + margin-bottom: 32px; +} + +.modal-mobile-content form { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 500px; + margin: 0 auto; +} + .modal-mobile-content label { text-align: left; font-size: 18px; @@ -59,6 +84,21 @@ font-weight: 700; } +.modal-mobile-content .modal-buttons { + display: flex; + justify-content: center; + gap: 16px; + padding-top: 24px; +} + +.modal-mobile-content .modal-buttons button { + padding: 12px 24px; + border-radius: 10px; + font-size: 16px; + font-weight: 700; + font-family: "Inter", sans-serif; +} + /* Hide scrollbar on mobile while keeping scroll functionality */ @media (max-width: 768px) { .wishlist-content-mobile { @@ -88,6 +128,10 @@ justify-content: center; border-radius: 40px; box-shadow: rgba(32, 9, 4, 0.2) 0px 2px 6px; + padding: 10px 20px; + font-size: 16px; + font-weight: 700; + font-family: "Inter", sans-serif; } .div-content-giftcard { @@ -116,11 +160,19 @@ padding-bottom: 16px; } + .modal-mobile-content { + padding: 32px; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + } + .modal-mobile-content h2 { font-weight: 900; text-align: center; font-size: 24px; - margin-top: 24px; + margin-top: 0; margin-bottom: 40px; } diff --git a/frontend/src/graphql/generated/graphql-types.ts b/frontend/src/graphql/generated/graphql-types.ts index b6ac299..11c77aa 100644 --- a/frontend/src/graphql/generated/graphql-types.ts +++ b/frontend/src/graphql/generated/graphql-types.ts @@ -18,6 +18,11 @@ export type Scalars = { DateTimeISO: { input: any; output: any; } }; +export type AddFundsInput = { + amount: Scalars['Float']['input']; + groupId: Scalars['Float']['input']; +}; + export type AddGiftInput = { description?: InputMaybe; imageUrl?: InputMaybe; @@ -38,7 +43,7 @@ export type CreateGroupInput = { deadline: Scalars['DateTimeISO']['input']; event_type: Scalars['String']['input']; name: Scalars['String']['input']; - piggy_bank: Scalars['Float']['input']; + piggy_bank?: InputMaybe; user_beneficiary?: InputMaybe; users?: InputMaybe>; }; @@ -153,6 +158,7 @@ export type Message = { export type Mutation = { __typename?: 'Mutation'; UpdateMyProfile: User; + addFundsToGroup: Group; addGift: Gift; addGiftToGroupList: Gift; banUser: BanUserResponse; @@ -175,6 +181,11 @@ export type MutationUpdateMyProfileArgs = { }; +export type MutationAddFundsToGroupArgs = { + data: AddFundsInput; +}; + + export type MutationAddGiftArgs = { data: AddGiftInput; }; @@ -377,6 +388,13 @@ export type SetLastMessageVuMutationVariables = Exact<{ export type SetLastMessageVuMutation = { __typename?: 'Mutation', setLastMessageVu: { __typename?: 'SetLastMessageVuOutput', sucess: boolean } }; +export type AddFundsToGroupMutationVariables = Exact<{ + data: AddFundsInput; +}>; + + +export type AddFundsToGroupMutation = { __typename?: 'Mutation', addFundsToGroup: { __typename?: 'Group', id: string, piggy_bank: number } }; + export type LoginMutationVariables = Exact<{ data: LoginInput; }>; @@ -731,6 +749,40 @@ export function useSetLastMessageVuMutation(baseOptions?: Apollo.MutationHookOpt export type SetLastMessageVuMutationHookResult = ReturnType; export type SetLastMessageVuMutationResult = Apollo.MutationResult; export type SetLastMessageVuMutationOptions = Apollo.BaseMutationOptions; +export const AddFundsToGroupDocument = gql` + mutation AddFundsToGroup($data: AddFundsInput!) { + addFundsToGroup(data: $data) { + id + piggy_bank + } +} + `; +export type AddFundsToGroupMutationFn = Apollo.MutationFunction; + +/** + * __useAddFundsToGroupMutation__ + * + * To run a mutation, you first call `useAddFundsToGroupMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAddFundsToGroupMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [addFundsToGroupMutation, { data, loading, error }] = useAddFundsToGroupMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useAddFundsToGroupMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(AddFundsToGroupDocument, options); + } +export type AddFundsToGroupMutationHookResult = ReturnType; +export type AddFundsToGroupMutationResult = Apollo.MutationResult; +export type AddFundsToGroupMutationOptions = Apollo.BaseMutationOptions; export const LoginDocument = gql` mutation Login($data: LoginInput!) { login(data: $data) { diff --git a/frontend/src/graphql/operations/groupOperations.ts b/frontend/src/graphql/operations/groupOperations.ts index 458fb64..cc8c43f 100644 --- a/frontend/src/graphql/operations/groupOperations.ts +++ b/frontend/src/graphql/operations/groupOperations.ts @@ -107,3 +107,12 @@ export const SET_LAST_MESSAGE_VU = gql` } } `; + +export const ADD_FUNDS_TO_GROUP = gql` + mutation AddFundsToGroup($data: AddFundsInput!) { + addFundsToGroup(data: $data) { + id + piggy_bank + } + } +`; diff --git a/frontend/src/hooks/formValidationRules.ts b/frontend/src/hooks/formValidationRules.ts index 8381bf0..98c0080 100644 --- a/frontend/src/hooks/formValidationRules.ts +++ b/frontend/src/hooks/formValidationRules.ts @@ -14,22 +14,12 @@ export function groupCreationFormValidation(values: CreateGroupInput) { if (!values.name) errors.name = "Le nom du groupe est requis"; else if (values.name.length < 6) errors.name = "Le nom du groupe doit faire au moins 6 charactères de long"; - if (!values.piggy_bank) errors.piggy_bank = "Veuillez définir une cagnotte"; - else if (Number.isNaN(Number(values.piggy_bank)) || Number(values.piggy_bank) <= 0) - errors.piggy_bank = "La cagnotte ne peut pas être négative"; - if (!values.deadline) errors.deadline = "La date butoire de l'évènement est requise"; else if (countdownDate(new Date(values.deadline)) < 0) errors.deadline = "La date ne peut pas être dans le passé"; //If all values are empty - if ( - !values.deadline && - !values.event_type && - !values.name && - !values.piggy_bank && - !values.user_beneficiary - ) { + if (!values.deadline && !values.event_type && !values.name && !values.user_beneficiary) { errors.main = "Les champs doivent être rempli"; } diff --git a/frontend/src/hooks/useWebSocket.ts b/frontend/src/hooks/useWebSocket.ts index ad2fe19..e009d33 100644 --- a/frontend/src/hooks/useWebSocket.ts +++ b/frontend/src/hooks/useWebSocket.ts @@ -12,13 +12,13 @@ export function useLive() { // l'événement de connexion socket.on("connect", () => { - // console.log("Connecté :", socket.id); + /* console.log("Connecté :", socket.id); */ }); setSocket(socket); return () => { - // console.log("Client déconncté") + /* console.log("Client déconncté"); */ socket.disconnect(); }; }, []); diff --git a/frontend/src/index.css b/frontend/src/index.css index 6b87b0b..6f0bc37 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -63,6 +63,29 @@ background-color: #9c9b97; } +/* Thin scrollbar for containers */ +.scrollbar-thin::-webkit-scrollbar { + width: 2px; +} + +.scrollbar-thin::-webkit-scrollbar-track { + background: transparent; +} + +.scrollbar-thin::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.5); + border-radius: 2px; +} + +.scrollbar-thin::-webkit-scrollbar-thumb:hover { + background-color: rgba(255, 255, 255, 0.7); +} + +.scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.5) transparent; +} + .bg-green { background-color: #019645; } diff --git a/frontend/src/pages/Conversations.tsx b/frontend/src/pages/Conversations.tsx index 18af900..1da7fdb 100644 --- a/frontend/src/pages/Conversations.tsx +++ b/frontend/src/pages/Conversations.tsx @@ -1,19 +1,42 @@ import { useEffect, useRef, useState } from "react"; +import { FaArrowLeft } from "react-icons/fa"; +import { HiDotsVertical } from "react-icons/hi"; +import { LuCirclePlus, LuMessageCircleMore } from "react-icons/lu"; +import CreateGroupForm from "../components/forms/CreateGroupForm"; +import AddFundsModal from "../components/groups/AddFundsModal"; import Groups from "../components/groups/Groups"; import Messaging from "../components/groups/Messaging/Messaging"; +import type { MobileView } from "../components/groups/Messaging/MobileBottomButtons"; +import MobileBottomButtons from "../components/groups/Messaging/MobileBottomButtons"; import PiggyBank from "../components/groups/PiggyBank"; import Wishlist from "../components/groups/Wishlist"; import Button from "../components/utils/Button"; +import Modal from "../components/utils/Modal"; import type { GetAllMessageMyGroupsQuery, GetAllMyGroupsQuery } from "../graphql/generated/graphql-types"; import { useGetAllMessageMyGroupsQuery, useGetAllMyGroupsQuery } from "../graphql/generated/graphql-types"; import useVuMessage from "../hooks/message/vuMessage"; import { useLiveChat } from "../hooks/useChat"; +import { useIsMobile } from "../hooks/useIsMobile"; import type { MessageType } from "../types/Groups"; +import { countdownDate, formatDate } from "../utils/dateCalculator"; +import { useMobileNavigationStore } from "../zustand/mobileNavigationStore"; +import "./conversations.css"; import type { Message } from "../types/Message"; +type MobileViewState = "groups" | "chat" | "wishlist" | "cagnotte"; + export default function Conversations() { + const isMobile = useIsMobile(); + const { setBottomNavVisible } = useMobileNavigationStore(); + const [wishlist, setWishlist] = useState(true); - const { data: groupData } = useGetAllMyGroupsQuery({ + const [mobileView, setMobileView] = useState("groups"); + const [isAnimating, setIsAnimating] = useState(false); + const [slideDirection, setSlideDirection] = useState<"left" | "right">("right"); + const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false); + const [isAddFundsModalOpen, setIsAddFundsModalOpen] = useState(false); + + const { data: groupData, refetch: refetchGroups } = useGetAllMyGroupsQuery({ fetchPolicy: "no-cache", nextFetchPolicy: "no-cache", }); @@ -21,12 +44,11 @@ export default function Conversations() { fetchPolicy: "no-cache", nextFetchPolicy: "no-cache", }); - // const { data: wishlistData } = useGroupWishlistItemsQuery({ variables: { groupId: Number(groups[indexGroups].id) } } || skip); + const [groups, setGroups] = useState([]); const [messages, setMessages] = useState({}); // const [nbNewMessagesRef, setNbNewMessagesRef] = useState<{ [groupId: number]: number }>({}); const { updateLastVu, getNbNewMessages, getLastVu } = useVuMessage(); - const [indexGroups, setIndexGroup] = useState(0); const contenairMessageRef = useRef(null); @@ -69,17 +91,29 @@ export default function Conversations() { setIndexGroup(groups.findIndex((g) => Number(g.id) === Number(group.id))); } + // Handle mobile view changes - hide/show bottom navigation + useEffect(() => { + if (isMobile) { + setBottomNavVisible(mobileView === "groups"); + } else { + setBottomNavVisible(true); + } + }, [isMobile, mobileView, setBottomNavVisible]); + + // Reset mobile view when switching from mobile to desktop + useEffect(() => { + if (!isMobile) { + setMobileView("groups"); + } + }, [isMobile]); + // pour set les groups useEffect(() => { - // if (!data?.getAllMyGroups) return; setGroups(groupData?.getAllMyGroups.groups || []); - - // demande au server de rejoindre les rooms que utilisateur possède chat.connectToRoom(groupData?.getAllMyGroups.groupToken); }, [groupData, chat]); useEffect(() => { - // waiting for data to load if (!groupData?.getAllMyGroups) return; if (groupData?.getAllMyGroups.groups.length === 0) { @@ -89,7 +123,6 @@ export default function Conversations() { setGroups(groupData?.getAllMyGroups.groups || []); - //keep active group in sync or default to first during refetch const existing = indexGroups !== -1 ? groupData?.getAllMyGroups.groups.find((group) => Number(group.id) === Number(indexGroups)) @@ -122,8 +155,6 @@ export default function Conversations() { setIndexGroup(indexGroups); }, [indexGroups]); - //TO DO: set activeGroup.id in url - const addMessage = (groupId: number, message: Message[]) => { setMessages((prev) => { const clone = structuredClone(prev); @@ -137,11 +168,259 @@ export default function Conversations() { const myGroups = groupData?.getAllMyGroups; + // Mobile navigation handlers + const handleGroupClick = (group: GetAllMyGroupsQuery["getAllMyGroups"]["groups"][number]) => { + setActiveGroup(group); + setSlideDirection("right"); + setIsAnimating(true); + setMobileView("chat"); + setTimeout(() => setIsAnimating(false), 300); + }; + + const handleBackToGroups = () => { + setSlideDirection("left"); + setIsAnimating(true); + setTimeout(() => { + setMobileView("groups"); + setIsAnimating(false); + }, 300); + }; + + const handleMobileViewChange = (view: MobileView) => { + setMobileView(view); + }; + + // Build subtitle for header + const getHeaderSubtitle = () => { + if (indexGroups === -1 || groups.length === 0) return undefined; + const group = groups[indexGroups]; + const daysLeft = countdownDate(new Date(group.deadline)); + const expired = daysLeft < 0; + const participants = group.groupMember?.length || 0; + + return `${expired ? `Expiré depuis ${Math.abs(daysLeft)} jour(s)` : `${daysLeft} jour(s) restant(s)`} - ${participants} ${participants === 1 ? "participant" : "participants"}`; + }; + + // Determine CSS classes for chat view + const getChatViewClasses = () => { + const classes = ["mobile-view", "mobile-chat-view"]; + + if (mobileView === "groups" && !isAnimating) { + classes.push("hidden-right"); + } + if (isAnimating && slideDirection === "right") { + classes.push("slide-in-right"); + } + if (isAnimating && slideDirection === "left") { + classes.push("slide-out-right"); + } + + return classes.join(" "); + }; + + // Mobile render + if (isMobile) { + return ( +
+ {/* Groups View - always visible underneath */} +
+
+ {/* Header */} +
+
+

Mes groupes

+
+
+ + {/* Content */} +
+ {groups.length === 0 ? ( +
+ +

Aucun groupe pour l'instant.

+ +
+ ) : ( +
+ {groups.map((group) => ( + + ))} +
+ )} +
+ + {/* Mobile Add Button */} + {groups.length > 0 && ( +
+ +
+ )} +
+ + {/* Create Group Modal */} + {isCreateGroupModalOpen && ( + setIsCreateGroupModalOpen(false)} isOpen={isCreateGroupModalOpen}> + setIsCreateGroupModalOpen(false)} + onCancel={() => setIsCreateGroupModalOpen(false)} + /> + + )} +
+ + {/* Chat/Wishlist/Cagnotte Views - slides over groups */} +
+ {/* Common Header for all views (Chat, Wishlist, Cagnotte) */} + {indexGroups !== -1 && groups.length > 0 && ( +
+ +
+

{groups[indexGroups].name}

+

{getHeaderSubtitle()}

+
+ +
+ )} + + {/* Chat View */} + {mobileView === "chat" && indexGroups !== -1 && groups.length > 0 && ( +
+ {messages[Number(groups[indexGroups].id)] !== undefined && ( + + )} +
+ )} + + {/* Wishlist View */} + {mobileView === "wishlist" && indexGroups !== -1 && ( +
+ {/* Idées du bénéficiaire */} +
+

Idées du bénéficiaire

+

Aucune idée ajoutée par le bénéficiaire.

+
+ + {/* Idées du groupe */} +
+

Idées proposées par le groupe

+

Aucune idée proposée pour le moment.

+
+ + {/* Button */} +
+ +
+
+ )} + + {/* Cagnotte View */} + {mobileView === "cagnotte" && indexGroups !== -1 && groups[indexGroups] && ( +
+
+

{groups[indexGroups]?.piggy_bank || 0}€

+

Cagnotte actuelle

+
+ + {/* Button */} +
+ +
+ + {/* Add Funds Modal for Mobile */} + setIsAddFundsModalOpen(false)} + onSuccess={() => refetchGroups()} + groupId={Number(groups[indexGroups].id)} + currentAmount={groups[indexGroups]?.piggy_bank || 0} + /> +
+ )} + + {/* Bottom Buttons */} + {mobileView !== "groups" && ( + handleMobileViewChange("chat")} + onWishlistClick={() => handleMobileViewChange("wishlist")} + onCagnotteClick={() => handleMobileViewChange("cagnotte")} + /> + )} +
+
+ ); + } + + // Desktop render (original layout) return (
{/* Left Column */} -
-
+
+
{myGroups && ( )}
-
+
-
+
{indexGroups !== -1 && + groups[indexGroups] && (wishlist ? ( {}} /> ) : ( - + setIsAddFundsModalOpen(true)} + /> ))}
+ + {/* Add Funds Modal */} + {indexGroups !== -1 && groups[indexGroups] && ( + setIsAddFundsModalOpen(false)} + onSuccess={() => refetchGroups()} + groupId={Number(groups[indexGroups].id)} + currentAmount={groups[indexGroups].piggy_bank} + /> + )}
{/* Right Column */} diff --git a/frontend/src/pages/UserProfilePage.tsx b/frontend/src/pages/UserProfilePage.tsx index 45062f5..8cc9875 100644 --- a/frontend/src/pages/UserProfilePage.tsx +++ b/frontend/src/pages/UserProfilePage.tsx @@ -314,155 +314,157 @@ const UserProfilePage = () => { {/* Conteneur principal */}
- {messageError &&

{messageError}

} - - {messageSuccess &&

{messageSuccess}

} - - {/* Grille des champs */} -
- {/* Prénom */} -
- - setProfile({ ...profile, firstName: e.target.value })} - disabled={!isEditing} - className={`profile-input ${isEditing ? "editable" : ""}`} - /> -
+
+ {messageError &&

{messageError}

} - {/* Nom */} -
- - setProfile({ ...profile, lastName: e.target.value })} - disabled={!isEditing} - className={`profile-input ${isEditing ? "editable" : ""}`} - /> -
+ {messageSuccess &&

{messageSuccess}

} - {/* Email */} -
- - setProfile({ ...profile, email: e.target.value })} - disabled={!isEditing} - className={`profile-input ${isEditing ? "editable" : ""}`} - /> -
+ {/* Grille des champs */} +
+ {/* Prénom */} +
+ + setProfile({ ...profile, firstName: e.target.value })} + disabled={!isEditing} + className={`profile-input ${isEditing ? "editable" : ""}`} + /> +
- {/* Téléphone */} -
- - setProfile({ ...profile, phone_number: e.target.value })} - disabled={!isEditing} - className={`profile-input ${isEditing ? "editable" : ""}`} - /> -
+ {/* Nom */} +
+ + setProfile({ ...profile, lastName: e.target.value })} + disabled={!isEditing} + className={`profile-input ${isEditing ? "editable" : ""}`} + /> +
- {/* Date de naissance */} -
- - setProfile({ ...profile, date_of_birth: e.target.value })} - disabled={!isEditing} - className={`profile-input ${isEditing ? "editable" : ""}`} - /> -
+ {/* Email */} +
+ + setProfile({ ...profile, email: e.target.value })} + disabled={!isEditing} + className={`profile-input ${isEditing ? "editable" : ""}`} + /> +
- {/* Mot de passe */} -
- - {!isEditing ? ( + {/* Téléphone */} +
+ setProfile({ ...profile, phone_number: e.target.value })} + disabled={!isEditing} + className={`profile-input ${isEditing ? "editable" : ""}`} /> - ) : ( +
+ + {/* Date de naissance */} +
+ setProfile({ ...profile, password: e.target.value })} - className="profile-input editable" + id={dateOfBirthInputId} + type={isEditing ? "date" : "text"} + value={ + isEditing + ? profile.date_of_birth + : new Date(profile.date_of_birth).toLocaleDateString("fr-FR") + } + onChange={(e) => setProfile({ ...profile, date_of_birth: e.target.value })} + disabled={!isEditing} + className={`profile-input ${isEditing ? "editable" : ""}`} /> +
+ + {/* Mot de passe */} +
+ + {!isEditing ? ( + + ) : ( + setProfile({ ...profile, password: e.target.value })} + className="profile-input editable" + /> + )} +
+ + {/* Confirmation mot de passe (uniquement en édition) */} + {isEditing && ( +
+ + setProfile({ ...profile, passwordConfirmation: e.target.value })} + className="profile-input editable profile-password-confirm" + /> +
)}
- - {/* Confirmation mot de passe (uniquement en édition) */} + {/* Boutons d'action */} {isEditing && ( -
- - setProfile({ ...profile, passwordConfirmation: e.target.value })} - className="profile-input editable profile-password-confirm" - /> +
+ +
)} -
- {/* Boutons d'action */} - {isEditing && ( -
- - -
- )} - {!isEditing && ( -
- - -
- )} + {!isEditing && ( +
+ + +
+ )} +
diff --git a/frontend/src/pages/conversations.css b/frontend/src/pages/conversations.css new file mode 100644 index 0000000..c5bce39 --- /dev/null +++ b/frontend/src/pages/conversations.css @@ -0,0 +1,463 @@ +/* Mobile Conversations View Animations */ + +.mobile-view-container { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.mobile-view { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* Groups view - base layer (always visible underneath) */ +.mobile-groups-view { + z-index: 1; + background-color: #292e96; +} + +/* Chat/Wishlist/Cagnotte views - overlay layer */ +.mobile-chat-view { + z-index: 2; + background-color: white; +} + +/* Hidden state - off screen to the right */ +.mobile-chat-view.hidden-right { + transform: translateX(100%); +} + +/* Animation: Slide in from right (entering chat) */ +.slide-in-right { + animation: slideInFromRight 0.3s ease-in-out forwards; +} + +/* Animation: Slide out to right (leaving chat, revealing groups) */ +.slide-out-right { + animation: slideOutToRight 0.3s ease-in-out forwards; +} + +@keyframes slideInFromRight { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slideOutToRight { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } +} + +/* ===== MOBILE GROUPS PAGE STYLES ===== */ + +.mobile-groups-page { + background-color: #292e96; + min-height: 100%; + padding: 24px; + padding-bottom: 140px; + display: flex; + flex-direction: column; +} + +.mobile-groups-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 24px; + margin-top: 16px; +} + +.mobile-groups-title { + display: flex; + align-items: center; + gap: 12px; + color: white; +} + +.mobile-groups-title h2 { + font-size: 24px; + font-weight: 700; + font-family: "Inter", sans-serif; + color: white; + margin: 0; +} + +.mobile-groups-content { + flex: 1; + overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.mobile-groups-content::-webkit-scrollbar { + display: none; +} + +.mobile-groups-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Mobile Group Card */ +.mobile-group-card { + display: flex; + align-items: center; + background-color: white; + border-radius: 12px; + padding: 16px; + cursor: pointer; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: none; + width: 100%; + text-align: left; +} + +.mobile-group-card:active { + transform: scale(0.98); +} + +.mobile-group-card-image { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + margin-right: 16px; + flex-shrink: 0; +} + +.mobile-group-card-content { + flex: 1; + min-width: 0; +} + +.mobile-group-card-title { + font-size: 16px; + font-weight: 700; + color: #200904; + margin: 0 0 4px 0; + font-family: "Inter", sans-serif; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mobile-group-card-info { + font-size: 14px; + color: #776b69; + margin: 0; + font-family: "Inter", sans-serif; +} + +/* Mobile Add Group Button */ +.mobile-groups-button-container { + position: fixed; + bottom: 6.4rem; + left: 24px; + right: 24px; + z-index: 10; + max-width: 640px; + margin: 0 auto; +} + +.mobile-groups-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + background-color: #00a650; + color: white; + border: none; + border-radius: 40px; + padding: 10px 20px; + font-size: 16px; + font-weight: 700; + font-family: "Inter", sans-serif; + cursor: pointer; + box-shadow: rgba(32, 9, 4, 0.2) 0px 2px 6px; + transition: background-color 0.2s ease; +} + +.mobile-groups-button:hover { + background-color: #01803b; +} + +.mobile-groups-button:active { + transform: scale(0.98); +} + +/* Empty state */ +.mobile-groups-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: white; + text-align: center; + padding: 40px 20px; +} + +.mobile-groups-empty-icon { + font-size: 70px; + opacity: 0.8; + margin-bottom: 12px; +} + +.mobile-groups-empty-text { + font-size: 18px; + margin-bottom: 32px; + font-family: "Inter", sans-serif; +} + +.mobile-groups-empty-button { + display: flex; + align-items: center; + gap: 8px; + background-color: #00a650; + color: white; + border: none; + border-radius: 12px; + padding: 12px 20px; + font-size: 16px; + font-weight: 600; + font-family: "Inter", sans-serif; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.mobile-groups-empty-button:hover { + background-color: #01803b; +} + +/* ===== COMMON MOBILE CHAT HEADER ===== */ + +.mobile-chat-header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #292e96; + padding: 16px 20px; + flex-shrink: 0; +} + +.mobile-chat-back { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.mobile-chat-title { + flex: 1; + text-align: center; +} + +.mobile-chat-title h2 { + font-size: 20px; + font-weight: 700; + font-family: "Inter", sans-serif; + color: white; + margin: 0; +} + +.mobile-chat-title p { + font-size: 12px; + color: rgba(255, 255, 255, 0.8); + margin: 4px 0 0 0; + font-family: "Inter", sans-serif; +} + +.mobile-chat-menu { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 8px; +} + +/* Chat content area */ +.mobile-chat-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + padding-bottom: 72px; +} + +/* ===== MOBILE SUBVIEW CONTENT (Wishlist/Cagnotte) ===== */ + +.mobile-subview-content { + flex: 1; + display: flex; + flex-direction: column; + padding: 24px; + padding-bottom: 160px; + overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.mobile-subview-content::-webkit-scrollbar { + display: none; +} + +.mobile-wishlist-bg { + background-color: #ea4b09; +} + +.mobile-cagnotte-bg { + background-color: #f5c400; +} + +/* Subview button (at bottom) */ +.mobile-subview-button-container { + position: fixed; + bottom: 7rem; + left: 24px; + right: 24px; + z-index: 10; + max-width: 640px; + margin: 0 auto; +} + +.mobile-subview-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + background-color: #00a650; + color: white; + border: none; + border-radius: 40px; + padding: 10px 20px; + font-size: 16px; + font-weight: 700; + font-family: "Inter", sans-serif; + cursor: pointer; + box-shadow: rgba(32, 9, 4, 0.2) 0px 2px 6px; + transition: background-color 0.2s ease; +} + +.mobile-subview-button:hover { + background-color: #01803b; +} + +.mobile-subview-button:active { + transform: scale(0.98); +} + +/* ===== MOBILE WISHLIST CONTENT STYLES ===== */ + +.mobile-wishlist-section { + margin-bottom: 24px; +} + +.mobile-wishlist-section-title { + font-size: 16px; + font-weight: 600; + color: white; + margin: 0 0 12px 0; + font-family: "Inter", sans-serif; +} + +.mobile-wishlist-section-empty { + font-size: 14px; + color: rgba(255, 255, 255, 0.7); + font-family: "Inter", sans-serif; +} + +.mobile-wishlist-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.mobile-wishlist-card { + display: flex; + align-items: center; + background-color: white; + border-radius: 12px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.mobile-wishlist-card-image { + width: 48px; + height: 48px; + border-radius: 8px; + object-fit: cover; + margin-right: 16px; + flex-shrink: 0; + background-color: #f5f5f5; +} + +.mobile-wishlist-card-content { + flex: 1; + min-width: 0; +} + +.mobile-wishlist-card-title { + font-size: 16px; + font-weight: 700; + color: #200904; + margin: 0 0 4px 0; + font-family: "Inter", sans-serif; +} + +.mobile-wishlist-card-description { + font-size: 14px; + color: #776b69; + margin: 0; + font-family: "Inter", sans-serif; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ===== MOBILE CAGNOTTE CONTENT STYLES ===== */ + +.mobile-cagnotte-amount { + text-align: center; + color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; +} + +.mobile-cagnotte-amount-value { + font-size: 48px; + font-weight: 800; + font-family: "Inter", sans-serif; + margin: 0; +} + +.mobile-cagnotte-amount-label { + font-size: 18px; + font-family: "Inter", sans-serif; + margin: 8px 0 0 0; + opacity: 0.9; +} diff --git a/frontend/src/pages/userprofile.css b/frontend/src/pages/userprofile.css index cfef34f..23657bb 100644 --- a/frontend/src/pages/userprofile.css +++ b/frontend/src/pages/userprofile.css @@ -73,8 +73,8 @@ .profile-image-edit-icon { position: absolute; - bottom: 50%; - right: -10px; + bottom: 14%; + right: 0px; transform: translateY(50%); background-color: #200904; color: #fdfbf6; @@ -106,8 +106,14 @@ display: flex; flex-direction: column; min-height: 0; + overflow: hidden; +} + +.profile-content-inner { + flex: 1; overflow-y: auto; - overflow-x: hidden; + padding-right: 32px; + margin-right: -24px; } .profile-success-message { diff --git a/frontend/src/zustand/mobileNavigationStore.ts b/frontend/src/zustand/mobileNavigationStore.ts new file mode 100644 index 0000000..4540967 --- /dev/null +++ b/frontend/src/zustand/mobileNavigationStore.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +type MobileNavigationState = { + isBottomNavVisible: boolean; + setBottomNavVisible: (visible: boolean) => void; +}; + +export const useMobileNavigationStore = create((set) => ({ + isBottomNavVisible: true, + setBottomNavVisible: (visible: boolean) => set({ isBottomNavVisible: visible }), +}));