Tighten ACP forum actions and avatar handling

This commit is contained in:
Micha
2026-01-13 00:07:25 +01:00
parent 3bb2946656
commit 98094459e3
9 changed files with 116 additions and 38 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
*.log *.log
.DS_Store .DS_Store
._*
.env .env
.env.backup .env.backup
.env.production .env.production

View File

@@ -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.

View File

@@ -68,7 +68,12 @@ class ForumController extends Controller
} }
} }
if ($parentId === null) {
Forum::whereNull('parent_id')->increment('position');
$position = 0;
} else {
$position = Forum::where('parent_id', $parentId)->max('position'); $position = Forum::where('parent_id', $parentId)->max('position');
}
$forum = Forum::create([ $forum = Forum::create([
'name' => $data['name'], 'name' => $data['name'],

View File

@@ -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) {

View File

@@ -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;

View File

@@ -905,7 +905,8 @@ 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">
<ButtonGroup size="sm" className="bb-action-group w-100">
{node.type === 'category' && ( {node.type === 'category' && (
<> <>
<Button <Button
@@ -933,6 +934,7 @@ export default function Acp({ isAdmin }) {
</ButtonGroup> </ButtonGroup>
</div> </div>
</div> </div>
</div>
{node.children?.length > 0 && {node.children?.length > 0 &&
(!node.type || node.type !== 'category' || isExpanded(node.id)) && ( (!node.type || node.type !== 'category' || isExpanded(node.id)) && (
<div className="mb-2">{renderTree(node.children, depth + 1)}</div> <div className="mb-2">{renderTree(node.children, depth + 1)}</div>

View File

@@ -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>

View File

@@ -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...",

View File

@@ -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...",