Tighten ACP forum actions and avatar handling
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
._*
|
||||||
.env
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
.env.production
|
.env.production
|
||||||
|
|||||||
@@ -7,15 +7,15 @@
|
|||||||
- Added user avatars (upload + display) and a basic profile page/API.
|
- Added user avatars (upload + display) and a basic profile page/API.
|
||||||
- Seeded a Micha test user with verified email.
|
- Seeded a Micha test user with verified email.
|
||||||
|
|
||||||
|
## 2026-01-11
|
||||||
|
- Restyled the thread view to mimic phpBB: compact toolbar, title row, and post layout.
|
||||||
|
- Added phpBB-style post action buttons and post author info for replies.
|
||||||
|
|
||||||
## 2026-01-02
|
## 2026-01-02
|
||||||
- Added ACP general settings for forum name, theme, accents, and logo (no reload required).
|
- Added ACP general settings for forum name, theme, accents, and logo (no reload required).
|
||||||
- Added admin-only upload endpoints and ACP UI for logos and favicons.
|
- Added admin-only upload endpoints and ACP UI for logos and favicons.
|
||||||
- Applied forum branding, theme defaults, accents, logos, and favicon links in the SPA header.
|
- Applied forum branding, theme defaults, accents, logos, and favicon links in the SPA header.
|
||||||
|
|
||||||
## 2026-01-11
|
|
||||||
- Restyled the thread view to mimic phpBB: compact toolbar, title row, and post layout.
|
|
||||||
- Added phpBB-style post action buttons and post author info for replies.
|
|
||||||
|
|
||||||
## 2025-12-30
|
## 2025-12-30
|
||||||
- Added soft deletes with audit metadata (deleted_at/deleted_by) for forums, threads, and posts.
|
- Added soft deletes with audit metadata (deleted_at/deleted_by) for forums, threads, and posts.
|
||||||
- Ensured API listings and ACP forum tree omit soft-deleted records by default.
|
- Ensured API listings and ACP forum tree omit soft-deleted records by default.
|
||||||
|
|||||||
@@ -68,7 +68,12 @@ class ForumController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$position = Forum::where('parent_id', $parentId)->max('position');
|
if ($parentId === null) {
|
||||||
|
Forum::whereNull('parent_id')->increment('position');
|
||||||
|
$position = 0;
|
||||||
|
} else {
|
||||||
|
$position = Forum::where('parent_id', $parentId)->max('position');
|
||||||
|
}
|
||||||
|
|
||||||
$forum = Forum::create([
|
$forum = Forum::create([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ class UploadController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'file' => ['required', 'image', 'mimes:jpg,jpeg,png,gif,webp', 'max:2048'],
|
'file' => [
|
||||||
|
'required',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png,gif,webp',
|
||||||
|
'max:2048',
|
||||||
|
'dimensions:max_width=150,max_height=150',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($user->avatar_path) {
|
if ($user->avatar_path) {
|
||||||
|
|||||||
@@ -1422,11 +1422,24 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bb-portal-user-avatar {
|
.bb-portal-user-avatar {
|
||||||
width: 72px;
|
width: 150px;
|
||||||
height: 72px;
|
height: 150px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.04));
|
background: linear-gradient(145deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.04));
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--bb-accent, #f29b3f);
|
||||||
|
font-size: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bb-portal-user-avatar img {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 150px;
|
||||||
|
max-height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bb-portal-user-name {
|
.bb-portal-user-name {
|
||||||
@@ -1644,6 +1657,14 @@ a {
|
|||||||
border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000);
|
border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bb-tree-action-group {
|
||||||
|
width: 176px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bb-tree-action-group .bb-action-group {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.bb-drag-handle {
|
.bb-drag-handle {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -905,32 +905,34 @@ export default function Acp({ isAdmin }) {
|
|||||||
>
|
>
|
||||||
<i className="bi bi-arrow-down-up" aria-hidden="true" />
|
<i className="bi bi-arrow-down-up" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
<ButtonGroup size="sm" className="bb-action-group">
|
<div className="bb-tree-action-group">
|
||||||
{node.type === 'category' && (
|
<ButtonGroup size="sm" className="bb-action-group w-100">
|
||||||
<>
|
{node.type === 'category' && (
|
||||||
<Button
|
<>
|
||||||
variant="dark"
|
<Button
|
||||||
onClick={() => handleStartCreateChild('category', node.id)}
|
variant="dark"
|
||||||
title={t('acp.add_category')}
|
onClick={() => handleStartCreateChild('category', node.id)}
|
||||||
>
|
title={t('acp.add_category')}
|
||||||
<i className="bi bi-folder-plus" aria-hidden="true" />
|
>
|
||||||
</Button>
|
<i className="bi bi-folder-plus" aria-hidden="true" />
|
||||||
<Button
|
</Button>
|
||||||
variant="dark"
|
<Button
|
||||||
onClick={() => handleStartCreateChild('forum', node.id)}
|
variant="dark"
|
||||||
title={t('acp.add_forum')}
|
onClick={() => handleStartCreateChild('forum', node.id)}
|
||||||
>
|
title={t('acp.add_forum')}
|
||||||
<i className="bi bi-chat-left-text" aria-hidden="true" />
|
>
|
||||||
</Button>
|
<i className="bi bi-chat-left-text" aria-hidden="true" />
|
||||||
</>
|
</Button>
|
||||||
)}
|
</>
|
||||||
<Button variant="dark" onClick={() => handleSelectForum(node)} title={t('acp.edit')}>
|
)}
|
||||||
<i className="bi bi-pencil" aria-hidden="true" />
|
<Button variant="dark" onClick={() => handleSelectForum(node)} title={t('acp.edit')}>
|
||||||
</Button>
|
<i className="bi bi-pencil" aria-hidden="true" />
|
||||||
<Button variant="dark" onClick={() => handleDelete(node.id)} title={t('acp.delete')}>
|
</Button>
|
||||||
<i className="bi bi-trash" aria-hidden="true" />
|
<Button variant="dark" onClick={() => handleDelete(node.id)} title={t('acp.delete')}>
|
||||||
</Button>
|
<i className="bi bi-trash" aria-hidden="true" />
|
||||||
</ButtonGroup>
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{node.children?.length > 0 &&
|
{node.children?.length > 0 &&
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Badge, Container } from 'react-bootstrap'
|
import { Badge, Container } from 'react-bootstrap'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { listAllForums, listThreads } from '../api/client'
|
import { getCurrentUser, listAllForums, listThreads } from '../api/client'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [forums, setForums] = useState([])
|
const [forums, setForums] = useState([])
|
||||||
@@ -10,6 +11,8 @@ export default function Home() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loadingForums, setLoadingForums] = useState(true)
|
const [loadingForums, setLoadingForums] = useState(true)
|
||||||
const [loadingThreads, setLoadingThreads] = useState(true)
|
const [loadingThreads, setLoadingThreads] = useState(true)
|
||||||
|
const [profile, setProfile] = useState(null)
|
||||||
|
const { token, roles, email } = useAuth()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -26,6 +29,27 @@ export default function Home() {
|
|||||||
.finally(() => setLoadingThreads(false))
|
.finally(() => setLoadingThreads(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
setProfile(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let active = true
|
||||||
|
|
||||||
|
getCurrentUser()
|
||||||
|
.then((data) => {
|
||||||
|
if (!active) return
|
||||||
|
setProfile(data)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (active) setProfile(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
}
|
||||||
|
}, [token])
|
||||||
|
|
||||||
const getParentId = (forum) => {
|
const getParentId = (forum) => {
|
||||||
if (!forum.parent) return null
|
if (!forum.parent) return null
|
||||||
if (typeof forum.parent === 'string') {
|
if (typeof forum.parent === 'string') {
|
||||||
@@ -79,6 +103,13 @@ export default function Home() {
|
|||||||
.slice(0, 12)
|
.slice(0, 12)
|
||||||
}, [threads])
|
}, [threads])
|
||||||
|
|
||||||
|
const roleLabel = useMemo(() => {
|
||||||
|
if (!roles?.length) return t('portal.user_role_member')
|
||||||
|
if (roles.includes('ROLE_ADMIN')) return t('portal.user_role_operator')
|
||||||
|
if (roles.includes('ROLE_MODERATOR')) return t('portal.user_role_moderator')
|
||||||
|
return t('portal.user_role_member')
|
||||||
|
}, [roles, t])
|
||||||
|
|
||||||
const resolveForumName = (thread) => {
|
const resolveForumName = (thread) => {
|
||||||
if (!thread?.forum) return t('portal.unknown_forum')
|
if (!thread?.forum) return t('portal.unknown_forum')
|
||||||
const parts = thread.forum.split('/')
|
const parts = thread.forum.split('/')
|
||||||
@@ -205,9 +236,15 @@ export default function Home() {
|
|||||||
<div className="bb-portal-card">
|
<div className="bb-portal-card">
|
||||||
<div className="bb-portal-card-title">{t('portal.user_menu')}</div>
|
<div className="bb-portal-card-title">{t('portal.user_menu')}</div>
|
||||||
<div className="bb-portal-user-card">
|
<div className="bb-portal-user-card">
|
||||||
<div className="bb-portal-user-avatar" />
|
<div className="bb-portal-user-avatar">
|
||||||
<div className="bb-portal-user-name">tracer</div>
|
{profile?.avatar_url ? (
|
||||||
<div className="bb-portal-user-role">Operator</div>
|
<img src={profile.avatar_url} alt="" />
|
||||||
|
) : (
|
||||||
|
<i className="bi bi-person" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="bb-portal-user-name">{profile?.name || email || 'User'}</div>
|
||||||
|
<div className="bb-portal-user-role">{roleLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="bb-portal-list">
|
<ul className="bb-portal-list">
|
||||||
<li>{t('portal.user_new_posts')}</li>
|
<li>{t('portal.user_new_posts')}</li>
|
||||||
|
|||||||
@@ -150,6 +150,9 @@
|
|||||||
"portal.user_control_panel": "Benutzerkontrollzentrum",
|
"portal.user_control_panel": "Benutzerkontrollzentrum",
|
||||||
"portal.user_profile": "Profil",
|
"portal.user_profile": "Profil",
|
||||||
"portal.user_logout": "Logout",
|
"portal.user_logout": "Logout",
|
||||||
|
"portal.user_role_operator": "Operator",
|
||||||
|
"portal.user_role_moderator": "Moderator",
|
||||||
|
"portal.user_role_member": "Mitglied",
|
||||||
"portal.advertisement": "Werbung",
|
"portal.advertisement": "Werbung",
|
||||||
"profile.title": "Profil",
|
"profile.title": "Profil",
|
||||||
"profile.loading": "Profil wird geladen...",
|
"profile.loading": "Profil wird geladen...",
|
||||||
|
|||||||
@@ -150,6 +150,9 @@
|
|||||||
"portal.user_control_panel": "User Control Panel",
|
"portal.user_control_panel": "User Control Panel",
|
||||||
"portal.user_profile": "Profile",
|
"portal.user_profile": "Profile",
|
||||||
"portal.user_logout": "Logout",
|
"portal.user_logout": "Logout",
|
||||||
|
"portal.user_role_operator": "Operator",
|
||||||
|
"portal.user_role_moderator": "Moderator",
|
||||||
|
"portal.user_role_member": "Member",
|
||||||
"portal.advertisement": "Advertisement",
|
"portal.advertisement": "Advertisement",
|
||||||
"profile.title": "Profile",
|
"profile.title": "Profile",
|
||||||
"profile.loading": "Loading profile...",
|
"profile.loading": "Loading profile...",
|
||||||
|
|||||||
Reference in New Issue
Block a user