feat(dashboard): Filter und Sortierung für die Logbuchliste
Hilft bei vielen Logbüchern: Suche nach Name/Jahr/Datum sowie Sortierung nach Name oder Datum in beide Richtungen. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1371,6 +1371,148 @@ html.scheme-dark .themed-select-option.is-selected {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-list-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--app-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-group {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 140px;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--app-border-subtle);
|
||||||
|
background: var(--app-surface-alt);
|
||||||
|
color: var(--app-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, background-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-btn:hover {
|
||||||
|
border-color: var(--app-border);
|
||||||
|
color: var(--app-text-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-btn.is-active {
|
||||||
|
border-color: var(--app-accent-border);
|
||||||
|
background: var(--app-accent-bg);
|
||||||
|
color: var(--app-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--app-accent-focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-btn span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--app-text-muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-input-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
color: var(--app-text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-input {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 42px;
|
||||||
|
padding-right: 42px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-input::-webkit-search-cancel-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--app-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s, background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-clear:hover {
|
||||||
|
color: var(--app-text-heading);
|
||||||
|
background: var(--app-accent-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-clear:focus-visible {
|
||||||
|
outline: 2px solid var(--app-accent-focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-filter-meta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--app-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.section-title-bar {
|
.section-title-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -2322,6 +2464,14 @@ html.scheme-dark .themed-select-option.is-selected {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sort-group {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.logbooks-grid {
|
.logbooks-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSyncIndicator } from '../hooks/useSyncIndicator.js'
|
import { useSyncIndicator } from '../hooks/useSyncIndicator.js'
|
||||||
import { fetchLogbooks, createLogbook, deleteLogbook, updateLogbookTitle, type DecryptedLogbook } from '../services/logbook.js'
|
import { fetchLogbooks, createLogbook, deleteLogbook, updateLogbookTitle, type DecryptedLogbook } from '../services/logbook.js'
|
||||||
@@ -7,7 +7,7 @@ import BetaBadge from './BetaBadge.tsx'
|
|||||||
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
||||||
import { logoutUser } from '../services/auth.js'
|
import { logoutUser } from '../services/auth.js'
|
||||||
import { useDialog } from './ModalDialog.tsx'
|
import { useDialog } from './ModalDialog.tsx'
|
||||||
import { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff } from 'lucide-react'
|
import { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff, Search, X, CalendarDays, CaseSensitive, ArrowUp, ArrowDown } from 'lucide-react'
|
||||||
import DisclaimerHeaderButton from './DisclaimerHeaderButton.tsx'
|
import DisclaimerHeaderButton from './DisclaimerHeaderButton.tsx'
|
||||||
import FeedbackHeaderButton from './FeedbackHeaderButton.tsx'
|
import FeedbackHeaderButton from './FeedbackHeaderButton.tsx'
|
||||||
|
|
||||||
@@ -17,6 +17,46 @@ interface LogbookDashboardProps {
|
|||||||
onOpenProfile: () => void
|
onOpenProfile: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logbookMatchesFilter(lb: DecryptedLogbook, query: string, locale: string): boolean {
|
||||||
|
const q = query.trim().toLowerCase()
|
||||||
|
if (!q) return true
|
||||||
|
|
||||||
|
if (lb.title.toLowerCase().includes(q)) return true
|
||||||
|
|
||||||
|
const updated = new Date(lb.updatedAt)
|
||||||
|
const year = updated.getFullYear().toString()
|
||||||
|
if (year.includes(q)) return true
|
||||||
|
|
||||||
|
const dateLabel = updated.toLocaleDateString(locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
}).toLowerCase()
|
||||||
|
if (dateLabel.includes(q)) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogbookSortKey = 'name' | 'date'
|
||||||
|
type LogbookSortDirection = 'asc' | 'desc'
|
||||||
|
|
||||||
|
function sortLogbooks(
|
||||||
|
items: DecryptedLogbook[],
|
||||||
|
sortBy: LogbookSortKey,
|
||||||
|
direction: LogbookSortDirection,
|
||||||
|
locale: string
|
||||||
|
): DecryptedLogbook[] {
|
||||||
|
const sorted = [...items]
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const cmp =
|
||||||
|
sortBy === 'name'
|
||||||
|
? a.title.localeCompare(b.title, locale, { sensitivity: 'base' })
|
||||||
|
: new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||||
|
return direction === 'asc' ? cmp : -cmp
|
||||||
|
})
|
||||||
|
return sorted
|
||||||
|
}
|
||||||
|
|
||||||
export default function LogbookDashboard({ onSelectLogbook, onLogout, onOpenProfile }: LogbookDashboardProps) {
|
export default function LogbookDashboard({ onSelectLogbook, onLogout, onOpenProfile }: LogbookDashboardProps) {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const { showConfirm } = useDialog()
|
const { showConfirm } = useDialog()
|
||||||
@@ -28,6 +68,10 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout, onOpenProf
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [filterQuery, setFilterQuery] = useState('')
|
||||||
|
const [sortBy, setSortBy] = useState<LogbookSortKey>('date')
|
||||||
|
const [sortDirection, setSortDirection] = useState<LogbookSortDirection>('desc')
|
||||||
|
const filterInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [online, setOnline] = useState(navigator.onLine)
|
const [online, setOnline] = useState(navigator.onLine)
|
||||||
const [username] = useState(localStorage.getItem('active_username') || 'Skipper')
|
const [username] = useState(localStorage.getItem('active_username') || 'Skipper')
|
||||||
|
|
||||||
@@ -156,6 +200,25 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout, onOpenProf
|
|||||||
const ownedLogbooks = logbooks.filter((lb) => !lb.isShared)
|
const ownedLogbooks = logbooks.filter((lb) => !lb.isShared)
|
||||||
const sharedLogbooks = logbooks.filter((lb) => lb.isShared)
|
const sharedLogbooks = logbooks.filter((lb) => lb.isShared)
|
||||||
|
|
||||||
|
const filterActive = filterQuery.trim().length > 0
|
||||||
|
const filteredOwnedLogbooks = useMemo(
|
||||||
|
() => ownedLogbooks.filter((lb) => logbookMatchesFilter(lb, filterQuery, i18n.language)),
|
||||||
|
[ownedLogbooks, filterQuery, i18n.language]
|
||||||
|
)
|
||||||
|
const filteredSharedLogbooks = useMemo(
|
||||||
|
() => sharedLogbooks.filter((lb) => logbookMatchesFilter(lb, filterQuery, i18n.language)),
|
||||||
|
[sharedLogbooks, filterQuery, i18n.language]
|
||||||
|
)
|
||||||
|
const sortedOwnedLogbooks = useMemo(
|
||||||
|
() => sortLogbooks(filteredOwnedLogbooks, sortBy, sortDirection, i18n.language),
|
||||||
|
[filteredOwnedLogbooks, sortBy, sortDirection, i18n.language]
|
||||||
|
)
|
||||||
|
const sortedSharedLogbooks = useMemo(
|
||||||
|
() => sortLogbooks(filteredSharedLogbooks, sortBy, sortDirection, i18n.language),
|
||||||
|
[filteredSharedLogbooks, sortBy, sortDirection, i18n.language]
|
||||||
|
)
|
||||||
|
const filteredLogbookCount = sortedOwnedLogbooks.length + sortedSharedLogbooks.length
|
||||||
|
|
||||||
const renderLogbookCard = (lb: DecryptedLogbook) => {
|
const renderLogbookCard = (lb: DecryptedLogbook) => {
|
||||||
const isEditingTitle = editingLogbookId === lb.id
|
const isEditingTitle = editingLogbookId === lb.id
|
||||||
|
|
||||||
@@ -376,18 +439,116 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout, onOpenProf
|
|||||||
) : logbooks.length === 0 ? (
|
) : logbooks.length === 0 ? (
|
||||||
<div className="dashboard-status-msg glass">{t('dashboard.no_logbooks')}</div>
|
<div className="dashboard-status-msg glass">{t('dashboard.no_logbooks')}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="logbook-sections">
|
<>
|
||||||
{ownedLogbooks.length > 0 && renderLogbookSection(
|
<div className="dashboard-list-controls">
|
||||||
sharedLogbooks.length > 0 ? t('dashboard.section_owned') : t('dashboard.title'),
|
<div className="dashboard-filter-bar">
|
||||||
ownedLogbooks
|
<label className="dashboard-filter-label" htmlFor="logbook-list-filter">
|
||||||
|
{t('dashboard.filter_label')}
|
||||||
|
</label>
|
||||||
|
<div className="dashboard-filter-input-wrap">
|
||||||
|
<Search size={18} className="dashboard-filter-icon" aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
ref={filterInputRef}
|
||||||
|
id="logbook-list-filter"
|
||||||
|
type="search"
|
||||||
|
className="input-text dashboard-filter-input"
|
||||||
|
placeholder={t('dashboard.filter_placeholder')}
|
||||||
|
value={filterQuery}
|
||||||
|
onChange={(e) => setFilterQuery(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
aria-describedby={filterActive ? 'logbook-filter-status' : undefined}
|
||||||
|
/>
|
||||||
|
{filterActive && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="dashboard-filter-clear"
|
||||||
|
onClick={() => {
|
||||||
|
setFilterQuery('')
|
||||||
|
filterInputRef.current?.focus()
|
||||||
|
}}
|
||||||
|
title={t('dashboard.filter_clear')}
|
||||||
|
aria-label={t('dashboard.filter_clear')}
|
||||||
|
>
|
||||||
|
<X size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
{sharedLogbooks.length > 0 && renderLogbookSection(
|
</div>
|
||||||
|
{filterActive && (
|
||||||
|
<p id="logbook-filter-status" className="dashboard-filter-meta" role="status">
|
||||||
|
{t('dashboard.filter_results', { count: filteredLogbookCount })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dashboard-sort-bar">
|
||||||
|
<span className="dashboard-sort-label">{t('dashboard.sort_label')}</span>
|
||||||
|
<div className="dashboard-sort-row">
|
||||||
|
<div className="dashboard-sort-group" role="group" aria-label={t('dashboard.sort_by_label')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dashboard-sort-btn${sortBy === 'name' ? ' is-active' : ''}`}
|
||||||
|
onClick={() => setSortBy('name')}
|
||||||
|
aria-pressed={sortBy === 'name'}
|
||||||
|
title={t('dashboard.sort_by_name')}
|
||||||
|
>
|
||||||
|
<CaseSensitive size={16} aria-hidden="true" />
|
||||||
|
<span>{t('dashboard.sort_by_name')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dashboard-sort-btn${sortBy === 'date' ? ' is-active' : ''}`}
|
||||||
|
onClick={() => setSortBy('date')}
|
||||||
|
aria-pressed={sortBy === 'date'}
|
||||||
|
title={t('dashboard.sort_by_date')}
|
||||||
|
>
|
||||||
|
<CalendarDays size={16} aria-hidden="true" />
|
||||||
|
<span>{t('dashboard.sort_by_date')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dashboard-sort-group" role="group" aria-label={t('dashboard.sort_dir_label')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dashboard-sort-btn${sortDirection === 'asc' ? ' is-active' : ''}`}
|
||||||
|
onClick={() => setSortDirection('asc')}
|
||||||
|
aria-pressed={sortDirection === 'asc'}
|
||||||
|
title={sortBy === 'name' ? t('dashboard.sort_name_asc') : t('dashboard.sort_date_asc')}
|
||||||
|
>
|
||||||
|
<ArrowUp size={16} aria-hidden="true" />
|
||||||
|
<span>{t('dashboard.sort_asc')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dashboard-sort-btn${sortDirection === 'desc' ? ' is-active' : ''}`}
|
||||||
|
onClick={() => setSortDirection('desc')}
|
||||||
|
aria-pressed={sortDirection === 'desc'}
|
||||||
|
title={sortBy === 'name' ? t('dashboard.sort_name_desc') : t('dashboard.sort_date_desc')}
|
||||||
|
>
|
||||||
|
<ArrowDown size={16} aria-hidden="true" />
|
||||||
|
<span>{t('dashboard.sort_desc')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filterActive && filteredLogbookCount === 0 ? (
|
||||||
|
<div className="dashboard-status-msg glass">{t('dashboard.filter_no_results')}</div>
|
||||||
|
) : (
|
||||||
|
<div className="logbook-sections">
|
||||||
|
{sortedOwnedLogbooks.length > 0 && renderLogbookSection(
|
||||||
|
sortedSharedLogbooks.length > 0 ? t('dashboard.section_owned') : t('dashboard.title'),
|
||||||
|
sortedOwnedLogbooks
|
||||||
|
)}
|
||||||
|
{sortedSharedLogbooks.length > 0 && renderLogbookSection(
|
||||||
t('dashboard.section_shared'),
|
t('dashboard.section_shared'),
|
||||||
sharedLogbooks,
|
sortedSharedLogbooks,
|
||||||
t('dashboard.section_shared_hint')
|
t('dashboard.section_shared_hint')
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -304,7 +304,23 @@
|
|||||||
"edit_title": "Logbuch umbenennen",
|
"edit_title": "Logbuch umbenennen",
|
||||||
"edit_placeholder": "Neuer Name des Logbuchs",
|
"edit_placeholder": "Neuer Name des Logbuchs",
|
||||||
"edit_success": "Logbuch erfolgreich umbenannt",
|
"edit_success": "Logbuch erfolgreich umbenannt",
|
||||||
"edit_btn": "Umbenennen"
|
"edit_btn": "Umbenennen",
|
||||||
|
"filter_label": "Logbücher filtern",
|
||||||
|
"filter_placeholder": "Name, Jahr oder Datum …",
|
||||||
|
"filter_clear": "Filter zurücksetzen",
|
||||||
|
"filter_results": "{{count}} Treffer",
|
||||||
|
"filter_no_results": "Keine Logbücher passen zu deiner Suche. Probiere einen anderen Namen oder ein anderes Jahr.",
|
||||||
|
"sort_label": "Sortieren",
|
||||||
|
"sort_by_label": "Sortieren nach",
|
||||||
|
"sort_by_name": "Name",
|
||||||
|
"sort_by_date": "Datum",
|
||||||
|
"sort_dir_label": "Reihenfolge",
|
||||||
|
"sort_asc": "Aufsteigend",
|
||||||
|
"sort_desc": "Absteigend",
|
||||||
|
"sort_name_asc": "Name A bis Z",
|
||||||
|
"sort_name_desc": "Name Z bis A",
|
||||||
|
"sort_date_asc": "Älteste zuerst",
|
||||||
|
"sort_date_desc": "Neueste zuerst"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Benutzerprofil",
|
"title": "Benutzerprofil",
|
||||||
|
|||||||
@@ -304,7 +304,23 @@
|
|||||||
"edit_title": "Rename Logbook",
|
"edit_title": "Rename Logbook",
|
||||||
"edit_placeholder": "New name of the logbook",
|
"edit_placeholder": "New name of the logbook",
|
||||||
"edit_success": "Logbook renamed successfully",
|
"edit_success": "Logbook renamed successfully",
|
||||||
"edit_btn": "Rename"
|
"edit_btn": "Rename",
|
||||||
|
"filter_label": "Filter logbooks",
|
||||||
|
"filter_placeholder": "Name, year or date …",
|
||||||
|
"filter_clear": "Clear filter",
|
||||||
|
"filter_results": "{{count}} matches",
|
||||||
|
"filter_no_results": "No logbooks match your search. Try a different name or year.",
|
||||||
|
"sort_label": "Sort",
|
||||||
|
"sort_by_label": "Sort by",
|
||||||
|
"sort_by_name": "Name",
|
||||||
|
"sort_by_date": "Date",
|
||||||
|
"sort_dir_label": "Order",
|
||||||
|
"sort_asc": "Ascending",
|
||||||
|
"sort_desc": "Descending",
|
||||||
|
"sort_name_asc": "Name A to Z",
|
||||||
|
"sort_name_desc": "Name Z to A",
|
||||||
|
"sort_date_asc": "Oldest first",
|
||||||
|
"sort_date_desc": "Newest first"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "User profile",
|
"title": "User profile",
|
||||||
|
|||||||
Reference in New Issue
Block a user