update
This commit is contained in:
@@ -1,11 +1,30 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
function actorLabel(item) {
|
||||
if (!item.actor) {
|
||||
return 'System'
|
||||
if (!item?.user) {
|
||||
return 'Someone'
|
||||
}
|
||||
|
||||
return item.actor.username ? `@${item.actor.username}` : item.actor.name || 'User'
|
||||
return item.user.username ? `@${item.user.username}` : item.user.name || 'User'
|
||||
}
|
||||
|
||||
function describeActivity(item) {
|
||||
const artworkTitle = item?.artwork?.title || 'an artwork'
|
||||
const mentionTarget = item?.mentioned_user?.username || item?.mentioned_user?.name || 'someone'
|
||||
const reactionLabel = item?.reaction?.label || 'reacted'
|
||||
|
||||
switch (item?.type) {
|
||||
case 'comment':
|
||||
return `commented on ${artworkTitle}`
|
||||
case 'reply':
|
||||
return `replied on ${artworkTitle}`
|
||||
case 'reaction':
|
||||
return `${reactionLabel.toLowerCase()} on ${artworkTitle}`
|
||||
case 'mention':
|
||||
return `mentioned @${mentionTarget} on ${artworkTitle}`
|
||||
default:
|
||||
return 'shared new activity'
|
||||
}
|
||||
}
|
||||
|
||||
function timeLabel(dateString) {
|
||||
@@ -28,9 +47,15 @@ export default function ActivityFeed() {
|
||||
async function load() {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await window.axios.get('/api/dashboard/activity')
|
||||
const response = await window.axios.get('/api/activity', {
|
||||
params: {
|
||||
filter: 'my',
|
||||
per_page: 8,
|
||||
},
|
||||
})
|
||||
if (!cancelled) {
|
||||
setItems(Array.isArray(response.data?.data) ? response.data.data : [])
|
||||
setError('')
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
@@ -77,14 +102,12 @@ export default function ActivityFeed() {
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm text-gray-100">
|
||||
<span className="font-semibold text-white">{actorLabel(item)}</span> {item.message}
|
||||
<span className="font-semibold text-white">{actorLabel(item)}</span> {describeActivity(item)}
|
||||
</p>
|
||||
{item.is_unread ? (
|
||||
<span className="rounded-full bg-cyan-500/20 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-cyan-200">
|
||||
unread
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{item.comment?.body ? (
|
||||
<p className="mt-2 line-clamp-2 text-xs text-gray-300">{item.comment.body}</p>
|
||||
) : null}
|
||||
<p className="mt-2 text-xs text-gray-400">{timeLabel(item.created_at)}</p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
@@ -1,59 +1,124 @@
|
||||
import React from 'react'
|
||||
|
||||
const baseCard =
|
||||
'group rounded-xl border border-gray-700 bg-gray-800 p-4 shadow-lg transition hover:scale-[1.02] hover:border-cyan-500/40'
|
||||
'group rounded-2xl border border-white/10 bg-[#0b1826]/85 p-4 shadow-lg shadow-black/20 transition hover:-translate-y-0.5 hover:border-sky-300/35 hover:bg-[#102033]'
|
||||
|
||||
const actions = [
|
||||
{
|
||||
key: 'upload-artwork',
|
||||
label: 'Upload Artwork',
|
||||
href: '/upload',
|
||||
icon: 'fa-solid fa-cloud-arrow-up',
|
||||
description: 'Publish a new piece to your portfolio.',
|
||||
},
|
||||
{
|
||||
key: 'write-story',
|
||||
label: 'Write Story',
|
||||
href: '/creator/stories/create',
|
||||
icon: 'fa-solid fa-pen-nib',
|
||||
description: 'Create a story, tutorial, or showcase.',
|
||||
},
|
||||
{
|
||||
key: 'edit-profile',
|
||||
label: 'Edit Profile',
|
||||
href: '/settings/profile',
|
||||
icon: 'fa-solid fa-user-gear',
|
||||
description: 'Update your profile details and links.',
|
||||
},
|
||||
{
|
||||
key: 'notifications',
|
||||
label: 'View Notifications',
|
||||
href: '/messages',
|
||||
icon: 'fa-solid fa-bell',
|
||||
description: 'Catch up with mentions and updates.',
|
||||
},
|
||||
]
|
||||
export default function QuickActions({ isCreator, receivedCommentsCount = 0, onNavigate }) {
|
||||
const actions = [
|
||||
{
|
||||
key: 'edit-profile',
|
||||
label: 'Edit Profile',
|
||||
href: '/dashboard/profile',
|
||||
icon: 'fa-solid fa-user-gear',
|
||||
description: 'Refresh your bio, socials, avatar, and country details.',
|
||||
},
|
||||
{
|
||||
key: 'received-comments',
|
||||
label: 'Review Feedback',
|
||||
href: '/dashboard/comments/received',
|
||||
icon: 'fa-solid fa-inbox',
|
||||
description: 'Read the latest comments left on your work.',
|
||||
badge: receivedCommentsCount > 0 ? `${receivedCommentsCount} new` : null,
|
||||
},
|
||||
{
|
||||
key: 'notifications',
|
||||
label: 'Open Notifications',
|
||||
href: '/dashboard/notifications',
|
||||
icon: 'fa-solid fa-bell',
|
||||
description: 'Catch up on mentions, replies, and updates.',
|
||||
},
|
||||
...(isCreator
|
||||
? [
|
||||
{
|
||||
key: 'manage-artworks',
|
||||
label: 'Manage Artworks',
|
||||
href: '/dashboard/artworks',
|
||||
icon: 'fa-solid fa-layer-group',
|
||||
description: 'Edit titles, details, and the presentation of your portfolio.',
|
||||
},
|
||||
{
|
||||
key: 'write-story',
|
||||
label: 'Write Story',
|
||||
href: '/creator/stories/create',
|
||||
icon: 'fa-solid fa-pen-nib',
|
||||
description: 'Publish a tutorial, devlog, showcase, or announcement.',
|
||||
},
|
||||
{
|
||||
key: 'open-studio',
|
||||
label: 'Open Studio',
|
||||
href: '/studio',
|
||||
icon: 'fa-solid fa-compass-drafting',
|
||||
description: 'Jump into the wider creator workspace and analytics tools.',
|
||||
},
|
||||
{
|
||||
key: 'creator-stories',
|
||||
label: 'Story Dashboard',
|
||||
href: '/creator/stories',
|
||||
icon: 'fa-solid fa-newspaper',
|
||||
description: 'Review creator stories, drafts, and publishing flow in one place.',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: 'upload-artwork',
|
||||
label: 'Upload Artwork',
|
||||
href: '/upload',
|
||||
icon: 'fa-solid fa-cloud-arrow-up',
|
||||
description: 'Start publishing and unlock more creator-focused dashboard tools.',
|
||||
},
|
||||
{
|
||||
key: 'explore-trending',
|
||||
label: 'Explore Trending',
|
||||
href: '/discover/trending',
|
||||
icon: 'fa-solid fa-fire-flame-curved',
|
||||
description: 'Browse what is hot right now to spot styles and creators worth following.',
|
||||
},
|
||||
{
|
||||
key: 'find-creators',
|
||||
label: 'Find Creators',
|
||||
href: '/creators/top',
|
||||
icon: 'fa-solid fa-user-group',
|
||||
description: 'Discover artists to follow and build a stronger feed around your taste.',
|
||||
},
|
||||
{
|
||||
key: 'saved-favorites',
|
||||
label: 'Open Favorites',
|
||||
href: '/dashboard/favorites',
|
||||
icon: 'fa-solid fa-bookmark',
|
||||
description: 'Revisit saved work and start shaping your own inspiration library.',
|
||||
},
|
||||
]),
|
||||
]
|
||||
|
||||
export default function QuickActions({ isCreator }) {
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-700 bg-gray-800 p-5 shadow-lg">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Quick Actions</h2>
|
||||
<span className="rounded-full border border-gray-600 px-2 py-1 text-xs text-gray-300">
|
||||
{isCreator ? 'Creator mode' : 'User mode'}
|
||||
<section className="rounded-[28px] border border-white/10 bg-[#08111c]/90 p-5 shadow-2xl shadow-black/20 sm:p-6">
|
||||
<div className="mb-5 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] uppercase tracking-[0.24em] text-sky-200/80">Quick Actions</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">Start something useful right now</h2>
|
||||
</div>
|
||||
<span className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs uppercase tracking-[0.16em] text-slate-300">
|
||||
{isCreator ? 'Creator Mode' : 'Member Mode'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{actions.map((action) => (
|
||||
<a key={action.key} href={action.href} className={baseCard}>
|
||||
<a key={action.key} href={action.href} onClick={() => onNavigate?.(action.href, action.label)} className={baseCard}>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 text-cyan-300">
|
||||
<span className="inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/5 text-sky-200">
|
||||
<i className={action.icon} aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{action.label}</p>
|
||||
<p className="mt-1 text-xs text-gray-300">{action.description}</p>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-semibold text-white">{action.label}</p>
|
||||
{action.badge ? (
|
||||
<span className="rounded-full border border-sky-300/20 bg-sky-400/10 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-[0.12em] text-sky-100">
|
||||
{action.badge}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">{action.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
66
resources/js/dashboard/components/RecentAchievements.jsx
Normal file
66
resources/js/dashboard/components/RecentAchievements.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import AchievementBadge from '../../components/achievements/AchievementBadge'
|
||||
|
||||
export default function RecentAchievements() {
|
||||
const [data, setData] = useState({ recent: [], counts: { unlocked: 0, total: 0 } })
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await window.axios.get('/api/user/achievements')
|
||||
if (!cancelled && response.data) {
|
||||
setData(response.data)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-700 bg-gray-800 p-5 shadow-lg">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-gray-400">Achievements</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">Recent Unlocks</h2>
|
||||
</div>
|
||||
<span className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs text-gray-300">
|
||||
{data?.counts?.unlocked || 0} / {data?.counts?.total || 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="mt-4 text-sm text-gray-400">Loading achievements...</p> : null}
|
||||
|
||||
{!loading && (!Array.isArray(data?.recent) || data.recent.length === 0) ? (
|
||||
<p className="mt-4 text-sm text-gray-400">No achievements unlocked yet.</p>
|
||||
) : null}
|
||||
|
||||
{!loading && Array.isArray(data?.recent) && data.recent.length > 0 ? (
|
||||
<div className="mt-4 space-y-3">
|
||||
{data.recent.map((achievement) => (
|
||||
<article key={achievement.id} className="rounded-xl border border-gray-700 bg-gray-900/60 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-white">{achievement.name}</p>
|
||||
<p className="mt-1 text-xs text-gray-400">{achievement.description}</p>
|
||||
</div>
|
||||
<AchievementBadge achievement={achievement} compact />
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import LevelBadge from '../../components/xp/LevelBadge'
|
||||
|
||||
export default function RecommendedCreators() {
|
||||
const [items, setItems] = useState([])
|
||||
@@ -59,6 +60,9 @@ export default function RecommendedCreators() {
|
||||
<p className="truncate text-sm font-semibold text-white">
|
||||
{creator.username ? `@${creator.username}` : creator.name}
|
||||
</p>
|
||||
<div className="mt-1">
|
||||
<LevelBadge level={creator.level} rank={creator.rank} compact />
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">{creator.followers_count} followers</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
76
resources/js/dashboard/components/TopCreatorsWidget.jsx
Normal file
76
resources/js/dashboard/components/TopCreatorsWidget.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import LevelBadge from '../../components/xp/LevelBadge'
|
||||
|
||||
export default function TopCreatorsWidget() {
|
||||
const [data, setData] = useState({ items: [] })
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await window.axios.get('/api/leaderboard/creators?period=weekly')
|
||||
if (!cancelled && response.data) {
|
||||
setData(response.data)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const items = Array.isArray(data?.items) ? data.items.slice(0, 5) : []
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-700 bg-gray-800 p-5 shadow-lg">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-gray-400">Leaderboard</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">Top Creators</h2>
|
||||
</div>
|
||||
<a href="/leaderboard?type=creators&period=weekly" className="text-xs font-semibold uppercase tracking-[0.14em] text-sky-300 hover:text-sky-200">
|
||||
View all
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="mt-4 text-sm text-gray-400">Loading leaderboard...</p> : null}
|
||||
|
||||
{!loading && items.length === 0 ? (
|
||||
<p className="mt-4 text-sm text-gray-400">No creators ranked yet.</p>
|
||||
) : null}
|
||||
|
||||
{!loading && items.length > 0 ? (
|
||||
<div className="mt-4 space-y-3">
|
||||
{items.map((item) => {
|
||||
const entity = item.entity || {}
|
||||
|
||||
return (
|
||||
<a key={item.rank} href={entity.url || '#'} className="flex items-center gap-3 rounded-xl border border-gray-700 bg-gray-900/60 p-3 transition hover:border-sky-400/40">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-sm font-black text-white">
|
||||
#{item.rank}
|
||||
</div>
|
||||
{entity.avatar ? <img src={entity.avatar} alt={entity.name || 'Creator'} className="h-11 w-11 rounded-xl object-cover" loading="lazy" /> : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold text-white">{entity.name}</p>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<LevelBadge level={entity.level} rank={entity.rank} compact />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-sky-300">{Math.round(item.score)}</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import LevelBadge from '../../components/xp/LevelBadge'
|
||||
|
||||
export default function TrendingArtworks() {
|
||||
const [items, setItems] = useState([])
|
||||
@@ -58,6 +59,14 @@ export default function TrendingArtworks() {
|
||||
/>
|
||||
<div className="p-2">
|
||||
<p className="line-clamp-1 text-sm font-semibold text-white">{item.title}</p>
|
||||
{item.creator ? (
|
||||
<div className="mt-1 flex items-center justify-between gap-2">
|
||||
<span className="truncate text-xs text-gray-400">
|
||||
{item.creator.username ? `@${item.creator.username}` : item.creator.name}
|
||||
</span>
|
||||
<LevelBadge level={item.creator.level} rank={item.creator.rank} compact />
|
||||
</div>
|
||||
) : null}
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
{item.likes} likes • {item.views} views
|
||||
</p>
|
||||
|
||||
63
resources/js/dashboard/components/XPProgressWidget.jsx
Normal file
63
resources/js/dashboard/components/XPProgressWidget.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import LevelBadge from '../../components/xp/LevelBadge'
|
||||
import XPProgressBar from '../../components/xp/XPProgressBar'
|
||||
|
||||
export default function XPProgressWidget({ initialLevel = 1, initialRank = 'Newbie' }) {
|
||||
const [data, setData] = useState({
|
||||
xp: 0,
|
||||
level: initialLevel,
|
||||
rank: initialRank,
|
||||
current_level_xp: 0,
|
||||
next_level_xp: 100,
|
||||
progress_percent: 0,
|
||||
max_level: false,
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await window.axios.get('/api/user/xp')
|
||||
if (!cancelled && response.data) {
|
||||
setData((current) => ({ ...current, ...response.data }))
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-700 bg-gray-800 p-5 shadow-lg">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-gray-400">Progression</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">XP Progress</h2>
|
||||
</div>
|
||||
<LevelBadge level={data.level} rank={data.rank} compact />
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<XPProgressBar
|
||||
xp={data.xp}
|
||||
currentLevelXp={data.current_level_xp}
|
||||
nextLevelXp={data.next_level_xp}
|
||||
progressPercent={data.progress_percent}
|
||||
maxLevel={data.max_level}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="mt-3 text-xs text-gray-400">Syncing your latest XP...</p> : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user