From 7cc123a9b451d07799269139cd9610a875c3dbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chlo=C3=A9=20Bretnacher?= Date: Thu, 5 Feb 2026 10:22:29 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Mobile=20discussion=20et=20am=C3=A9lioratio?= =?UTF-8?q?ns=20style=20global?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .husky/pre-commit | 24 +- backend/src/entities/Group.ts | 2 +- backend/src/resolvers/GroupResolver.ts | 45 +- frontend/src/components/Wishlist.tsx | 44 +- .../src/components/auth/AuthFormTemplate.tsx | 2 +- frontend/src/components/auth/auth.css | 3 +- .../src/components/forms/CreateGroupForm.tsx | 56 ++- .../src/components/groups/AddFundsModal.tsx | 105 ++++ frontend/src/components/groups/Groups.tsx | 20 +- .../components/groups/Messaging/Messaging.tsx | 68 ++- .../groups/Messaging/MobileBottomButtons.tsx | 74 +++ .../groups/Messaging/MobileChatHeader.tsx | 38 ++ frontend/src/components/groups/PiggyBank.tsx | 12 +- frontend/src/components/groups/Wishlist.tsx | 13 +- .../navigation/BottomNavigation.tsx | 6 + .../src/components/navigation/Navigation.tsx | 2 +- .../src/components/navigation/navigation.css | 8 +- frontend/src/components/utils/Button.tsx | 2 +- frontend/src/components/utils/Card.tsx | 8 +- frontend/src/components/utils/Container.tsx | 13 +- frontend/src/components/utils/Icon.tsx | 20 +- frontend/src/components/utils/Input.tsx | 6 +- .../src/components/utils/InputWithToggle.tsx | 2 +- frontend/src/components/utils/Modal.tsx | 24 +- .../components/utils/SearchSelectInput.tsx | 8 +- .../src/components/utils/ToggleSwitch.tsx | 42 +- frontend/src/components/wishlist/Wishlist.css | 54 +- .../src/graphql/generated/graphql-types.ts | 54 +- .../src/graphql/operations/groupOperations.ts | 9 + frontend/src/hooks/formValidationRules.ts | 12 +- frontend/src/hooks/useWebSocket.ts | 4 +- frontend/src/index.css | 23 + frontend/src/pages/Conversations.tsx | 337 ++++++++++++- frontend/src/pages/UserProfilePage.tsx | 268 +++++----- frontend/src/pages/conversations.css | 463 ++++++++++++++++++ frontend/src/pages/userprofile.css | 12 +- frontend/src/zustand/mobileNavigationStore.ts | 11 + 37 files changed, 1555 insertions(+), 339 deletions(-) create mode 100644 frontend/src/components/groups/AddFundsModal.tsx create mode 100644 frontend/src/components/groups/Messaging/MobileBottomButtons.tsx create mode 100644 frontend/src/components/groups/Messaging/MobileChatHeader.tsx create mode 100644 frontend/src/pages/conversations.css create mode 100644 frontend/src/zustand/mobileNavigationStore.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 90ab0aa..dbe5798 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,13 +1,13 @@ -npm run husky-lint - -# si le paramètre bypass-bin est pas actif -if [ "$bypass_bin" != "1" ]; then - # Bloquer toute modification du dossier bin/ - if git diff --cached --name-only | grep -q "^bin/"; then - echo "" - echo "🚫 Vous n'avez pas le droit de modifier le contenu du dossier \"bin/\"" - echo "Ce fichier est sensible donc vous devez étre sûr de ce que vous faites." - echo "" - exit 1 - fi +npm run husky-lint + +# si le paramètre bypass-bin est pas actif +if [ "$bypass_bin" != "1" ]; then + # Bloquer toute modification du dossier bin/ + if git diff --cached --name-only | grep -q "^bin/"; then + echo "" + echo "🚫 Vous n'avez pas le droit de modifier le contenu du dossier \"bin/\"" + echo "Ce fichier est sensible donc vous devez étre sûr de ce que vous faites." + echo "" + exit 1 + fi fi \ No newline at end of file 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 3729b75..c5f4fde 100644 --- a/frontend/src/components/Wishlist.tsx +++ b/frontend/src/components/Wishlist.tsx @@ -132,14 +132,6 @@ export default function Wishlist() {

Aucune idée pour l'instant.

-
) : ( @@ -255,21 +247,13 @@ export default function Wishlist() { className="w-full border border-gray-300 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-orange" /> -
- - +
@@ -333,21 +317,13 @@ export default function Wishlist() { className="w-full border border-gray-300 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-orange" /> -
- - +
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 80f40a3..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 267ac48..3cb3be4 100644 --- a/frontend/src/components/groups/Groups.tsx +++ b/frontend/src/components/groups/Groups.tsx @@ -13,9 +13,18 @@ type GroupsProps = { loading: boolean; error?: string; onClick?: () => void; + onGroupClick?: (group: GetAllMyGroupsQuery["getAllMyGroups"]["groups"][number]) => void; + activeGroupId?: number; }; -export default function Groups({ groups, setActiveGroup, loading, error }: GroupsProps) { +export default function Groups({ + groups, + setActiveGroup, + loading, + error, + onGroupClick, + activeGroupId, +}: GroupsProps) { const [isOpen, setIsOpen] = useState(false); function toggleModal() { @@ -26,7 +35,8 @@ export default function Groups({ groups, setActiveGroup, loading, error }: Group <> } > {loading &&
Loading...
} @@ -38,11 +48,13 @@ export default function Groups({ groups, setActiveGroup, loading, error }: Group key={group.id} id={Number(group.id)} title={group.name} + active={activeGroupId === Number(group.id)} onClick={() => { setActiveGroup?.(group); + onGroupClick?.(group); }} > -

+

Date limite: {formatDate(new Date(group.deadline))}
{group.groupMember?.length}{" "} @@ -55,7 +67,7 @@ export default function Groups({ groups, setActiveGroup, loading, error }: Group {isOpen && ( - + )} diff --git a/frontend/src/components/groups/Messaging/Messaging.tsx b/frontend/src/components/groups/Messaging/Messaging.tsx index 4b49012..75a1389 100644 --- a/frontend/src/components/groups/Messaging/Messaging.tsx +++ b/frontend/src/components/groups/Messaging/Messaging.tsx @@ -1,6 +1,6 @@ import type { FormEvent, KeyboardEvent, RefObject } from "react"; import { useEffect, useId, useMemo, useRef, useState } from "react"; -import { FaLocationArrow } from "react-icons/fa"; +import { FaArrowLeft, FaLocationArrow } from "react-icons/fa"; import type { GetAllMessageMyGroupsQuery } from "../../../graphql/generated/graphql-types.ts"; import { countdownDate } from "../../../utils/dateCalculator.ts"; import { useMyProfileStore } from "../../../zustand/myProfileStore.ts"; @@ -16,6 +16,9 @@ type MessagingProps = { messages: GetAllMessageMyGroupsQuery["getAllMessageMyGroups"][number]["messages"]; calbackSendMessage: (groupId: number, message: string) => void; contenairMessageRef: RefObject; + onBack?: () => void; + isMobile?: boolean; + hideHeader?: boolean; }; export default function Messaging({ @@ -26,6 +29,9 @@ export default function Messaging({ messages, calbackSendMessage, contenairMessageRef, + onBack, + isMobile = false, + hideHeader = false, }: MessagingProps) { const id = useId(); const [messageInput, setMessageInput] = useState(""); @@ -93,31 +99,47 @@ export default function Messaging({ }, [messages]); return ( -

-
-
- {title} -

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

-
-
- +
+ {!hideHeader && ( +
+ {isMobile && onBack && ( + + )} +
+ {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) => { return ( diff --git a/frontend/src/components/groups/Messaging/MobileBottomButtons.tsx b/frontend/src/components/groups/Messaging/MobileBottomButtons.tsx new file mode 100644 index 0000000..c1817fe --- /dev/null +++ b/frontend/src/components/groups/Messaging/MobileBottomButtons.tsx @@ -0,0 +1,74 @@ +import { LuHeart, LuMessageCircleMore, LuPiggyBank } from "react-icons/lu"; + +export type MobileView = "chat" | "wishlist" | "cagnotte"; + +type MobileBottomButtonsProps = { + currentView: MobileView; + onChatClick: () => void; + onWishlistClick: () => void; + onCagnotteClick: () => void; +}; + +export default function MobileBottomButtons({ + currentView, + onChatClick, + onWishlistClick, + onCagnotteClick, +}: MobileBottomButtonsProps) { + const renderLeftButton = () => { + if (currentView === "wishlist") { + return ( + + ); + } + 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={ 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 949cc2e..cc82c51 100644 --- a/frontend/src/components/utils/Modal.tsx +++ b/frontend/src/components/utils/Modal.tsx @@ -5,23 +5,27 @@ type ModalProps = { onClose: () => void; children: React.ReactNode; className?: string; + hideCloseButton?: boolean; }; -export default function Modal({ isOpen, onClose, children, className }: ModalProps) { +export default function Modal({ isOpen, onClose, children, className, hideCloseButton }: ModalProps) { if (!isOpen) return null; return ( -
+
- + {/* Desktop close button */} + {!hideCloseButton && ( + + )} {children}
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 730b3a0..46fe111 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>; }; @@ -143,6 +148,7 @@ export type Message = { export type Mutation = { __typename?: 'Mutation'; UpdateMyProfile: User; + addFundsToGroup: Group; addGift: Gift; addGiftToGroupList: Gift; banUser: BanUserResponse; @@ -164,6 +170,11 @@ export type MutationUpdateMyProfileArgs = { }; +export type MutationAddFundsToGroupArgs = { + data: AddFundsInput; +}; + + export type MutationAddGiftArgs = { data: AddGiftInput; }; @@ -332,6 +343,13 @@ export type GetAllMessageMyGroupsQueryVariables = Exact<{ [key: string]: never; export type GetAllMessageMyGroupsQuery = { __typename?: 'Query', getAllMessageMyGroups: Array<{ __typename?: 'GroupMessagesOutput', groupId: number, messages: Array<{ __typename?: 'Message', id: string, content: string, createdAt: any, updatedAt: any, isEdited: boolean, user: { __typename?: 'User', id: string, firstName: string, lastName: string, image_url?: string | null, isAdmin: 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; }>; @@ -598,6 +616,40 @@ export type GetAllMessageMyGroupsQueryHookResult = ReturnType; export type GetAllMessageMyGroupsSuspenseQueryHookResult = ReturnType; export type GetAllMessageMyGroupsQueryResult = Apollo.QueryResult; +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 9e69303..1af8624 100644 --- a/frontend/src/graphql/operations/groupOperations.ts +++ b/frontend/src/graphql/operations/groupOperations.ts @@ -75,3 +75,12 @@ export const GET_ALL_MESSAGE_MY_GROUPS = 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 d4ae235..7193b96 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -62,6 +62,29 @@ background-color: #c3c1bc; } +/* 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 69240ee..c056ca7 100644 --- a/frontend/src/pages/Conversations.tsx +++ b/frontend/src/pages/Conversations.tsx @@ -1,19 +1,41 @@ import { useEffect, useLayoutEffect, 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 { 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"; type message = GetAllMessageMyGroupsQuery["getAllMessageMyGroups"][number]["messages"][number]; +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,10 +43,9 @@ 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 [indexGroups, setIndexGroup] = useState(0); const contenairMessageRef = useRef(null); @@ -37,7 +58,6 @@ export default function Conversations() { }); }; - // scrolle vers le bas quand le rerendu est fait useLayoutEffect(() => { contenairMessageRef.current?.scrollTo(0, contenairMessageRef.current.scrollHeight); }, [messages]); @@ -48,17 +68,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) { @@ -68,7 +100,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)) @@ -77,7 +108,6 @@ export default function Conversations() { setIndexGroup(existing ? indexGroups : 0); }, [groupData, indexGroups]); - // pour set les messages useEffect(() => { const data = messageData?.getAllMessageMyGroups; const messagesMap: MessageType = {}; @@ -96,21 +126,275 @@ export default function Conversations() { setIndexGroup(indexGroups); }, [indexGroups]); - //TO DO: set activeGroup.id in url - 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 581903d..a9ba770 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 }), +})); From c033f304a72fa068fc95e4aa957bf67383cc839b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chlo=C3=A9=20Bretnacher?= Date: Fri, 6 Feb 2026 11:14:31 +0100 Subject: [PATCH 2/2] Fix merged issues 2 --- frontend/src/components/groups/Messaging/Messaging.tsx | 4 +++- frontend/src/components/utils/Modal.tsx | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/groups/Messaging/Messaging.tsx b/frontend/src/components/groups/Messaging/Messaging.tsx index d76ade2..9f2ec1b 100644 --- a/frontend/src/components/groups/Messaging/Messaging.tsx +++ b/frontend/src/components/groups/Messaging/Messaging.tsx @@ -225,7 +225,9 @@ export default function Messaging({
)} -
+
diff --git a/frontend/src/components/utils/Modal.tsx b/frontend/src/components/utils/Modal.tsx index e023cf8..1321fc6 100644 --- a/frontend/src/components/utils/Modal.tsx +++ b/frontend/src/components/utils/Modal.tsx @@ -20,7 +20,6 @@ const sizeClasses: Record = { sm: "max-w-md", md: "max-w-2xl", lg: "max-w-6xl", - }; export default function Modal({