report bug
This commit is contained in:
@@ -8,4 +8,6 @@ VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
||||
VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
|
||||
VITE_CONSOLE_URL = 'https://console.danakcorp.com'
|
||||
|
||||
VITE_WORKSPACE_ID = 'workspace_id'
|
||||
VITE_WORKSPACE_ID = 'workspace_id'
|
||||
|
||||
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
||||
|
||||
@@ -16,6 +16,7 @@ import { refreshToken } from './pages/auth/service/Service';
|
||||
import BuySpace from './shared/components/BuySpace'
|
||||
import useConvertNumbers from './hooks/useConvertNumbers'
|
||||
import CheckWorkSpace from './components/CheckWorkSpace'
|
||||
import ReportBug from './pages/guide/ReportBug';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -105,6 +106,7 @@ const App: FC = () => {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CheckWorkSpace />
|
||||
<Main />
|
||||
<ReportBug />
|
||||
<ToastContainer />
|
||||
<BuySpace />
|
||||
</QueryClientProvider>
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 21 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 29 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 33 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 29 KiB |
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { More } from 'iconsax-react';
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { More } from 'iconsax-react'
|
||||
|
||||
export interface RowActionItem {
|
||||
label: string;
|
||||
@@ -12,72 +12,70 @@ export interface RowActionItem {
|
||||
interface RowActionsDropdownProps {
|
||||
actions: RowActionItem[];
|
||||
className?: string;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, className = '' }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, className = '', trigger }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 })
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
setIsOpen(false)
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleToggle = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isOpen && buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const dropdownWidth = isMobile ? 140 : 150;
|
||||
const dropdownHeight = actions.length * (isMobile ? 36 : 40); // تخمین ارتفاع منو
|
||||
|
||||
let left = rect.left + window.scrollX - dropdownWidth + rect.width;
|
||||
let top = rect.bottom + window.scrollY;
|
||||
|
||||
// بررسی اینکه منو از سمت راست خارج نشود
|
||||
if (left + dropdownWidth > window.innerWidth) {
|
||||
left = rect.left + window.scrollX - dropdownWidth;
|
||||
}
|
||||
|
||||
// بررسی اینکه منو از سمت چپ خارج نشود
|
||||
if (left < 0) {
|
||||
left = isMobile ? 20 : 40;
|
||||
}
|
||||
|
||||
// بررسی اینکه منو از پایین خارج نشود
|
||||
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
|
||||
top = rect.top + window.scrollY - dropdownHeight;
|
||||
}
|
||||
|
||||
setPosition({ top, left });
|
||||
}
|
||||
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleToggle = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (!isOpen && buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect()
|
||||
const isMobile = window.innerWidth < 768
|
||||
const dropdownWidth = isMobile ? 140 : 150
|
||||
const dropdownHeight = actions.length * (isMobile ? 36 : 40)
|
||||
|
||||
let left = rect.left + window.scrollX - dropdownWidth + rect.width
|
||||
let top = rect.bottom + window.scrollY
|
||||
|
||||
if (left + dropdownWidth > window.innerWidth) {
|
||||
left = rect.left + window.scrollX - dropdownWidth
|
||||
}
|
||||
|
||||
if (left < 0) {
|
||||
left = isMobile ? 20 : 40
|
||||
}
|
||||
|
||||
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
|
||||
top = rect.top + window.scrollY - dropdownHeight
|
||||
}
|
||||
|
||||
setPosition({ top, left })
|
||||
}
|
||||
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
const handleActionClick = (action: RowActionItem) => {
|
||||
action.onClick();
|
||||
setIsOpen(false);
|
||||
};
|
||||
action.onClick()
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const renderDropdown = () => {
|
||||
if (!isOpen) return null;
|
||||
if (!isOpen) return null
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="fixed py-1 md:py-2 bg-white rounded-xl md:rounded-2xl shadow-lg z-[9999] min-w-[140px] md:min-w-[150px]"
|
||||
className="fixed py-3 md:py-3 bg-white dark:bg-[#252526] rounded-xl md:rounded-2xl shadow-lg z-[5] min-w-[140px] md:min-w-[150px] border border-gray-200 dark:border-[#3e3e42]"
|
||||
style={{
|
||||
top: `${position.top}px`,
|
||||
left: `${position.left}px`,
|
||||
@@ -87,31 +85,34 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, classN
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleActionClick(action)}
|
||||
className={`w-full text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 ${index === 0 ? 'rounded-t-lg' : ''
|
||||
} ${action.className || ''}`}
|
||||
className={`w-full mt-1 text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-[#2d2d30] ${index === 0 ? 'rounded-t-lg' : ''} ${action.className || ''}`}
|
||||
>
|
||||
{action.icon && <span className="ml-2">{action.icon}</span>}
|
||||
{action.label}
|
||||
<span className={`${action.label === 'حذف' ? 'text-red-500' : ''} dark:text-gray-200`}>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={handleToggle}
|
||||
className="p-1 hover:bg-gray-100 rounded-full transition-colors"
|
||||
className="mt-2 hover:bg-gray-100 dark:hover:bg-[#2d2d30] rounded-full transition-colors"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<More size={14} color="black" className="rotate-90" />
|
||||
{trigger ? trigger : (
|
||||
<More size={16} color='black' className="rotate-90" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{renderDropdown()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default RowActionsDropdown;
|
||||
export default RowActionsDropdown
|
||||
@@ -267,3 +267,8 @@ textarea::placeholder {
|
||||
|
||||
backdrop-filter: blur(44px);
|
||||
}
|
||||
|
||||
.filterBlue {
|
||||
filter: brightness(0) saturate(100%) invert(9%) sepia(100%) saturate(7246%)
|
||||
hue-rotate(236deg) brightness(111%) contrast(112%);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import DefaulModal from '@/components/DefaulModal'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import Button from '@/components/Button'
|
||||
import { useSharedStore } from '@/shared/store/sharedStore'
|
||||
import { useSendFeedback } from './hooks/useFeedbackData'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { ErrorType } from '@/helpers/types'
|
||||
|
||||
|
||||
// Import images
|
||||
import sad1 from '@/assets/images/sad1.svg'
|
||||
import sad2 from '@/assets/images/sad2.svg'
|
||||
import sad3 from '@/assets/images/sad3.svg'
|
||||
import smile from '@/assets/images/smile.svg'
|
||||
import smile2 from '@/assets/images/smile2.svg'
|
||||
import smile3 from '@/assets/images/smile3.svg'
|
||||
import { useGetProfile } from '../profile/hooks/useProfileData'
|
||||
|
||||
const ReportBug: React.FC = () => {
|
||||
const { openReportBug, setOpenReportBug } = useSharedStore()
|
||||
const [description, setDescription] = useState('')
|
||||
const [feeling, setFeeling] = useState<string | null>(null)
|
||||
const { data } = useGetProfile()
|
||||
const { mutate: sendReportBug, isPending } = useSendFeedback()
|
||||
|
||||
const isValid = useMemo(() => description.trim().length > 0, [description])
|
||||
|
||||
const getRatingFromFeeling = (feeling: string | null): number => {
|
||||
if (!feeling) return 0
|
||||
|
||||
const feelingMap: Record<string, number> = {
|
||||
'angry': 1, // sad1
|
||||
'crying': 2, // sad2
|
||||
'sad': 3, // sad3
|
||||
'neutral': 4, // smile
|
||||
'smile': 6, // smile2
|
||||
'love': 5, // smile3
|
||||
}
|
||||
|
||||
return feelingMap[feeling] || 0
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!isValid) return
|
||||
|
||||
sendReportBug({
|
||||
content: description,
|
||||
rating: getRatingFromFeeling(feeling),
|
||||
serviceId: import.meta.env.VITE_SERVICE_ID,
|
||||
email: data?.data?.user?.emailAddress,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast('گزارش با موفقیت ارسال شد', 'success')
|
||||
setDescription('')
|
||||
setFeeling(null)
|
||||
setOpenReportBug(false)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const feelings = [
|
||||
{ id: 'neutral', image: smile, alt: 'خنثی' },
|
||||
{ id: 'smile', image: smile2, alt: 'راضی' },
|
||||
{ id: 'love', image: smile3, alt: 'راضی' },
|
||||
{ id: 'angry', image: sad1, alt: 'ناراضی' },
|
||||
{ id: 'crying', image: sad2, alt: 'ناراضی' },
|
||||
{ id: 'sad', image: sad3, alt: 'ناراضی' },
|
||||
]
|
||||
|
||||
return (
|
||||
<DefaulModal open={openReportBug} close={() => setOpenReportBug(false)} isHeader title_header='گزارش اشکالات' width={500}>
|
||||
<div className='space-y-6 mt-4'>
|
||||
<div className='text-xs text-muted-foreground leading-5'>
|
||||
لطفاً در صورت مشاهده هرگونه اشکال یا باگ در سایت، از طریق این فرم گزارش دهید. همچنین میتوانید بازخورد، نظر یا پیشنهادات خود را درباره سایت با ما به اشتراک بگذارید. اطلاعات دقیق شما به ما کمک میکند مشکلات را سریعتر برطرف کنیم و کیفیت سایت را بهبود بخشیم.
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
label='متن گزارش'
|
||||
placeholder='اینجا بنویسید...'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className='flex justify-start'>
|
||||
<div className='flex w-full justify-between gap-4'>
|
||||
{feelings.map((f, idx) => (
|
||||
<div key={f.id} className='flex flex-col items-center gap-1'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setFeeling(f.id)}
|
||||
className={`p-1 rounded-lg transition-all`}
|
||||
>
|
||||
<img
|
||||
src={f.image}
|
||||
alt={f.alt}
|
||||
className={`w-8 object-contain ${feeling === f.id ? 'filterBlue' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{idx === 0 && (
|
||||
<div className='text-xs mt-2 text-muted-foreground'>راضی</div>
|
||||
)}
|
||||
{idx === feelings.length - 1 && (
|
||||
<div className='text-xs mt-2 text-muted-foreground'>ناراضی</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='text-[11px] text-muted-foreground leading-6'>
|
||||
با ادامه و ارسال، شما تأیید میکنید که «دیمیل» میتواند پاسخها و اطلاعات حساب شما را بهمنظور بهبود خدمات جمعآوری و استفاده کند. برای جزئیات بیشتر لطفاً به <a target='_blank' href='https://danakcorp.com/conditions' className='text-blue-500'>قوانین و شرایط و سیاست حریم خصوصی</a> مراجعه کنید.
|
||||
</div>
|
||||
|
||||
<div className='w-full flex justify-end'>
|
||||
<Button onClick={handleSubmit} disabled={!isValid} loading={isPending} className='px-10 w-fit'>ارسال</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReportBug
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { sendFeedback } from "../service/Service";
|
||||
import { FeedbackType } from "../types/Types";
|
||||
|
||||
export const useSendFeedback = () => {
|
||||
return useMutation({
|
||||
mutationFn: (params: FeedbackType) => sendFeedback(params),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import axios from "@/config/axiosDanak";
|
||||
import { FeedbackType } from "../types/Types";
|
||||
|
||||
export const sendFeedback = async (params: FeedbackType) => {
|
||||
const { data } = await axios.post("/danak-services/feedback", params);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export type FeedbackType = {
|
||||
content: string;
|
||||
rating: number;
|
||||
serviceId: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
};
|
||||
+21
-2
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
// import Input from '../components/Input'
|
||||
import { ArrowDown2, CloseCircle, Element4, HambergerMenu, Logout } from 'iconsax-react'
|
||||
import { ArrowDown2, CloseCircle, Element4, HambergerMenu, Logout, MessageQuestion } from 'iconsax-react'
|
||||
import AvatarImage from '../assets/images/avatar_image.png'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
@@ -10,13 +10,14 @@ import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
||||
import { removeRefreshToken, removeToken } from '../config/func'
|
||||
import { clx } from '../helpers/utils'
|
||||
import RowActionsDropdown from '@/components/RowActionsDropdown'
|
||||
|
||||
const Header: FC = () => {
|
||||
|
||||
const location = useLocation();
|
||||
const [popoverKey, setPopoverKey] = useState(0);
|
||||
const { t } = useTranslation('global')
|
||||
const { setOpenSidebar, openSidebar, hasSubMenu } = useSharedStore()
|
||||
const { setOpenSidebar, openSidebar, hasSubMenu, setOpenReportBug } = useSharedStore()
|
||||
const { data } = useGetProfile()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,6 +55,24 @@ const Header: FC = () => {
|
||||
{/* <Link className='xl:hidden' to={Pages.wallet}>
|
||||
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
|
||||
</Link> */}
|
||||
|
||||
<RowActionsDropdown
|
||||
actions={[
|
||||
{
|
||||
label: 'راهنما',
|
||||
onClick: () => window.open(
|
||||
import.meta.env.VITE_HELP_URL + `/${import.meta.env.VITE_SERVICE_ID}`,
|
||||
'_blank'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'گزارش اشکالات',
|
||||
onClick: () => setOpenReportBug(true),
|
||||
},
|
||||
]}
|
||||
trigger={<MessageQuestion size={20} color='black' />}
|
||||
/>
|
||||
|
||||
<Notifications />
|
||||
{
|
||||
data && (
|
||||
|
||||
@@ -10,4 +10,6 @@ export const useSharedStore = create<SharedStoreType>((set) => ({
|
||||
setOpenBuySpace: (value) => set({ openBuySpace: value }),
|
||||
hasSubMenu: false,
|
||||
setSubtMenu: (value: boolean) => set({ hasSubMenu: value }),
|
||||
openReportBug: false,
|
||||
setOpenReportBug: (value) => set({ openReportBug: value }),
|
||||
}));
|
||||
|
||||
@@ -7,4 +7,6 @@ export type SharedStoreType = {
|
||||
setOpenBuySpace: (value: boolean) => void;
|
||||
hasSubMenu: boolean;
|
||||
setSubtMenu: (value: boolean) => void;
|
||||
openReportBug: boolean;
|
||||
setOpenReportBug: (value: boolean) => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user