0.9.0.0
This commit is contained in:
+193
-270
@@ -29,6 +29,7 @@ import { Textarea } from "@/components/ui/textarea"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import type { FamilyPhoto } from "@/types/family"
|
||||
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
||||
import { PhotoCard } from "@/components/dashboard/photo-card"
|
||||
|
||||
// 动态导入统计图表组件(减少初始加载体积)
|
||||
const StatisticsCharts = dynamic(
|
||||
@@ -64,6 +65,71 @@ const isVideoFile = (url: string) => {
|
||||
return videoExtensions.some(ext => url.toLowerCase().endsWith(ext))
|
||||
}
|
||||
|
||||
// 计算事件日期的辅助函数
|
||||
const calculateEventDate = (
|
||||
dateStr: string,
|
||||
isLunar: boolean,
|
||||
now: Date
|
||||
): { date: Date; lunarDisplay?: string } | null => {
|
||||
const date = new Date(dateStr)
|
||||
let eventDate: Date
|
||||
let lunarDisplay: string | undefined
|
||||
|
||||
if (isLunar) {
|
||||
const lunarInfo = solar2lunar(date)
|
||||
if (lunarInfo) {
|
||||
const thisYearLunar = lunar2solar(
|
||||
now.getFullYear(),
|
||||
lunarInfo.lunarMonth,
|
||||
lunarInfo.lunarDay,
|
||||
lunarInfo.isLeap
|
||||
)
|
||||
if (thisYearLunar) {
|
||||
eventDate = thisYearLunar
|
||||
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
||||
} else {
|
||||
eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
} else {
|
||||
eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
} else {
|
||||
eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
|
||||
return { date: eventDate, lunarDisplay }
|
||||
}
|
||||
|
||||
// 创建即将到来的事件
|
||||
const createUpcomingEvent = (
|
||||
member: any,
|
||||
type: 'birth' | 'death',
|
||||
dateStr: string,
|
||||
isLunar: boolean,
|
||||
now: Date,
|
||||
threeMonthsLater: Date
|
||||
) => {
|
||||
const result = calculateEventDate(dateStr, isLunar, now)
|
||||
if (!result) return null
|
||||
|
||||
const { date, lunarDisplay } = result
|
||||
|
||||
if (date >= now && date <= threeMonthsLater) {
|
||||
return {
|
||||
member,
|
||||
type,
|
||||
date,
|
||||
originalDate: dateStr,
|
||||
isLunar,
|
||||
lunarDisplay,
|
||||
month: date.getMonth() + 1,
|
||||
day: date.getDate()
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { treeData, isLoading, currentTree, updateMember } = useFamily()
|
||||
const { data: session } = useSession()
|
||||
@@ -120,39 +186,59 @@ export default function DashboardPage() {
|
||||
return () => controller.abort()
|
||||
}, [currentTree?.id, session?.user?.id])
|
||||
|
||||
// 计算统计数据
|
||||
// 计算统计数据 - 单次遍历优化
|
||||
const stats = useMemo(() => {
|
||||
const members = Object.values(treeData.members)
|
||||
const totalMembers = members.length
|
||||
|
||||
// 在世和已故人数
|
||||
const livingMembers = members.filter(m => !m.deathDate).length
|
||||
const deceasedMembers = members.filter(m => m.deathDate).length
|
||||
// 单次遍历计算所有统计数据
|
||||
let livingMembers = 0
|
||||
let deceasedMembers = 0
|
||||
let maleCount = 0
|
||||
let femaleCount = 0
|
||||
let maxGeneration = 0
|
||||
const birthYears: number[] = []
|
||||
let totalAge = 0
|
||||
let deceasedWithAgeCount = 0
|
||||
|
||||
// 性别统计
|
||||
const maleCount = members.filter(m => m.gender === 'MALE').length
|
||||
const femaleCount = members.filter(m => m.gender === 'FEMALE').length
|
||||
members.forEach(m => {
|
||||
// 生死统计
|
||||
if (m.deathDate) {
|
||||
deceasedMembers++
|
||||
} else {
|
||||
livingMembers++
|
||||
}
|
||||
|
||||
// 性别统计
|
||||
if (m.gender === 'MALE') maleCount++
|
||||
else if (m.gender === 'FEMALE') femaleCount++
|
||||
|
||||
// 代数统计
|
||||
if (m.generation && m.generation > maxGeneration) {
|
||||
maxGeneration = m.generation
|
||||
}
|
||||
|
||||
// 出生年份和寿命统计
|
||||
if (m.birthDate) {
|
||||
const birthYear = new Date(m.birthDate).getFullYear()
|
||||
birthYears.push(birthYear)
|
||||
|
||||
if (m.deathDate) {
|
||||
const deathYear = new Date(m.deathDate).getFullYear()
|
||||
totalAge += deathYear - birthYear
|
||||
deceasedWithAgeCount++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 计算最大代数(处理空数组的情况)
|
||||
const generations = members.map(m => m.generation || 0)
|
||||
const maxGeneration = generations.length > 0 ? Math.max(...generations) : 0
|
||||
|
||||
// 计算最早出生年份
|
||||
const birthYears = members
|
||||
.map(m => m.birthDate ? new Date(m.birthDate).getFullYear() : null)
|
||||
.filter(y => y !== null) as number[]
|
||||
const earliestYear = birthYears.length > 0 ? Math.min(...birthYears) : new Date().getFullYear()
|
||||
const yearsSpan = birthYears.length > 0 ? new Date().getFullYear() - earliestYear : 0
|
||||
|
||||
// 计算平均寿命(只统计已故成员)
|
||||
const deceasedWithAge = members.filter(m => m.birthDate && m.deathDate)
|
||||
const totalAge = deceasedWithAge.reduce((sum, m) => {
|
||||
const birthYear = new Date(m.birthDate!).getFullYear()
|
||||
const deathYear = new Date(m.deathDate!).getFullYear()
|
||||
return sum + (deathYear - birthYear)
|
||||
}, 0)
|
||||
const averageLifespan = deceasedWithAge.length > 0
|
||||
? Math.round(totalAge / deceasedWithAge.length)
|
||||
const earliestYear = birthYears.length > 0
|
||||
? Math.min(...birthYears)
|
||||
: new Date().getFullYear()
|
||||
const yearsSpan = birthYears.length > 0
|
||||
? new Date().getFullYear() - earliestYear
|
||||
: 0
|
||||
const averageLifespan = deceasedWithAgeCount > 0
|
||||
? Math.round(totalAge / deceasedWithAgeCount)
|
||||
: 0
|
||||
|
||||
return {
|
||||
@@ -166,14 +252,14 @@ export default function DashboardPage() {
|
||||
earliestYear,
|
||||
averageLifespan
|
||||
}
|
||||
}, [treeData])
|
||||
}, [treeData.members])
|
||||
|
||||
// 获取最近的成员(按ID排序,取最新的5个)
|
||||
const recentMembers = useMemo(() => {
|
||||
return Object.values(treeData.members)
|
||||
.sort((a, b) => parseInt(b.id) - parseInt(a.id))
|
||||
.slice(0, 5)
|
||||
}, [treeData])
|
||||
}, [treeData.members])
|
||||
|
||||
// 收集所有成员的照片
|
||||
const isOwner = session && currentTree?.ownerId === session.user?.id
|
||||
@@ -212,7 +298,22 @@ export default function DashboardPage() {
|
||||
})
|
||||
|
||||
return photos.sort((a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime())
|
||||
}, [treeData, isOwner])
|
||||
}, [treeData.members, isOwner])
|
||||
|
||||
const groupedPhotosByMonth = useMemo(() => {
|
||||
const sortedPhotos = [...allPhotos].sort((a, b) =>
|
||||
new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()
|
||||
)
|
||||
const groupedByMonth: Record<string, typeof allPhotos> = {}
|
||||
sortedPhotos.forEach(photo => {
|
||||
const monthKey = format(new Date(photo.uploadedAt), 'yyyy年MM月')
|
||||
if (!groupedByMonth[monthKey]) {
|
||||
groupedByMonth[monthKey] = []
|
||||
}
|
||||
groupedByMonth[monthKey].push(photo)
|
||||
})
|
||||
return Object.entries(groupedByMonth)
|
||||
}, [allPhotos])
|
||||
|
||||
const handleAdminPhotoToggle = useCallback(async (memberId: string, photoUrl: string, value: boolean) => {
|
||||
if (!isOwner) return
|
||||
@@ -230,7 +331,7 @@ export default function DashboardPage() {
|
||||
} finally {
|
||||
setAdminToggleLoading(null)
|
||||
}
|
||||
}, [isOwner, treeData.members, updateMember])
|
||||
}, [isOwner, updateMember])
|
||||
|
||||
// 保存编辑
|
||||
const handleSaveEdit = async () => {
|
||||
@@ -334,7 +435,54 @@ export default function DashboardPage() {
|
||||
const sorted = anniversaries.sort((a, b) => a.day - b.day)
|
||||
|
||||
return sorted
|
||||
}, [treeData])
|
||||
}, [treeData.members])
|
||||
|
||||
// 计算未来三月纪念日 - 优化版本
|
||||
const upcomingEvents = useMemo(() => {
|
||||
const now = new Date()
|
||||
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
|
||||
const members = Object.values(treeData.members)
|
||||
const events: Array<{
|
||||
member: any
|
||||
type: 'birth' | 'death'
|
||||
date: Date
|
||||
originalDate: string
|
||||
isLunar: boolean
|
||||
lunarDisplay?: string
|
||||
month: number
|
||||
day: number
|
||||
}> = []
|
||||
|
||||
members.forEach(member => {
|
||||
// 生日
|
||||
if (member.birthDate) {
|
||||
const birthEvent = createUpcomingEvent(
|
||||
member,
|
||||
'birth',
|
||||
member.birthDate,
|
||||
member.isLunarDate || false,
|
||||
now,
|
||||
threeMonthsLater
|
||||
)
|
||||
if (birthEvent) events.push(birthEvent)
|
||||
}
|
||||
|
||||
// 忌日
|
||||
if (member.deathDate) {
|
||||
const deathEvent = createUpcomingEvent(
|
||||
member,
|
||||
'death',
|
||||
member.deathDate,
|
||||
member.isLunarDate || false,
|
||||
now,
|
||||
threeMonthsLater
|
||||
)
|
||||
if (deathEvent) events.push(deathEvent)
|
||||
}
|
||||
})
|
||||
|
||||
return events.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
}, [treeData.members])
|
||||
|
||||
// 计算家族迁徙记录
|
||||
const locationGroups = useMemo(() => {
|
||||
@@ -351,7 +499,7 @@ export default function DashboardPage() {
|
||||
})
|
||||
|
||||
return Object.entries(groups).sort((a, b) => b[1].length - a[1].length)
|
||||
}, [treeData])
|
||||
}, [treeData.members])
|
||||
|
||||
// 如果用户没有家族树,显示欢迎页面
|
||||
if (!isLoading && !currentTree) {
|
||||
@@ -647,21 +795,7 @@ export default function DashboardPage() {
|
||||
{allPhotos.length > 0 ? (
|
||||
<div className="space-y-8">
|
||||
{/* 按月份分组显示 */}
|
||||
{(() => {
|
||||
// 按月份分组
|
||||
const sortedPhotos = [...allPhotos].sort((a, b) =>
|
||||
new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()
|
||||
)
|
||||
const groupedByMonth: Record<string, typeof allPhotos> = {}
|
||||
sortedPhotos.forEach(photo => {
|
||||
const monthKey = format(new Date(photo.uploadedAt), 'yyyy年MM月')
|
||||
if (!groupedByMonth[monthKey]) {
|
||||
groupedByMonth[monthKey] = []
|
||||
}
|
||||
groupedByMonth[monthKey].push(photo)
|
||||
})
|
||||
|
||||
return Object.entries(groupedByMonth).map(([month, monthPhotos]) => (
|
||||
{groupedPhotosByMonth.map(([month, monthPhotos]) => (
|
||||
<div key={month}>
|
||||
<h4 className="text-sm font-medium text-muted-foreground mb-4 flex items-center gap-2 sticky top-0 bg-card/95 backdrop-blur py-2 z-10">
|
||||
<span className="w-2 h-2 rounded-full bg-primary"></span>
|
||||
@@ -674,123 +808,19 @@ export default function DashboardPage() {
|
||||
key={`${photo.memberId}-${index}`}
|
||||
className="break-inside-avoid group"
|
||||
>
|
||||
<div className="rounded-lg overflow-hidden bg-card shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 border border-border/50">
|
||||
{photo.adminVisibleOverride === false ? (
|
||||
<div className="p-4 space-y-2">
|
||||
<div className="flex items-center justify-between gap-1.5 text-xs">
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<span>来自</span>
|
||||
<Link
|
||||
href={`/members/${photo.memberId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
|
||||
className="hover:text-primary hover:underline"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={photo.memberName}
|
||||
isDead={photo.isDead}
|
||||
className="text-foreground font-medium"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="text-muted-foreground/70 text-[10px]">
|
||||
{format(new Date(photo.uploadedAt), 'MM-dd')}
|
||||
</span>
|
||||
</div>
|
||||
{isOwner ? (
|
||||
<div className="flex items-center justify-between gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span>允许展示</span>
|
||||
<Switch
|
||||
checked={photo.adminVisibleOverride ?? true}
|
||||
onCheckedChange={(checked) => handleAdminPhotoToggle(photo.memberId, photo.url, checked)}
|
||||
disabled={adminToggleLoading === `${photo.memberId}|${photo.url}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] text-muted-foreground">管理员已隐藏</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 媒体区域 - 点击放大/播放 */}
|
||||
<div
|
||||
className="relative cursor-zoom-in"
|
||||
onClick={() => setSelectedPhoto(photo.url)}
|
||||
>
|
||||
{isVideoFile(photo.url) ? (
|
||||
<div className="relative">
|
||||
<video
|
||||
src={photo.url}
|
||||
className="w-full h-auto object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||
<div className="w-12 h-12 rounded-full bg-white/90 flex items-center justify-center">
|
||||
<Play className="h-6 w-6 text-black ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-2 left-2 bg-black/70 text-white text-xs px-2 py-1 rounded flex items-center gap-1">
|
||||
<Video className="h-3 w-3" />
|
||||
视频
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={photo.url}
|
||||
alt={photo.caption || `${photo.memberName}的照片`}
|
||||
className="w-full h-auto object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 底部显示信息 */}
|
||||
<div className="px-3 py-2 bg-card border-t border-border/30 space-y-1">
|
||||
{/* 照片说明 */}
|
||||
<div className="text-xs line-clamp-2">
|
||||
{photo.caption ? (
|
||||
<span className="text-foreground">{photo.caption}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground/70">暂无说明</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 分享人和时间 */}
|
||||
<div className="flex items-center justify-between gap-1.5 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">来自</span>
|
||||
<Link
|
||||
href={`/members/${photo.memberId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
|
||||
className="hover:text-primary hover:underline"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={photo.memberName}
|
||||
isDead={photo.isDead}
|
||||
className="text-foreground font-medium"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="text-muted-foreground/70 text-[10px]">
|
||||
{format(new Date(photo.uploadedAt), 'MM-dd')}
|
||||
</span>
|
||||
</div>
|
||||
{isOwner && (
|
||||
<div className="mt-2 flex items-center justify-between gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span>允许展示</span>
|
||||
<Switch
|
||||
checked={photo.adminVisibleOverride ?? true}
|
||||
onCheckedChange={(checked) => handleAdminPhotoToggle(photo.memberId, photo.url, checked)}
|
||||
disabled={adminToggleLoading === `${photo.memberId}|${photo.url}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<PhotoCard
|
||||
photo={photo}
|
||||
isOwner={isOwner ?? false}
|
||||
currentTree={currentTree ?? undefined}
|
||||
onSelect={setSelectedPhoto}
|
||||
onToggle={handleAdminPhotoToggle}
|
||||
isLoading={adminToggleLoading === `${photo.memberId}|${photo.url}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
})()}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
@@ -861,7 +891,7 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
第 {anniversary.member.generation} 世 · {yearsAgo} 年
|
||||
第 {anniversary.member.generation} 世 · {yearsAgo} 岁
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -953,113 +983,6 @@ export default function DashboardPage() {
|
||||
<ChineseCardContent>
|
||||
{(() => {
|
||||
const now = new Date()
|
||||
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
|
||||
const members = Object.values(treeData.members)
|
||||
const upcomingEvents: Array<{
|
||||
member: any
|
||||
type: 'birth' | 'death'
|
||||
date: Date
|
||||
originalDate: string
|
||||
isLunar: boolean
|
||||
lunarDisplay?: string
|
||||
month: number
|
||||
day: number
|
||||
}> = []
|
||||
|
||||
members.forEach(member => {
|
||||
// 生日
|
||||
if (member.birthDate) {
|
||||
const birthDate = new Date(member.birthDate)
|
||||
let thisYearBirth: Date
|
||||
let lunarDisplay: string | undefined
|
||||
|
||||
// 检查是否按农历计算
|
||||
if (member.isLunarDate) {
|
||||
// 农历生日:计算今年对应的公历日期
|
||||
const lunarInfo = solar2lunar(birthDate)
|
||||
if (lunarInfo) {
|
||||
const thisYearLunar = lunar2solar(
|
||||
now.getFullYear(),
|
||||
lunarInfo.lunarMonth,
|
||||
lunarInfo.lunarDay,
|
||||
lunarInfo.isLeap
|
||||
)
|
||||
if (thisYearLunar) {
|
||||
thisYearBirth = thisYearLunar
|
||||
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
||||
} else {
|
||||
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||
}
|
||||
} else {
|
||||
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||
}
|
||||
} else {
|
||||
// 公历生日
|
||||
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||
}
|
||||
|
||||
if (thisYearBirth >= now && thisYearBirth <= threeMonthsLater) {
|
||||
upcomingEvents.push({
|
||||
member,
|
||||
type: 'birth',
|
||||
date: thisYearBirth,
|
||||
originalDate: member.birthDate,
|
||||
isLunar: member.isLunarDate || false,
|
||||
lunarDisplay,
|
||||
month: thisYearBirth.getMonth() + 1,
|
||||
day: thisYearBirth.getDate()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 忌日
|
||||
if (member.deathDate) {
|
||||
const deathDate = new Date(member.deathDate)
|
||||
let thisYearDeath: Date
|
||||
let lunarDisplay: string | undefined
|
||||
|
||||
// 检查是否按农历计算
|
||||
if (member.isLunarDate) {
|
||||
// 农历忌日:计算今年对应的公历日期
|
||||
const lunarInfo = solar2lunar(deathDate)
|
||||
if (lunarInfo) {
|
||||
const thisYearLunar = lunar2solar(
|
||||
now.getFullYear(),
|
||||
lunarInfo.lunarMonth,
|
||||
lunarInfo.lunarDay,
|
||||
lunarInfo.isLeap
|
||||
)
|
||||
if (thisYearLunar) {
|
||||
thisYearDeath = thisYearLunar
|
||||
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
||||
} else {
|
||||
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||
}
|
||||
} else {
|
||||
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||
}
|
||||
} else {
|
||||
// 公历忌日
|
||||
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||
}
|
||||
|
||||
if (thisYearDeath >= now && thisYearDeath <= threeMonthsLater) {
|
||||
upcomingEvents.push({
|
||||
member,
|
||||
type: 'death',
|
||||
date: thisYearDeath,
|
||||
originalDate: member.deathDate,
|
||||
isLunar: member.isLunarDate || false,
|
||||
lunarDisplay,
|
||||
month: thisYearDeath.getMonth() + 1,
|
||||
day: thisYearDeath.getDate()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
|
||||
const birthEvents = upcomingEvents.filter(e => e.type === 'birth')
|
||||
const deathEvents = upcomingEvents.filter(e => e.type === 'death')
|
||||
|
||||
@@ -1109,7 +1032,7 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
第 {event.member.generation} 世 · {yearsAgo} 年 · <span className="text-primary font-medium">{daysUntil}天</span>
|
||||
第 {event.member.generation} 世 · {yearsAgo} 岁 · <span className="text-primary font-medium">{daysUntil}天</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user