more fixes
This commit is contained in:
95
resources/js/dashboard/components/ActivityFeed.jsx
Normal file
95
resources/js/dashboard/components/ActivityFeed.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
function actorLabel(item) {
|
||||
if (!item.actor) {
|
||||
return 'System'
|
||||
}
|
||||
|
||||
return item.actor.username ? `@${item.actor.username}` : item.actor.name || 'User'
|
||||
}
|
||||
|
||||
function timeLabel(dateString) {
|
||||
const date = new Date(dateString)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return 'just now'
|
||||
}
|
||||
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
export default function ActivityFeed() {
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await window.axios.get('/api/dashboard/activity')
|
||||
if (!cancelled) {
|
||||
setItems(Array.isArray(response.data?.data) ? response.data.data : [])
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setError('Could not load activity right now.')
|
||||
}
|
||||
} 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="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Activity Feed</h2>
|
||||
<span className="text-xs text-gray-400">Recent actions</span>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="text-sm text-gray-400">Loading activity...</p> : null}
|
||||
{error ? <p className="text-sm text-rose-300">{error}</p> : null}
|
||||
|
||||
{!loading && !error && items.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No recent activity yet.</p>
|
||||
) : null}
|
||||
|
||||
{!loading && !error && items.length > 0 ? (
|
||||
<div className="max-h-[520px] space-y-3 overflow-y-auto pr-1">
|
||||
{items.map((item) => (
|
||||
<article
|
||||
key={item.id}
|
||||
className={`rounded-xl border p-3 transition ${
|
||||
item.is_unread
|
||||
? 'border-cyan-500/40 bg-cyan-500/10'
|
||||
: 'border-gray-700 bg-gray-900/60'
|
||||
}`}
|
||||
>
|
||||
<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}
|
||||
</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>
|
||||
<p className="mt-2 text-xs text-gray-400">{timeLabel(item.created_at)}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
66
resources/js/dashboard/components/CreatorAnalytics.jsx
Normal file
66
resources/js/dashboard/components/CreatorAnalytics.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
function Widget({ label, value }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-700 bg-gray-900/70 p-4 shadow-lg transition hover:scale-[1.02]">
|
||||
<p className="text-xs uppercase tracking-wide text-gray-400">{label}</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{value}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CreatorAnalytics({ isCreator }) {
|
||||
const [data, setData] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await window.axios.get('/api/dashboard/analytics')
|
||||
if (!cancelled) {
|
||||
setData(response.data?.data || null)
|
||||
}
|
||||
} 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="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Creator Analytics</h2>
|
||||
<a href="/creator/analytics" className="text-xs text-cyan-300 hover:text-cyan-200">
|
||||
Open analytics
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="text-sm text-gray-400">Loading analytics...</p> : null}
|
||||
|
||||
{!loading && !isCreator && !data?.is_creator ? (
|
||||
<div className="rounded-xl border border-gray-700 bg-gray-900/60 p-4 text-sm text-gray-300">
|
||||
Upload your first artwork to unlock creator-only insights.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!loading && (isCreator || data?.is_creator) ? (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Widget label="Total Artworks" value={data?.total_artworks ?? 0} />
|
||||
<Widget label="Total Story Views" value={data?.total_story_views ?? 0} />
|
||||
<Widget label="Total Followers" value={data?.total_followers ?? 0} />
|
||||
<Widget label="Total Likes" value={data?.total_likes ?? 0} />
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
64
resources/js/dashboard/components/QuickActions.jsx
Normal file
64
resources/js/dashboard/components/QuickActions.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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'
|
||||
|
||||
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 }) {
|
||||
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'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{actions.map((action) => (
|
||||
<a key={action.key} href={action.href} 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">
|
||||
<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>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
78
resources/js/dashboard/components/RecommendedCreators.jsx
Normal file
78
resources/js/dashboard/components/RecommendedCreators.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export default function RecommendedCreators() {
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await window.axios.get('/api/dashboard/recommended-creators')
|
||||
if (!cancelled) {
|
||||
setItems(Array.isArray(response.data?.data) ? response.data.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="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Recommended Creators</h2>
|
||||
<a className="text-xs text-cyan-300 hover:text-cyan-200" href="/creators/top">
|
||||
See all
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="text-sm text-gray-400">Loading creators...</p> : null}
|
||||
|
||||
{!loading && items.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No creator recommendations right now.</p>
|
||||
) : null}
|
||||
|
||||
{!loading && items.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{items.map((creator) => (
|
||||
<article
|
||||
key={creator.id}
|
||||
className="flex items-center justify-between rounded-xl border border-gray-700 bg-gray-900/70 p-3 transition hover:scale-[1.02]"
|
||||
>
|
||||
<a href={creator.url || '#'} className="flex min-w-0 items-center gap-3">
|
||||
<img
|
||||
src={creator.avatar || '/images/default-avatar.png'}
|
||||
alt={creator.username || creator.name || 'Creator'}
|
||||
className="h-10 w-10 rounded-full border border-gray-600 object-cover"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-white">
|
||||
{creator.username ? `@${creator.username}` : creator.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">{creator.followers_count} followers</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={creator.url || '#'}
|
||||
className="rounded-lg border border-cyan-400/60 px-3 py-1 text-xs font-semibold text-cyan-200 transition hover:bg-cyan-500/20"
|
||||
>
|
||||
Follow
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
71
resources/js/dashboard/components/TrendingArtworks.jsx
Normal file
71
resources/js/dashboard/components/TrendingArtworks.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export default function TrendingArtworks() {
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await window.axios.get('/api/dashboard/trending-artworks')
|
||||
if (!cancelled) {
|
||||
setItems(Array.isArray(response.data?.data) ? response.data.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="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Trending Artworks</h2>
|
||||
<a className="text-xs text-cyan-300 hover:text-cyan-200" href="/discover/trending">
|
||||
Explore more
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="text-sm text-gray-400">Loading trending artworks...</p> : null}
|
||||
|
||||
{!loading && items.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No trending artworks available.</p>
|
||||
) : null}
|
||||
|
||||
{!loading && items.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{items.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={item.url}
|
||||
className="group overflow-hidden rounded-xl border border-gray-700 bg-gray-900/70 transition hover:scale-[1.02] hover:border-cyan-500/40"
|
||||
>
|
||||
<img
|
||||
src={item.thumbnail || '/images/placeholder.jpg'}
|
||||
alt={item.title}
|
||||
loading="lazy"
|
||||
className="h-28 w-full object-cover sm:h-32"
|
||||
/>
|
||||
<div className="p-2">
|
||||
<p className="line-clamp-1 text-sm font-semibold text-white">{item.title}</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
{item.likes} likes • {item.views} views
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user