feat: messages images and components
This commit is contained in:
parent
028487d708
commit
860dadf82c
25
bun.lock
25
bun.lock
|
|
@ -7,7 +7,10 @@
|
|||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.99.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"laravel-echo": "^2.3.4",
|
||||
"lucide-react": "^1.8.0",
|
||||
"next": "16.2.3",
|
||||
"pusher-js": "^8.5.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
},
|
||||
|
|
@ -187,6 +190,8 @@
|
|||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
||||
|
|
@ -397,6 +402,10 @@
|
|||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
|
||||
|
||||
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
||||
|
|
@ -613,6 +622,8 @@
|
|||
|
||||
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||
|
||||
"laravel-echo": ["laravel-echo@2.3.4", "", { "peerDependencies": { "pusher-js": "*", "socket.io-client": "*" } }, "sha512-rpALCIK1uw2SrttcK9P5JzItt5I85RcfXQKUNnkcorzhtKeXi5GS0PVFFBH8ppNo8wnbdBKuD1EtIHgTbXo9FQ=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
|
@ -647,6 +658,8 @@
|
|||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
|
@ -719,6 +732,8 @@
|
|||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"pusher-js": ["pusher-js@8.5.0", "", { "dependencies": { "tweetnacl": "^1.0.3" } }, "sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
|
@ -771,6 +786,10 @@
|
|||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
||||
|
||||
"socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||
|
|
@ -813,6 +832,8 @@
|
|||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
|
@ -849,6 +870,10 @@
|
|||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@
|
|||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.99.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"laravel-echo": "^2.3.4",
|
||||
"lucide-react": "^1.8.0",
|
||||
"next": "16.2.3",
|
||||
"pusher-js": "^8.5.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Conversation } from '@/types/messaging';
|
||||
import ConversationList from '@/components/messaging/ConversationList';
|
||||
import ChatWindow from '@/components/messaging/ChatWindow';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
|
||||
export default function MessagesPage() {
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation | null>(null);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-neutral-950 overflow-hidden">
|
||||
{/* Sidebar / List */}
|
||||
<div className={`w-full md:w-80 lg:w-96 h-full flex-shrink-0 border-r border-gray-100 dark:border-neutral-800 ${selectedConversation ? 'hidden md:flex' : 'flex'}`}>
|
||||
<ConversationList
|
||||
onSelect={setSelectedConversation}
|
||||
activeId={selectedConversation?.id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Chat Area */}
|
||||
<div className={`flex-1 h-full flex flex-col ${!selectedConversation ? 'hidden md:flex' : 'flex'}`}>
|
||||
{selectedConversation ? (
|
||||
<div className="flex-1 h-full relative overflow-hidden">
|
||||
{/* Back button for mobile */}
|
||||
<button
|
||||
onClick={() => setSelectedConversation(null)}
|
||||
className="md:hidden absolute top-4 left-4 z-50 p-2 bg-white dark:bg-neutral-900 rounded-full shadow-md border border-gray-100 dark:border-neutral-800"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<ChatWindow conversation={selectedConversation} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 h-full flex flex-col items-center justify-center text-center p-8">
|
||||
<div className="w-20 h-20 bg-gray-100 dark:bg-neutral-900 rounded-full flex items-center justify-center mb-6">
|
||||
<MessageSquare className="w-10 h-10 text-gray-300 dark:text-neutral-700" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2">Vos messages</h2>
|
||||
<p className="text-gray-500 max-w-xs">
|
||||
Sélectionnez une conversation pour commencer à discuter ou créez-en une nouvelle.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
'use client'
|
||||
|
||||
import { useUser, useLogout } from '@/hooks/queries/auth'
|
||||
import Link from 'next/link'
|
||||
import Button from '@/components/common/Button'
|
||||
import Card from '@/components/common/Card'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAuth } from '@/providers/auth-provider'
|
||||
|
||||
export default function Home() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { data: user, isLoading } = useUser()
|
||||
const logout = useLogout()
|
||||
const { user, isLoading, logout } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
|
@ -36,7 +35,7 @@ export default function Home() {
|
|||
<div className="flex flex-col items-center">
|
||||
{user.avatarUrl && (
|
||||
<img
|
||||
src={user?.avatarUrl}
|
||||
src={user.avatarUrl.startsWith('http') ? user.avatarUrl : `http://localhost:8003/storage/${user.avatarUrl}`}
|
||||
alt={user.name}
|
||||
className="w-24 h-24 rounded-full object-cover mb-4 border-2 border-black dark:border-white"
|
||||
/>
|
||||
|
|
@ -45,14 +44,20 @@ export default function Home() {
|
|||
<p className="text-gray-600 dark:text-neutral-400">{user.email}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => logout.mutate()}
|
||||
disabled={logout.isPending}
|
||||
className="text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
>
|
||||
{logout.isPending ? 'Déconnexion...' : 'Se déconnecter'}
|
||||
</Button>
|
||||
<div className="flex justify-center gap-4">
|
||||
<Link href="/messages">
|
||||
<Button variant="primary">
|
||||
Accéder aux messages
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => logout()}
|
||||
className="text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
>
|
||||
Se déconnecter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
'use client'
|
||||
|
||||
import { Conversation } from '@/types/messaging';
|
||||
import { User } from '@/types/auth';
|
||||
import { MoreVertical } from 'lucide-react';
|
||||
import Button from '@/components/common/Button';
|
||||
|
||||
interface ChatHeaderProps {
|
||||
conversation: Conversation;
|
||||
currentUser: User | null;
|
||||
}
|
||||
|
||||
export default function ChatHeader({ conversation, currentUser }: ChatHeaderProps) {
|
||||
const otherParticipant = conversation.participants?.find(p => p.id !== currentUser?.id);
|
||||
|
||||
return (
|
||||
<div className="p-4 border-b border-gray-100 dark:border-neutral-800 flex justify-between items-center shrink-0 bg-white dark:bg-black">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 dark:bg-neutral-900 flex items-center justify-center overflow-hidden border border-gray-100 dark:border-neutral-800">
|
||||
{otherParticipant?.avatarUrl ? (
|
||||
<img src={otherParticipant.avatarUrl} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-xs font-bold text-gray-400">
|
||||
{(conversation.label || otherParticipant?.name || '?').substring(0, 2).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="font-semibold text-sm leading-none">
|
||||
{conversation.label || otherParticipant?.name || 'Conversation'}
|
||||
</h3>
|
||||
<p className="text-[10px] text-green-500 mt-1 uppercase font-bold tracking-wider">En ligne</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" className="p-2 h-auto rounded-full bg-transparent border-0 hover:bg-gray-100 dark:hover:bg-neutral-900">
|
||||
<MoreVertical className="w-5 h-5 text-gray-400" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { Send, Image as ImageIcon, Paperclip, X } from 'lucide-react';
|
||||
import Button from '@/components/common/Button';
|
||||
import Input from '@/components/common/Input';
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (data: { body?: string, image?: File }) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ChatInput({ onSend, disabled }: ChatInputProps) {
|
||||
const [content, setContent] = useState('');
|
||||
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setSelectedImage(file);
|
||||
setPreviewUrl(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
const removeImage = () => {
|
||||
setSelectedImage(null);
|
||||
setPreviewUrl(null);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if ((!content.trim() && !selectedImage) || disabled) return;
|
||||
|
||||
onSend({
|
||||
body: content.trim() || undefined,
|
||||
image: selectedImage || undefined
|
||||
});
|
||||
|
||||
setContent('');
|
||||
removeImage();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 border-t border-gray-100 dark:border-neutral-800 shrink-0 bg-white dark:bg-black">
|
||||
{previewUrl && (
|
||||
<div className="mb-4 relative inline-block">
|
||||
<img src={previewUrl} alt="Preview" className="h-24 w-auto rounded-lg border border-gray-200 dark:border-neutral-800" />
|
||||
<button
|
||||
onClick={removeImage}
|
||||
className="absolute -top-2 -right-2 p-1 bg-red-500 text-white rounded-full shadow-lg hover:bg-red-600 transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 items-center">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
ref={fileInputRef}
|
||||
onChange={handleImageSelect}
|
||||
/>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
className={`p-2 h-auto bg-transparent border-0 hover:bg-gray-100 dark:hover:bg-neutral-900 ${selectedImage ? 'text-indigo-500' : ''}`}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-5 h-5 text-gray-400" />
|
||||
</Button>
|
||||
<Button variant="secondary" type="button" className="p-2 h-auto bg-transparent border-0 hover:bg-gray-100 dark:hover:bg-neutral-900">
|
||||
<Paperclip className="w-5 h-5 text-gray-400" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
key="chat-input"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder={selectedImage ? "Ajouter une légende..." : "Écrivez un message..."}
|
||||
className="flex-1 bg-gray-50 dark:bg-neutral-900 border-0 focus:ring-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={(!content.trim() && !selectedImage) || disabled}
|
||||
type="submit"
|
||||
className="p-2 h-10 w-10 flex items-center justify-center rounded-full"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
'use client'
|
||||
|
||||
import { useMessages, useSendMessage } from '@/hooks/queries/messaging';
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { Conversation } from '@/types/messaging';
|
||||
import ChatHeader from './ChatHeader';
|
||||
import MessageList from './MessageList';
|
||||
import ChatInput from './ChatInput';
|
||||
|
||||
interface ChatWindowProps {
|
||||
conversation: Conversation;
|
||||
}
|
||||
|
||||
export default function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
const { data: messages, isLoading } = useMessages(conversation.id);
|
||||
const sendMessage = useSendMessage(conversation.id);
|
||||
const { user: currentUser } = useAuth();
|
||||
|
||||
const handleSend = (data: { body?: string, image?: File }) => {
|
||||
sendMessage.mutate(data);
|
||||
};
|
||||
|
||||
if (isLoading) return (
|
||||
<div className="flex-1 flex items-center justify-center bg-white dark:bg-black">
|
||||
<p className="text-gray-500 animate-pulse">Chargement des messages...</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const messageList = Array.isArray(messages) ? messages : [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full max-h-full bg-white dark:bg-black overflow-hidden">
|
||||
<ChatHeader conversation={conversation} currentUser={currentUser} />
|
||||
|
||||
<MessageList messages={messageList} currentUser={currentUser} />
|
||||
|
||||
<ChatInput
|
||||
onSend={handleSend}
|
||||
disabled={sendMessage.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
'use client'
|
||||
|
||||
import { useConversations } from '@/hooks/queries/messaging';
|
||||
import { useUser } from '@/hooks/queries/auth';
|
||||
import { Conversation } from '@/types/messaging';
|
||||
import { Search } from 'lucide-react';
|
||||
import Input from '@/components/common/Input';
|
||||
|
||||
interface ConversationListProps {
|
||||
onSelect: (conversation: Conversation) => void;
|
||||
activeId?: string;
|
||||
}
|
||||
|
||||
export default function ConversationList({ onSelect, activeId }: ConversationListProps) {
|
||||
const { data: conversations, isLoading } = useConversations();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
if (isLoading) return <div className="p-4">Chargement des conversations...</div>;
|
||||
|
||||
// Fix: If conversations is not an array (though useQuery should handle it now with response.data)
|
||||
const conversationList = Array.isArray(conversations) ? conversations : [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white dark:bg-black border-r border-gray-100 dark:border-neutral-800">
|
||||
<div className="p-4 border-b border-gray-100 dark:border-neutral-800">
|
||||
<h2 className="text-xl font-bold mb-4">Messages</h2>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Rechercher..."
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{conversationList.map((conv) => {
|
||||
const otherParticipant = conv.participants?.find(p => p.id !== currentUser?.id);
|
||||
const isActive = activeId === conv.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={conv.id}
|
||||
onClick={() => onSelect(conv)}
|
||||
className={`w-full flex items-center p-4 hover:bg-gray-50 dark:hover:bg-neutral-900 transition-colors border-b border-gray-50 dark:border-neutral-900 last:border-0 ${isActive ? 'bg-gray-50 dark:bg-neutral-900' : ''}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-800 flex items-center justify-center overflow-hidden border border-gray-100 dark:border-neutral-700">
|
||||
{otherParticipant?.avatarUrl ? (
|
||||
<img src={otherParticipant.avatarUrl} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-sm font-bold text-gray-500">
|
||||
{(conv.label || otherParticipant?.name || '?').substring(0, 2).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{conv.unread_count && conv.unread_count > 0 ? (
|
||||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-black dark:bg-white text-white dark:text-black text-[10px] font-bold rounded-full flex items-center justify-center">
|
||||
{conv.unread_count}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex-1 text-left">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<h3 className="font-semibold text-sm truncate max-w-[140px]">
|
||||
{conv.label || otherParticipant?.name || 'Sans nom'}
|
||||
</h3>
|
||||
<span className="text-[10px] text-gray-400 uppercase">
|
||||
{conv.lastMessage?.createdAt ? new Date(conv.lastMessage.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-neutral-400 truncate max-w-[200px]">
|
||||
{conv.lastMessage?.body || 'Aucun message'}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
'use client'
|
||||
|
||||
import { Message } from '@/types/messaging';
|
||||
import { User } from '@/types/auth';
|
||||
|
||||
interface MessageItemProps {
|
||||
message: Message;
|
||||
currentUser: User | null;
|
||||
}
|
||||
|
||||
export default function MessageItem({ message, currentUser }: MessageItemProps) {
|
||||
const isMe = message.senderId === currentUser?.id;
|
||||
|
||||
return (
|
||||
<div className={`flex ${isMe ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[70%] space-y-2 ${isMe ? 'items-end' : 'items-start'}`}>
|
||||
<div className={`p-3 rounded-2xl text-sm shadow-sm ${
|
||||
isMe
|
||||
? 'bg-black text-white rounded-br-none'
|
||||
: 'bg-gray-100 dark:bg-neutral-900 text-gray-900 dark:text-white rounded-bl-none border border-gray-200 dark:border-neutral-800'
|
||||
}`}>
|
||||
{message.imageUrl && (
|
||||
<div className="mb-2 rounded-lg overflow-hidden border border-white/10">
|
||||
<img
|
||||
src={message.imageUrl}
|
||||
alt="Shared image"
|
||||
className="max-w-full h-auto object-cover hover:scale-105 transition-transform cursor-pointer"
|
||||
onClick={() => window.open(message.imageUrl, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.body && <p className="break-words">{message.body}</p>}
|
||||
|
||||
<p className={`text-[10px] mt-1 ${isMe ? 'text-gray-400 text-right' : 'text-gray-500 text-left'}`}>
|
||||
{new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
'use client'
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { Message } from '@/types/messaging';
|
||||
import { User } from '@/types/auth';
|
||||
import MessageItem from './MessageItem';
|
||||
|
||||
interface MessageListProps {
|
||||
messages: Message[];
|
||||
currentUser: User | null;
|
||||
}
|
||||
|
||||
export default function MessageList({ messages, currentUser }: MessageListProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4 scroll-smooth"
|
||||
>
|
||||
{messages.map((msg, index) => (
|
||||
<MessageItem
|
||||
key={msg.id || index}
|
||||
message={msg}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -27,7 +27,9 @@ export function useLogin() {
|
|||
body: JSON.stringify(credentials),
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
Cookies.set('token', data.token, { expires: 30, sameSite: 'lax', secure: process.env.NODE_ENV === 'production' })
|
||||
console.log('Token received and saved:', data.token.substring(0, 10) + '...');
|
||||
Cookies.set('token', data.token, { expires: 30, sameSite: 'lax' })
|
||||
localStorage.setItem('token', data.token)
|
||||
queryClient.setQueryData(authKeys.user(), data.user)
|
||||
},
|
||||
})
|
||||
|
|
@ -54,7 +56,9 @@ export function useRegister() {
|
|||
})
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
Cookies.set('token', data.token, { expires: 30, sameSite: 'lax', secure: process.env.NODE_ENV === 'production' })
|
||||
console.log('Token received and saved:', data.token.substring(0, 10) + '...');
|
||||
Cookies.set('token', data.token, { expires: 30, sameSite: 'lax' })
|
||||
localStorage.setItem('token', data.token)
|
||||
queryClient.setQueryData(authKeys.user(), data.user)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
'use client'
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch } from '@/lib/api-client';
|
||||
import { Conversation, Message, SendMessageData, ApiResponse } from '@/types/messaging';
|
||||
import { useEffect } from 'react';
|
||||
import echo from '@/lib/echo';
|
||||
|
||||
export const messagingKeys = {
|
||||
all: ['messaging'] as const,
|
||||
conversations: () => [...messagingKeys.all, 'conversations'] as const,
|
||||
conversation: (id: string) => [...messagingKeys.all, 'conversation', id] as const,
|
||||
messages: (conversationId: string) => [...messagingKeys.conversation(conversationId), 'messages'] as const,
|
||||
};
|
||||
|
||||
export function useConversations() {
|
||||
return useQuery<Conversation[]>({
|
||||
queryKey: messagingKeys.conversations(),
|
||||
queryFn: async () => {
|
||||
const response = await apiFetch('/conversations');
|
||||
return (response as any).data || response;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useMessages(conversationId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery<Message[]>({
|
||||
queryKey: messagingKeys.messages(conversationId),
|
||||
queryFn: async () => {
|
||||
const response = await apiFetch(`/conversations/${conversationId}/messages`);
|
||||
return (response as any).data || response;
|
||||
},
|
||||
enabled: !!conversationId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversationId || !echo) return;
|
||||
|
||||
const channel = echo.private(`conversation.${conversationId}`)
|
||||
.listen('MessageSent', (e: any) => {
|
||||
const newMessage = e.message;
|
||||
if (!newMessage) return;
|
||||
|
||||
queryClient.setQueryData<Message[]>(messagingKeys.messages(conversationId), (old) => {
|
||||
const currentMessages = old || [];
|
||||
if (currentMessages.find(m => (m.id === newMessage.id) || (m.createdAt === newMessage.createdAt && m.senderId === newMessage.senderId))) {
|
||||
return currentMessages;
|
||||
}
|
||||
return [...currentMessages, newMessage];
|
||||
});
|
||||
|
||||
queryClient.setQueryData<Conversation[]>(messagingKeys.conversations(), (old) => {
|
||||
if (!old) return old;
|
||||
return old.map(conv => {
|
||||
if (conv.id === conversationId) {
|
||||
return {
|
||||
...conv,
|
||||
lastMessage: newMessage,
|
||||
updatedAt: newMessage.createdAt
|
||||
};
|
||||
}
|
||||
return conv;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
echo.leave(`conversation.${conversationId}`);
|
||||
};
|
||||
}, [conversationId, queryClient]);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export function useSendMessage(conversationId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Message, Error, SendMessageData>({
|
||||
mutationFn: async (data) => {
|
||||
const formData = new FormData();
|
||||
if (data.body) formData.append('body', data.body);
|
||||
if (data.image) formData.append('image', data.image);
|
||||
|
||||
const response = await apiFetch(`/conversations/${conversationId}/messages`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
return (response as any).data || response;
|
||||
},
|
||||
onSuccess: (newMessage) => {
|
||||
queryClient.setQueryData<Message[]>(messagingKeys.messages(conversationId), (old) => {
|
||||
const currentMessages = old || [];
|
||||
return [...currentMessages, newMessage];
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -3,37 +3,43 @@ import Cookies from 'js-cookie';
|
|||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
|
||||
|
||||
export async function apiFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const token = typeof window !== 'undefined' ? Cookies.get('token') : null;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
};
|
||||
|
||||
|
||||
// Only set Content-Type if it's NOT FormData
|
||||
if (!(options.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
let token = typeof window !== 'undefined' ? Cookies.get('token') : null;
|
||||
|
||||
if (!token && typeof window !== 'undefined') {
|
||||
token = localStorage.getItem('token');
|
||||
}
|
||||
|
||||
// Convert HeadersInit to a plain object
|
||||
const optionsHeaders = Object.fromEntries(new Headers(options.headers).entries());
|
||||
|
||||
// Override headers from options if provided
|
||||
const finalHeaders: Record<string, string> = { ...headers, ...optionsHeaders };
|
||||
|
||||
// Delete Content-Type if set to 'undefined' to let the browser handle it (for FormData)
|
||||
if (finalHeaders['Content-Type'] === 'undefined') {
|
||||
delete finalHeaders['Content-Type'];
|
||||
// Normalisation des headers en minuscules pour éviter les doublons
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
headers.set('Accept', 'application/json');
|
||||
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
// Si c'est un FormData, on NE DOIT PAS mettre de Content-Type
|
||||
// Sinon, on met application/json par défaut si rien n'est spécifié
|
||||
if (options.body instanceof FormData) {
|
||||
headers.delete('Content-Type');
|
||||
} else if (!headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
console.log(`[apiFetch] ${options.method || 'GET'} ${endpoint}`, {
|
||||
hasToken: !!token,
|
||||
isFormData: options.body instanceof FormData,
|
||||
contentType: headers.get('Content-Type')
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers: finalHeaders,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error(`[apiFetch] Error ${response.status} on ${endpoint}:`, errorData);
|
||||
throw new Error(errorData.message || 'An error occurred');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import Echo from 'laravel-echo';
|
||||
import Pusher from 'pusher-js';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).Pusher = Pusher;
|
||||
(window as any).Pusher.logToConsole = true;
|
||||
}
|
||||
|
||||
const echo = typeof window !== 'undefined'
|
||||
? new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: process.env.NEXT_PUBLIC_REVERB_APP_KEY,
|
||||
wsHost: 'localhost',
|
||||
wsPort: Number(process.env.NEXT_PUBLIC_REVERB_PORT),
|
||||
wssPort: Number(process.env.NEXT_PUBLIC_REVERB_PORT),
|
||||
forceTLS: false,
|
||||
enabledTransports: ['ws'],
|
||||
authEndpoint: `${process.env.NEXT_PUBLIC_API_URL}/broadcasting/auth`,
|
||||
auth: {
|
||||
headers: {
|
||||
// Get latest token from either source
|
||||
Authorization: `Bearer ${Cookies.get('token') || localStorage.getItem('token')}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
export default echo;
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import Cookies from 'js-cookie'
|
||||
import { User } from '@/types/auth'
|
||||
import { apiFetch } from '@/lib/api-client'
|
||||
import { authKeys } from '@/hooks/queries/auth'
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
isLoading: boolean
|
||||
isAuthenticated: boolean
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = useQueryClient()
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Read from both sources
|
||||
const cookieToken = Cookies.get('token')
|
||||
const localToken = localStorage.getItem('token')
|
||||
const finalToken = cookieToken || localToken
|
||||
|
||||
setToken(finalToken)
|
||||
console.log('[AuthProvider] Initial token check:', finalToken ? 'Found' : 'None')
|
||||
}, [])
|
||||
|
||||
const { data: user, isLoading } = useQuery<User | null>({
|
||||
queryKey: authKeys.user(),
|
||||
queryFn: () => apiFetch('/auth/me'),
|
||||
enabled: !!token,
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
const logout = () => {
|
||||
Cookies.remove('token')
|
||||
localStorage.removeItem('token')
|
||||
setToken(null)
|
||||
queryClient.setQueryData(authKeys.user(), null)
|
||||
queryClient.clear()
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user: user ?? null,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
'use client'
|
||||
|
||||
import { QueryProvider } from './query-provider'
|
||||
import { AuthProvider } from './auth-provider'
|
||||
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryProvider>
|
||||
{/* Ajoutez d'autres providers ici si besoin (ThemeProvider, etc.) */}
|
||||
{children}
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</QueryProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { User } from './auth';
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
label?: string;
|
||||
isGroup: boolean;
|
||||
participants?: User[];
|
||||
lastMessage?: Message;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
unread_count?: number;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
senderAvatarUrl?: string;
|
||||
body?: string;
|
||||
imageUrl?: string;
|
||||
readAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface SendMessageData {
|
||||
body?: string;
|
||||
image?: File;
|
||||
}
|
||||
Loading…
Reference in New Issue