fix: chat page styles for dark theme
This commit is contained in:
@@ -1,137 +1,160 @@
|
||||
'use client';
|
||||
'use client'
|
||||
|
||||
import { ef } from '@/lib/helpers/utfNumbers';
|
||||
import { EmojiHappy, Microphone2, Paperclip2 } from 'iconsax-react';
|
||||
import { ef } from '@/lib/helpers/utfNumbers'
|
||||
import { EmojiHappy, Microphone2, Paperclip2 } from 'iconsax-react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = object
|
||||
|
||||
type MessageModel = {
|
||||
type: "sender" | "receiver",
|
||||
senderName: string,
|
||||
content: string,
|
||||
date: string,
|
||||
time: string,
|
||||
type: 'sender' | 'receiver'
|
||||
senderName: string
|
||||
content: string
|
||||
date: string
|
||||
time: string
|
||||
}
|
||||
|
||||
function ChatIndex({ }: Props) {
|
||||
function ChatIndex ({}: Props) {
|
||||
const { t } = useTranslation('chat')
|
||||
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const [messages, setMessages] = useState<Array<MessageModel>>([
|
||||
{
|
||||
type: "sender",
|
||||
senderName: "علی مصلحی",
|
||||
content: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است',
|
||||
date: '1403/09/30',
|
||||
time: '10:07'
|
||||
},
|
||||
{
|
||||
type: 'receiver',
|
||||
senderName: 'سیما فرهادی',
|
||||
content: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است',
|
||||
date: '1403/09/30',
|
||||
time: '10:07'
|
||||
},
|
||||
]);
|
||||
|
||||
const addMessageOutgoing = (model: MessageModel) => {
|
||||
setMessages((previous) => {
|
||||
return [...previous, model]
|
||||
})
|
||||
const [messages, setMessages] = useState<Array<MessageModel>>([
|
||||
{
|
||||
type: 'sender',
|
||||
senderName: 'علی مصلحی',
|
||||
content:
|
||||
'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است',
|
||||
date: '1403/09/30',
|
||||
time: '10:07'
|
||||
},
|
||||
{
|
||||
type: 'receiver',
|
||||
senderName: 'سیما فرهادی',
|
||||
content:
|
||||
'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است',
|
||||
date: '1403/09/30',
|
||||
time: '10:07'
|
||||
}
|
||||
])
|
||||
|
||||
const submitMessage = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const content = formData.get('textMessage');
|
||||
if (!content) return;
|
||||
const model: MessageModel = {
|
||||
type: 'sender',
|
||||
senderName: 'علی مصلحی',
|
||||
content: String(content),
|
||||
date: '1403/09/30',
|
||||
time: '10:07'
|
||||
}
|
||||
addMessageOutgoing(model);
|
||||
};
|
||||
const addMessageOutgoing = (model: MessageModel) => {
|
||||
setMessages(previous => {
|
||||
return [...previous, model]
|
||||
})
|
||||
}
|
||||
|
||||
const submitMessage = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.currentTarget)
|
||||
const content = formData.get('textMessage')
|
||||
if (!content) return
|
||||
const model: MessageModel = {
|
||||
type: 'sender',
|
||||
senderName: 'علی مصلحی',
|
||||
content: String(content),
|
||||
date: '1403/09/30',
|
||||
time: '10:07'
|
||||
}
|
||||
addMessageOutgoing(model)
|
||||
}
|
||||
|
||||
const messagesMemo = useMemo(() => messages, [messages]);
|
||||
const messagesMemo = useMemo(() => messages, [messages])
|
||||
|
||||
return (
|
||||
<section className="flex flex-col h-full pt-6">
|
||||
<h1 className="font-medium">علی مصلحی</h1>
|
||||
return (
|
||||
<section className='flex flex-col h-full pt-6'>
|
||||
<h1 className='font-medium'>علی مصلحی</h1>
|
||||
|
||||
<div className="bg-container rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden">
|
||||
<div className="overflow-y-auto flex flex-col-reverse">
|
||||
<ul className="grid gap-6 pb-6 ">
|
||||
{messagesMemo.map((v, i) => {
|
||||
if (v.type === "sender") {
|
||||
return (
|
||||
<li key={i} className='bg-[#F6F7FA] rounded-[30px] rounded-tr-none p-6'>
|
||||
<article>
|
||||
<p className='text-xs2'>
|
||||
{v.content}
|
||||
</p>
|
||||
<div className='bg-container rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden'>
|
||||
<div className='overflow-y-auto flex flex-col-reverse'>
|
||||
<ul className='grid gap-6 pb-6 '>
|
||||
{messagesMemo.map((v, i) => {
|
||||
if (v.type === 'sender') {
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
className='bg-[#F6F7FA] dark:bg-gray-700 rounded-[30px] rounded-tr-none p-6'
|
||||
>
|
||||
<article>
|
||||
<p className='text-xs2'>{v.content}</p>
|
||||
|
||||
<span dir='ltr' className='text-[11px] float-end text-disabled-text mt-2.5'>
|
||||
{ef(`${v.time} | ${v.date}`)}
|
||||
</span>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<li key={i} className='bg-[#EBEDF5] rounded-[30px] rounded-tl-none p-6'>
|
||||
<article>
|
||||
<h6 className='text-xs font-medium'>
|
||||
{v.senderName}
|
||||
</h6>
|
||||
<p className='text-xs2 mt-2.5'>
|
||||
{v.content}
|
||||
</p>
|
||||
<span
|
||||
dir='ltr'
|
||||
className='text-[11px] float-end text-disabled-text mt-2.5'
|
||||
>
|
||||
{ef(`${v.time} | ${v.date}`)}
|
||||
</span>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
className='bg-[#EBEDF5] dark:bg-gray-600 rounded-[30px] rounded-tl-none p-6'
|
||||
>
|
||||
<article>
|
||||
<h6 className='text-xs font-medium'>{v.senderName}</h6>
|
||||
<p className='text-xs2 mt-2.5'>{v.content}</p>
|
||||
|
||||
<span dir='ltr' className='text-[11px] float-end text-disabled-text mt-2.5'>
|
||||
{ef(`${v.time} | ${v.date}`)}
|
||||
</span>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<span
|
||||
dir='ltr'
|
||||
className='text-[11px] float-end text-disabled-text mt-2.5'
|
||||
>
|
||||
{ef(`${v.time} | ${v.date}`)}
|
||||
</span>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section aria-labelledby={t("InputMessage.AriaLabelBy")} className="w-full">
|
||||
<form
|
||||
onSubmit={submitMessage}
|
||||
className="focus-within:outline-blue-400 outline outline-transparent transition-colors duration-100 flex items-center gap-0 bg-transparent border border-[#D0D0D0] h-12 rounded-xl py-2.5 px-2"
|
||||
>
|
||||
<button type="button" className="active:bg-neutral-100 transition-colors duration-150 rounded-full p-1">
|
||||
<Microphone2 className="stroke-foreground" size={24} />
|
||||
</button>
|
||||
<input
|
||||
name="textMessage"
|
||||
role="textbox"
|
||||
autoComplete="off"
|
||||
className="w-full text-[11px] h-full outline-none ms-1"
|
||||
type="text"
|
||||
placeholder={t("InputMessage.Placeholder")}
|
||||
/>
|
||||
<button type="button" className="active:bg-neutral-100 transition-colors duration-150 rounded-full p-1.5">
|
||||
<EmojiHappy className="stroke-disabled2" size={20} />
|
||||
</button>
|
||||
<button type="button" className="active:bg-neutral-100 transition-colors duration-150 rounded-full p-1.5">
|
||||
<Paperclip2 className="stroke-disabled2" size={20} />
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<section
|
||||
aria-labelledby={t('InputMessage.AriaLabelBy')}
|
||||
className='w-full'
|
||||
>
|
||||
<form
|
||||
onSubmit={submitMessage}
|
||||
className='focus-within:outline-blue-400 outline outline-transparent transition-colors duration-100 flex items-center gap-0 bg-transparent border border-border h-12 rounded-xl py-2.5 px-2'
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='active:bg-neutral-100 dark:active:bg-neutral-600 transition-colors duration-150 rounded-full p-1'
|
||||
>
|
||||
<Microphone2 className='stroke-foreground' size={24} />
|
||||
</button>
|
||||
<input
|
||||
name='textMessage'
|
||||
role='textbox'
|
||||
autoComplete='off'
|
||||
className='w-full text-[11px] h-full outline-none ms-1'
|
||||
type='text'
|
||||
placeholder={t('InputMessage.Placeholder')}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
className='active:bg-neutral-100 dark:active:bg-neutral-600 transition-colors duration-150 rounded-full p-1.5'
|
||||
>
|
||||
<EmojiHappy
|
||||
className='stroke-disabled2 dark:stroke-neutral-400'
|
||||
size={20}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='active:bg-neutral-100 dark:active:bg-neutral-600 transition-colors duration-150 rounded-full p-1.5'
|
||||
>
|
||||
<Paperclip2
|
||||
className='stroke-disabled2 dark:stroke-neutral-400'
|
||||
size={20}
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatIndex
|
||||
@@ -1,140 +1,184 @@
|
||||
'use client';
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
const users = [
|
||||
{ id: 0, name: '1 علی مصلحی', table: 12, avatar: '/assets/images/user-avatar.png' },
|
||||
{ id: 1, name: '2 علی مصلحی', table: 10, avatar: '/assets/images/user-avatar.png' },
|
||||
{ id: 2, name: '3 علی مصلحی', table: 11, avatar: '/assets/images/user-avatar.png' },
|
||||
{ id: 3, name: '4 علی مصلحی', table: 5, avatar: '/assets/images/user-avatar.png' },
|
||||
{ id: 4, name: '5 علی مصلحی', table: 2, avatar: '/assets/images/user-avatar.png' },
|
||||
{ id: 5, name: '6 علی مصلحی', table: 1, avatar: '/assets/images/user-avatar.png' },
|
||||
];
|
||||
{
|
||||
id: 0,
|
||||
name: '1 علی مصلحی',
|
||||
table: 12,
|
||||
avatar: '/assets/images/user-avatar.png'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: '2 علی مصلحی',
|
||||
table: 10,
|
||||
avatar: '/assets/images/user-avatar.png'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '3 علی مصلحی',
|
||||
table: 11,
|
||||
avatar: '/assets/images/user-avatar.png'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '4 علی مصلحی',
|
||||
table: 5,
|
||||
avatar: '/assets/images/user-avatar.png'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '5 علی مصلحی',
|
||||
table: 2,
|
||||
avatar: '/assets/images/user-avatar.png'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '6 علی مصلحی',
|
||||
table: 1,
|
||||
avatar: '/assets/images/user-avatar.png'
|
||||
}
|
||||
]
|
||||
|
||||
function ChatNearby() {
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
const [positions, setPositions] = useState<{ top: number; left: number }[]>([]);
|
||||
function ChatNearby () {
|
||||
const containerRef = useRef<HTMLElement>(null)
|
||||
const [positions, setPositions] = useState<{ top: number; left: number }[]>(
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const maxW = containerRef.current.offsetWidth - 50;
|
||||
const maxH = containerRef.current.offsetHeight - 180;
|
||||
const elementSize = 110; // approximate size of each user element
|
||||
const padding = 80; // minimum distance between elements
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const maxW = containerRef.current.offsetWidth - 50
|
||||
const maxH = containerRef.current.offsetHeight - 180
|
||||
const elementSize = 110 // approximate size of each user element
|
||||
const padding = 80 // minimum distance between elements
|
||||
|
||||
const positions: { top: number; left: number }[] = [];
|
||||
const positions: { top: number; left: number }[] = []
|
||||
|
||||
const isTooClose = (top: number, left: number) => {
|
||||
return positions.some(p => {
|
||||
const dx = p.left - left;
|
||||
const dy = p.top - top;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
return distance < padding;
|
||||
});
|
||||
};
|
||||
const isTooClose = (top: number, left: number) => {
|
||||
return positions.some(p => {
|
||||
const dx = p.left - left
|
||||
const dy = p.top - top
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
return distance < padding
|
||||
})
|
||||
}
|
||||
|
||||
users.forEach(() => {
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
let tries = 0;
|
||||
users.forEach(() => {
|
||||
let top = 0
|
||||
let left = 0
|
||||
let tries = 0
|
||||
|
||||
do {
|
||||
top = Math.random() * Math.abs(maxH - elementSize);
|
||||
left = Math.random() * Math.abs(maxW - elementSize);
|
||||
tries++;
|
||||
} while (isTooClose(top, left) && tries < 100);
|
||||
do {
|
||||
top = Math.random() * Math.abs(maxH - elementSize)
|
||||
left = Math.random() * Math.abs(maxW - elementSize)
|
||||
tries++
|
||||
} while (isTooClose(top, left) && tries < 100)
|
||||
|
||||
positions.push({ top, left });
|
||||
});
|
||||
positions.push({ top, left })
|
||||
})
|
||||
|
||||
setPositions(positions);
|
||||
}
|
||||
}, []);
|
||||
setPositions(positions)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section ref={containerRef} className='h-svh relative'>
|
||||
<section className='relative h-full w-full mt-7'>
|
||||
<motion.div
|
||||
className='w-[1145px] h-[1145px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
||||
bg-[linear-gradient(180deg,#EAECF0_4.52%,#DBDEE8_95.02%)]
|
||||
dark:bg-[linear-gradient(180deg,#1F2937_4.52%,#374151_95.02%)]'
|
||||
style={{ willChange: 'transform' }}
|
||||
></motion.div>
|
||||
|
||||
return (
|
||||
<section ref={containerRef} className="h-svh relative">s
|
||||
<section className="relative h-full w-full mt-7">
|
||||
<motion.div
|
||||
className='size-[1145px] rounded-full fixed top-1/2 left-1/2 -translate-1/2'
|
||||
style={{ background: 'linear-gradient(180deg, #EAECF0 4.52%, #DBDEE8 95.02%)', willChange: 'transform' }}
|
||||
>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ scale: [1, 0.95, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 13 }}
|
||||
className='w-[824px] h-[824px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
||||
bg-[linear-gradient(180deg,#EBF0FC_4.52%,#EBF0FC_95.02%)]
|
||||
dark:bg-[linear-gradient(180deg,#111827_4.52%,#1E293B_95.02%)]'
|
||||
style={{ willChange: 'transform' }}
|
||||
></motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ scale: [1, 0.95, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 13 }}
|
||||
className='size-[824px] rounded-full fixed top-1/2 left-1/2 -translate-1/2'
|
||||
style={{ background: 'linear-gradient(180deg, #EBF0FC 4.52%, #EBF0FC 95.02%)', willChange: 'transform' }}
|
||||
>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ scale: [0.9, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 4, repeatType: 'reverse' }}
|
||||
className='w-[617px] h-[617px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
||||
bg-[linear-gradient(180deg,#E6EBFA_0%,#DFE2EC_100%)]
|
||||
dark:bg-[linear-gradient(180deg,#1E293B_0%,#111827_100%)]'
|
||||
style={{ willChange: 'transform' }}
|
||||
></motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ scale: [0.9, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 4, repeatType: 'reverse' }}
|
||||
className='size-[617px] rounded-full fixed top-1/2 left-1/2 -translate-1/2'
|
||||
style={{ background: 'linear-gradient(180deg, #E6EBFA 0%, #DFE2EC 100%)', willChange: 'transform' }}
|
||||
>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ scale: [1.02, 0.95] }}
|
||||
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
|
||||
className='w-[425px] h-[425px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
||||
bg-[linear-gradient(180deg,#E1E5EF_0%,#D8DDE8_60.2%,#D2D7E3_100%)]
|
||||
dark:bg-[linear-gradient(180deg,#1F2937_0%,#374151_60.2%,#1E293B_100%)]'
|
||||
style={{ willChange: 'transform' }}
|
||||
></motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ scale: [1.02, 0.95] }}
|
||||
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
|
||||
className='size-[425px] rounded-full fixed top-1/2 left-1/2 -translate-1/2'
|
||||
style={{ background: 'linear-gradient(180deg, #E1E5EF 0%, #D8DDE8 60.2%, #D2D7E3 100%)', willChange: 'transform' }}
|
||||
>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ scale: [1.05, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
|
||||
className='w-[292px] h-[292px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
||||
bg-[linear-gradient(180deg,#EAEDF5_0%,#E4E7F0_46.52%,#DDDFEB_100%)]
|
||||
dark:bg-[linear-gradient(180deg,#111827_0%,#1E293B_46.52%,#1F2937_100%)]'
|
||||
style={{ willChange: 'transform' }}
|
||||
></motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ scale: [1.05, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
|
||||
className='size-[292px] rounded-full fixed top-1/2 left-1/2 -translate-1/2'
|
||||
style={{ background: 'linear-gradient(180deg, #EAEDF5 0%, #E4E7F0 46.52%, #DDDFEB 100%)', willChange: 'transform' }}
|
||||
>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.05] }}
|
||||
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
|
||||
className='w-[174px] h-[174px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
||||
bg-[linear-gradient(180deg,#F3F7FF_0%,#F2F5FC_100%)]
|
||||
dark:bg-[linear-gradient(180deg,#1E293B_0%,#111827_100%)]'
|
||||
style={{ willChange: 'transform' }}
|
||||
></motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.05] }}
|
||||
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
|
||||
<motion.div
|
||||
animate={{ scale: [0, 1.2], opacity: [1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
className='h-full w-[100dvh] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
||||
outline outline-solid outline-blue-400'
|
||||
style={{ willChange: 'transform, opacity' }}
|
||||
></motion.div>
|
||||
|
||||
className='size-[174px] rounded-full fixed top-1/2 left-1/2 -translate-1/2'
|
||||
style={{ background: 'linear-gradient(180deg, #F3F7FF 0%, #F2F5FC 100%)', willChange: 'transform' }}
|
||||
>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ scale: [0, 1.2], opacity: [1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
|
||||
className='h-full w-dvh rounded-full fixed top-1/2 left-1/2 -translate-1/2 outline outline-solid outline-blue-400'
|
||||
style={{ willChange: 'transform, opacity' }}
|
||||
>
|
||||
</motion.div>
|
||||
|
||||
<div>
|
||||
{positions.length === users.length &&
|
||||
users.map((v, i) => (
|
||||
<Link
|
||||
href={`${v.id}`}
|
||||
prefetch={false}
|
||||
key={v.id}
|
||||
style={{ position: 'absolute', top: positions[i].top, left: positions[i].left }}
|
||||
className="justify-items-center"
|
||||
>
|
||||
<Image height={46} width={46} alt="user avatar" src={v.avatar} />
|
||||
<h6 className="mt-2 text-xs font-medium">{v.name}</h6>
|
||||
<div className="mt-[3px] text-xs2 font-medium bg-disabled-text text-accent rounded-[5px] px-[5px] py-[3px]">
|
||||
میز {v.table}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
<div>
|
||||
{positions.length === users.length &&
|
||||
users.map((v, i) => (
|
||||
<Link
|
||||
href={`${v.id}`}
|
||||
prefetch={false}
|
||||
key={v.id}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: positions[i].top,
|
||||
left: positions[i].left
|
||||
}}
|
||||
className='justify-items-center'
|
||||
>
|
||||
<Image
|
||||
height={46}
|
||||
width={46}
|
||||
alt='user avatar'
|
||||
src={v.avatar}
|
||||
/>
|
||||
<h6 className='mt-2 text-xs font-medium'>{v.name}</h6>
|
||||
<div className='mt-[3px] text-xs2 font-medium bg-disabled-text dark:bg-neutral-800 text-accent rounded-[5px] px-[5px] py-[3px]'>
|
||||
میز {v.table}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatNearby;
|
||||
export default ChatNearby
|
||||
|
||||
+246
-216
@@ -1,235 +1,265 @@
|
||||
'use client';
|
||||
'use client'
|
||||
|
||||
import SearchBox from '@/components/input/SearchBox';
|
||||
import { ScrollArea } from '@/components/ui/scrollarea';
|
||||
import { Radar2, Trash } from 'iconsax-react';
|
||||
import { Check, CheckCheck } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import SearchBox from '@/components/input/SearchBox'
|
||||
import { ScrollArea } from '@/components/ui/scrollarea'
|
||||
import { Radar2, Trash } from 'iconsax-react'
|
||||
import { Check, CheckCheck } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LongPressCallbackMeta, LongPressReactEvents, useLongPress } from 'use-long-press'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import useToggle from '@/hooks/helpers/useToggle';
|
||||
import Prompt from '@/components/utils/Prompt';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
LongPressCallbackMeta,
|
||||
LongPressReactEvents,
|
||||
useLongPress
|
||||
} from 'use-long-press'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useToggle from '@/hooks/helpers/useToggle'
|
||||
import Prompt from '@/components/utils/Prompt'
|
||||
|
||||
type Props = object
|
||||
|
||||
type ChatEntryModel = {
|
||||
id: string,
|
||||
username: string,
|
||||
avatarUrl: string,
|
||||
update?: {
|
||||
time: string,
|
||||
content: string,
|
||||
count?: number,
|
||||
seen?: boolean
|
||||
}
|
||||
id: string
|
||||
username: string
|
||||
avatarUrl: string
|
||||
update?: {
|
||||
time: string
|
||||
content: string
|
||||
count?: number
|
||||
seen?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
function ChatIndex({ }: Props) {
|
||||
const { t } = useTranslation('chat');
|
||||
const { t: tDeleteModal } = useTranslation('common', {
|
||||
keyPrefix: 'DeleteChatModal'
|
||||
});
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedChats, setSelectedChats] = useState<Array<string>>([]);
|
||||
const router = useRouter();
|
||||
const { state: deleteModal, toggle: toggleDeleteModal } = useToggle();
|
||||
function ChatIndex ({}: Props) {
|
||||
const { t } = useTranslation('chat')
|
||||
const { t: tDeleteModal } = useTranslation('common', {
|
||||
keyPrefix: 'DeleteChatModal'
|
||||
})
|
||||
const [search, setSearch] = useState('')
|
||||
const [selectedChats, setSelectedChats] = useState<Array<string>>([])
|
||||
const router = useRouter()
|
||||
const { state: deleteModal, toggle: toggleDeleteModal } = useToggle()
|
||||
|
||||
const [chatsList, setChatList] = useState<Array<ChatEntryModel>>([
|
||||
{
|
||||
id: '0',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
count: 2,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
count: 2,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: false
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
const toggleDeleteModalVisiblity = (e?: React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
if (deleteModal) {
|
||||
setSelectedChats([]);
|
||||
}
|
||||
toggleDeleteModal()
|
||||
};
|
||||
|
||||
const deleteSelectedChats = () => {
|
||||
if (setSelectedChats.length <= 0) return;
|
||||
setChatList((prev) => {
|
||||
return prev.filter(x => !selectedChats.includes(x.id));
|
||||
})
|
||||
toggleDeleteModalVisiblity();
|
||||
const [chatsList, setChatList] = useState<Array<ChatEntryModel>>([
|
||||
{
|
||||
id: '0',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
count: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
count: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
username: 'علی مصلحی',
|
||||
avatarUrl: '/assets/images/user-avatar.png',
|
||||
update: {
|
||||
time: '12:03',
|
||||
content: 'چه خبر',
|
||||
seen: false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const longPressCallback = useCallback((e: LongPressReactEvents<Element>, ctx: LongPressCallbackMeta<unknown>) => {
|
||||
e.preventDefault();
|
||||
if (!ctx?.context) return;
|
||||
const id = String(ctx.context);
|
||||
setSelectedChats((prev) => {
|
||||
if (prev.includes(id)) return prev.filter(x => x !== id);
|
||||
return [...prev, id]
|
||||
const toggleDeleteModalVisiblity = (e?: React.MouseEvent) => {
|
||||
e?.preventDefault()
|
||||
e?.stopPropagation()
|
||||
if (deleteModal) {
|
||||
setSelectedChats([])
|
||||
}
|
||||
toggleDeleteModal()
|
||||
}
|
||||
|
||||
const deleteSelectedChats = () => {
|
||||
if (setSelectedChats.length <= 0) return
|
||||
setChatList(prev => {
|
||||
return prev.filter(x => !selectedChats.includes(x.id))
|
||||
})
|
||||
toggleDeleteModalVisiblity()
|
||||
}
|
||||
|
||||
const longPressCallback = useCallback(
|
||||
(e: LongPressReactEvents<Element>, ctx: LongPressCallbackMeta<unknown>) => {
|
||||
e.preventDefault()
|
||||
if (!ctx?.context) return
|
||||
const id = String(ctx.context)
|
||||
setSelectedChats(prev => {
|
||||
if (prev.includes(id)) return prev.filter(x => x !== id)
|
||||
return [...prev, id]
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const longPressCancelledCallback = useCallback(
|
||||
(id: string, reason?: string) => {
|
||||
if (reason && reason !== 'cancelled-by-release') return
|
||||
if (selectedChats.length > 0) {
|
||||
setSelectedChats(prev => {
|
||||
if (prev.includes(id)) return prev.filter(x => x !== id)
|
||||
return [...prev, id]
|
||||
})
|
||||
}, [])
|
||||
} else {
|
||||
router.push(`chat/${id}`)
|
||||
}
|
||||
},
|
||||
[router, selectedChats.length]
|
||||
)
|
||||
|
||||
const longPressCancelledCallback = useCallback((id: string, reason?: string) => {
|
||||
if (reason && reason !== 'cancelled-by-release') return;
|
||||
if (selectedChats.length > 0) {
|
||||
setSelectedChats((prev) => {
|
||||
if (prev.includes(id)) return prev.filter(x => x !== id);
|
||||
return [...prev, id]
|
||||
})
|
||||
} else {
|
||||
router.push(`chat/${id}`)
|
||||
}
|
||||
}, [router, selectedChats.length])
|
||||
const longPress = useLongPress(longPressCallback, {
|
||||
onCancel: (event, ctx) =>
|
||||
longPressCancelledCallback(String(ctx.context), String(ctx.reason)),
|
||||
threshold: 500, // In milliseconds
|
||||
captureEvent: true, // Event won't get cleared after React finish processing it
|
||||
cancelOnMovement: 100, // Square side size (in pixels) inside which movement won't cancel long press
|
||||
cancelOutsideElement: true // Cancel long press when moved mouse / pointer outside element while pressing
|
||||
})
|
||||
|
||||
const longPress = useLongPress(longPressCallback, {
|
||||
onCancel: (event, ctx) => longPressCancelledCallback(String(ctx.context), String(ctx.reason)),
|
||||
threshold: 500, // In milliseconds
|
||||
captureEvent: true, // Event won't get cleared after React finish processing it
|
||||
cancelOnMovement: 100, // Square side size (in pixels) inside which movement won't cancel long press
|
||||
cancelOutsideElement: true, // Cancel long press when moved mouse / pointer outside element while pressing
|
||||
});
|
||||
return (
|
||||
<div className='pt-8 mb-8'>
|
||||
<section className='flex justify-between items-center gap-x-4'>
|
||||
<SearchBox
|
||||
placeholder={t('InputSearch.Placeholder')}
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
return (
|
||||
<div className='pt-8 mb-8'>
|
||||
<section className='flex justify-between items-center gap-x-4'>
|
||||
<SearchBox placeholder={t('InputSearch.Placeholder')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
|
||||
{selectedChats.length > 0 ?
|
||||
<span onClick={toggleDeleteModal} className='bg-container p-2 rounded-xl active:bg-gray-50'>
|
||||
<Trash size={24} className='stroke-primary' />
|
||||
</span>
|
||||
:
|
||||
<Link href={'chat/nearby'} className='bg-container p-2 rounded-xl active:bg-gray-50'>
|
||||
<Radar2 size={24} className='stroke-primary' />
|
||||
</Link>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ScrollArea dir='rtl' className='w-full h-full'>
|
||||
<ul className='pt-4'>
|
||||
{chatsList.map((chat, i) => {
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
{...longPress(chat.id)}
|
||||
className='select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 bg-background rounded-xl'
|
||||
>
|
||||
<div className='size-[49px] min-w-[49px]'>
|
||||
{
|
||||
selectedChats.includes(chat.id) ?
|
||||
<div className='size-[49px] relative bg-primary rounded-full'>
|
||||
<Check className='absolute top-1/2 left-1/2 -translate-1/2 stroke-white' />
|
||||
</div>
|
||||
:
|
||||
<Image
|
||||
src={chat.avatarUrl}
|
||||
height={49}
|
||||
width={49}
|
||||
|
||||
alt={`${chat.username}'s avatar`}
|
||||
unoptimized
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col justify-between w-full gap-2'>
|
||||
<h5 className='text-sm2 font-medium'>{chat.username}</h5>
|
||||
<p className='text-xs'>{chat.update?.content}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col justify-between items-end gap-2'>
|
||||
<span className='text-sm2 text-disabled2'>{chat.update?.time}</span>
|
||||
{chat.update?.count !== undefined && chat.update?.count > 0 ?
|
||||
<span className='text-xs w-5 h-5 relative bg-foreground text-white rounded-full'>
|
||||
<span className='absolute top-1/2 left-1/2 -translate-1/2 pt-1'>{chat.update?.count}</span>
|
||||
</span>
|
||||
:
|
||||
chat.update?.seen !== undefined &&
|
||||
chat.update?.seen ?
|
||||
<CheckCheck className='stroke-foreground' size={16} /> :
|
||||
<Check className='stroke-foreground' size={16} />
|
||||
}
|
||||
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</section>
|
||||
|
||||
<Prompt
|
||||
title={tDeleteModal('Heading')}
|
||||
description={tDeleteModal('Description').replace('{count}', String(selectedChats.length))}
|
||||
textConfirm={tDeleteModal('ButtonOk')}
|
||||
textCancel={tDeleteModal('ButtonCancel')}
|
||||
onConfirm={deleteSelectedChats}
|
||||
onCancel={toggleDeleteModalVisiblity}
|
||||
onClick={toggleDeleteModalVisiblity}
|
||||
visible={deleteModal}
|
||||
{selectedChats.length > 0 ? (
|
||||
<span
|
||||
onClick={toggleDeleteModal}
|
||||
className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
|
||||
>
|
||||
<Trash
|
||||
size={24}
|
||||
className='stroke-primary dark:stroke-disabled-text'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={'chat/nearby'}
|
||||
className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
|
||||
>
|
||||
<Radar2
|
||||
size={24}
|
||||
className='stroke-primary dark:stroke-disabled-text'
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ScrollArea dir='rtl' className='w-full h-full'>
|
||||
<ul className='pt-4'>
|
||||
{chatsList.map((chat, i) => {
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
{...longPress(chat.id)}
|
||||
className='select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 bg-background rounded-xl'
|
||||
>
|
||||
<div className='size-[49px] min-w-[49px]'>
|
||||
{selectedChats.includes(chat.id) ? (
|
||||
<div className='size-[49px] relative bg-primary rounded-full'>
|
||||
<Check className='absolute top-1/2 left-1/2 -translate-1/2 stroke-white' />
|
||||
</div>
|
||||
) : (
|
||||
<Image
|
||||
src={chat.avatarUrl}
|
||||
height={49}
|
||||
width={49}
|
||||
alt={`${chat.username}'s avatar`}
|
||||
unoptimized
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col justify-between w-full gap-2'>
|
||||
<h5 className='text-sm2 font-medium'>{chat.username}</h5>
|
||||
<p className='text-xs'>{chat.update?.content}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col justify-between items-end gap-2'>
|
||||
<span className='text-sm2 text-disabled2'>
|
||||
{chat.update?.time}
|
||||
</span>
|
||||
{chat.update?.count !== undefined &&
|
||||
chat.update?.count > 0 ? (
|
||||
<span className='text-xs w-5 h-5 relative bg-foreground dark:bg-neutral-700 text-white rounded-full'>
|
||||
<span className='absolute top-1/2 left-1/2 -translate-1/2 pt-1'>
|
||||
{chat.update?.count}
|
||||
</span>
|
||||
</span>
|
||||
) : chat.update?.seen !== undefined && chat.update?.seen ? (
|
||||
<CheckCheck className='stroke-foreground' size={16} />
|
||||
) : (
|
||||
<Check className='stroke-foreground' size={16} />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</section>
|
||||
|
||||
<Prompt
|
||||
title={tDeleteModal('Heading')}
|
||||
description={tDeleteModal('Description').replace(
|
||||
'{count}',
|
||||
String(selectedChats.length)
|
||||
)}
|
||||
textConfirm={tDeleteModal('ButtonOk')}
|
||||
textCancel={tDeleteModal('ButtonCancel')}
|
||||
onConfirm={deleteSelectedChats}
|
||||
onCancel={toggleDeleteModalVisiblity}
|
||||
onClick={toggleDeleteModalVisiblity}
|
||||
visible={deleteModal}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatIndex
|
||||
Reference in New Issue
Block a user