Compare commits

...

9 Commits

14 changed files with 323 additions and 504 deletions

View File

@@ -1 +1 @@
{".":"0.3.0"}
{".":"0.4.0"}

View File

@@ -1,5 +1,22 @@
# Changelog
## [0.4.0](https://github.com/chenasraf/nextcloud-forum/compare/v0.3.0...v0.4.0) (2025-11-19)
### Features
* **AppNavigation:** save collapse state to local storage ([a36da9f](https://github.com/chenasraf/nextcloud-forum/commit/a36da9f8822aa6b091e34d82cce8b56a86547b39))
* **BBCodeEditor:** add attachment disclaimer ([b0bfbbc](https://github.com/chenasraf/nextcloud-forum/commit/b0bfbbccdf04bd92d374ed31e404c9fadc23f51b))
* **BBCodeToolbar:** add emoji picker button ([255a5cf](https://github.com/chenasraf/nextcloud-forum/commit/255a5cf53dcce38c9356b30713a76e95592abe44))
* **PostReactions:** use Nextcloud emoji picker ([feeefa2](https://github.com/chenasraf/nextcloud-forum/commit/feeefa2926589cbd0c62053f1700c9bfb6bca545))
### Bug Fixes
* mobile responsiveness ([c076215](https://github.com/chenasraf/nextcloud-forum/commit/c0762158d75e6eebf0ac77a512218cf7b4119a97))
* **ProfileView:** mobile responsiveness ([67e9fb9](https://github.com/chenasraf/nextcloud-forum/commit/67e9fb9f8cdb9d1ada660b1d90e8de5aa35051de))
* **ThreadCard:** mobile responsiveness ([9525ebf](https://github.com/chenasraf/nextcloud-forum/commit/9525ebfb9705e66281898af7fcb733ba1ae8208c))
## [0.3.0](https://github.com/chenasraf/nextcloud-forum/compare/v0.2.1...v0.3.0) (2025-11-18)

View File

@@ -36,7 +36,7 @@ This app is in early stages of development. While functional, you may encounter
The forum integrates seamlessly with your Nextcloud instance, using your existing users and groups for authentication and access control.
]]></description>
<version>0.3.0</version>
<version>0.4.0</version>
<licence>agpl</licence>
<author mail="contact@casraf.dev" homepage="https://casraf.dev">Chen Asraf</author>
<namespace>Forum</namespace>

View File

@@ -103,6 +103,10 @@ export default defineComponent({
padding: 1rem;
min-height: 0;
scroll-behavior: smooth;
@media (max-width: 768px) {
padding: 0;
}
}
.bottom-spacer {
@@ -115,6 +119,7 @@ export default defineComponent({
align-items: center;
justify-content: center;
height: 100%;
margin-top: 128px;
}
</style>

View File

@@ -10,7 +10,7 @@
<NcAppNavigationItem
:name="strings.navSearch"
:to="{ path: '/search' }"
:active="isSearchActive"
:active="isPathActive('/search')"
>
<template #icon>
<MagnifyIcon :size="20" />
@@ -60,7 +60,7 @@
<NcAppNavigationItem
:name="strings.navPreferences"
:to="{ path: '/preferences' }"
:active="isPreferencesActive"
:active="isPathActive('/preferences')"
>
<template #icon>
<AccountCogIcon :size="20" />
@@ -91,7 +91,7 @@
<NcAppNavigationItem
:name="strings.navAdminDashboard"
:to="{ path: '/admin' }"
:active="isAdminDashboardActive"
:active="isPathActive('/admin')"
>
<template #icon>
<ChartLineIcon :size="20" />
@@ -101,7 +101,7 @@
<NcAppNavigationItem
:name="strings.navAdminSettings"
:to="{ path: '/admin/settings' }"
:active="isAdminSettingsActive"
:active="isPathActive('/admin/settings')"
>
<template #icon>
<CogIcon :size="20" />
@@ -111,7 +111,7 @@
<NcAppNavigationItem
:name="strings.navAdminUsers"
:to="{ path: '/admin/users' }"
:active="isAdminUsersActive"
:active="isPathActive('/admin/users', true)"
>
<template #icon>
<AccountMultipleIcon :size="20" />
@@ -121,7 +121,7 @@
<NcAppNavigationItem
:name="strings.navAdminRoles"
:to="{ path: '/admin/roles' }"
:active="isAdminRolesActive"
:active="isPathActive('/admin/roles', true)"
>
<template #icon>
<ShieldAccountIcon :size="20" />
@@ -131,7 +131,7 @@
<NcAppNavigationItem
:name="strings.navAdminCategories"
:to="{ path: '/admin/categories' }"
:active="isAdminCategoriesActive"
:active="isPathActive('/admin/categories', true)"
>
<template #icon>
<FolderIcon :size="20" />
@@ -141,7 +141,7 @@
<NcAppNavigationItem
:name="strings.navAdminBBCodes"
:to="{ path: '/admin/bbcodes' }"
:active="isAdminBBCodesActive"
:active="isPathActive('/admin/bbcodes', true)"
>
<template #icon>
<CodeBracketsIcon :size="20" />
@@ -237,14 +237,15 @@ export default defineComponent({
searchValue: '',
openHeaders: {} as Record<number, boolean>,
isAdminOpen: true,
STORAGE_KEY: 'forum_navigation_state',
strings: {
searchLabel: t('forum', 'Search'),
navHome: t('forum', 'Home'),
navSearch: t('forum', 'Search'),
navPreferences: t('forum', 'Preferences'),
navPreferences: t('forum', 'User Preferences'),
navAdmin: t('forum', 'Admin'),
navAdminDashboard: t('forum', 'Dashboard'),
navAdminSettings: t('forum', 'Settings'),
navAdminSettings: t('forum', 'Forum Settings'),
navAdminUsers: t('forum', 'Users'),
navAdminRoles: t('forum', 'Roles'),
navAdminCategories: t('forum', 'Categories'),
@@ -254,53 +255,80 @@ export default defineComponent({
},
}
},
computed: {
isSearchActive(): boolean {
return this.$route.path === '/search'
},
isPreferencesActive(): boolean {
return this.$route.path === '/preferences'
},
isAdminDashboardActive(): boolean {
return this.$route.path === '/admin'
},
isAdminSettingsActive(): boolean {
return this.$route.path === '/admin/settings'
},
isAdminUsersActive(): boolean {
return this.$route.path.startsWith('/admin/users')
},
isAdminRolesActive(): boolean {
return this.$route.path.startsWith('/admin/roles')
},
isAdminCategoriesActive(): boolean {
return this.$route.path.startsWith('/admin/categories')
},
isAdminBBCodesActive(): boolean {
return this.$route.path.startsWith('/admin/bbcodes')
},
},
async created() {
// Fetch categories for sidebar
try {
await this.fetchCategories()
// Initialize all headers as open by default
const openState: Record<number, boolean> = {}
this.categoryHeaders.forEach((header) => {
openState[header.id] = true
})
this.openHeaders = openState
// Load saved state from local storage
this.loadNavigationState()
} catch (e) {
console.error('Failed to load categories for sidebar:', e)
}
},
methods: {
loadNavigationState(): void {
try {
const savedState = localStorage.getItem(this.STORAGE_KEY)
if (savedState) {
const parsed = JSON.parse(savedState)
// Load admin section state
if (typeof parsed.isAdminOpen === 'boolean') {
this.isAdminOpen = parsed.isAdminOpen
}
// Load category headers state
if (parsed.openHeaders && typeof parsed.openHeaders === 'object') {
this.openHeaders = parsed.openHeaders
}
}
// Initialize headers that don't have saved state to open by default
const openState: Record<number, boolean> = { ...this.openHeaders }
this.categoryHeaders.forEach((header) => {
if (openState[header.id] === undefined) {
openState[header.id] = true
}
})
this.openHeaders = openState
} catch (e) {
console.error('Failed to load navigation state from local storage:', e)
// Fallback: Initialize all headers as open by default
const openState: Record<number, boolean> = {}
this.categoryHeaders.forEach((header) => {
openState[header.id] = true
})
this.openHeaders = openState
}
},
saveNavigationState(): void {
try {
const state = {
isAdminOpen: this.isAdminOpen,
openHeaders: this.openHeaders,
}
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state))
} catch (e) {
console.error('Failed to save navigation state to local storage:', e)
}
},
isPathActive(path: string, usePrefix = false): boolean {
if (usePrefix) {
return this.$route.path.startsWith(path)
}
return this.$route.path === path
},
toggleHeader(headerId: number): void {
this.openHeaders = {
...this.openHeaders,
[headerId]: !this.openHeaders[headerId],
}
this.saveNavigationState()
},
isHeaderOpen(headerId: number): boolean {
@@ -309,6 +337,7 @@ export default defineComponent({
toggleAdmin(): void {
this.isAdminOpen = !this.isAdminOpen
this.saveNavigationState()
},
isCategoryActive(category: Category): boolean {

View File

@@ -22,7 +22,6 @@ export default defineComponent({
.app-toolbar {
margin-top: 8px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
@@ -33,15 +32,22 @@ export default defineComponent({
align-items: center;
gap: 12px;
flex-wrap: wrap;
max-width: 100%;
}
.toolbar-left {
flex: 1;
flex: 1 1 auto;
min-width: 200px;
@media (max-width: 768px) {
padding-left: 32px;
}
}
.toolbar-right {
flex-shrink: 0;
margin-left: auto;
justify-content: flex-end;
}
}
</style>

View File

@@ -11,18 +11,24 @@
class="bbcode-editor-textarea"
ref="textarea"
/>
<NcNoteCard v-if="hasAttachmentBBCode" type="warning" class="attachment-disclaimer">
<span v-html="strings.attachmentDisclaimer"></span>
</NcNoteCard>
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType } from 'vue'
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import BBCodeToolbar from './BBCodeToolbar.vue'
import { t } from '@nextcloud/l10n'
export default defineComponent({
name: 'BBCodeEditor',
components: {
NcTextArea,
NcNoteCard,
BBCodeToolbar,
},
props: {
@@ -51,8 +57,21 @@ export default defineComponent({
data() {
return {
textareaElement: null as HTMLTextAreaElement | null,
strings: {
attachmentDisclaimer: t(
'forum',
"{bStart}Please note:{bEnd} Attached files will be visible to anyone in the forum, regardless of the file's sharing settings.",
{ bStart: '<strong>', bEnd: '</strong>' },
{ escape: false },
),
},
}
},
computed: {
hasAttachmentBBCode(): boolean {
return /\[attachment[^\]]*\]/i.test(this.modelValue)
},
},
mounted() {
this.updateTextareaRef()
},
@@ -102,4 +121,8 @@ export default defineComponent({
height: unset !important;
}
}
.attachment-disclaimer {
margin-top: 8px;
}
</style>

View File

@@ -14,12 +14,25 @@
</template>
</NcButton>
<NcEmojiPicker @select="handleEmojiSelect">
<NcButton
variant="tertiary"
:aria-label="strings.emojiLabel"
:title="strings.emojiLabel"
class="bbcode-button"
>
<template #icon>
<EmoticonIcon :size="20" />
</template>
</NcButton>
</NcEmojiPicker>
<div class="toolbar-spacer"></div>
<NcButton
variant="tertiary"
:aria-label="helpLabel"
:title="helpLabel"
:aria-label="strings.helpLabel"
:title="strings.helpLabel"
@click="showHelp = true"
class="bbcode-button bbcode-help-button"
>
@@ -36,6 +49,7 @@
<script lang="ts">
import { defineComponent, type PropType } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcEmojiPicker from '@nextcloud/vue/components/NcEmojiPicker'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import FormatBoldIcon from '@icons/FormatBold.vue'
import FormatItalicIcon from '@icons/FormatItalic.vue'
@@ -56,6 +70,7 @@ import FormatAlignRightIcon from '@icons/FormatAlignRight.vue'
import EyeOffIcon from '@icons/EyeOff.vue'
import FormatListBulletedIcon from '@icons/FormatListBulleted.vue'
import PaperclipIcon from '@icons/Paperclip.vue'
import EmoticonIcon from '@icons/Emoticon.vue'
import HelpCircleIcon from '@icons/HelpCircle.vue'
import BBCodeHelpDialog from './BBCodeHelpDialog.vue'
import { t } from '@nextcloud/l10n'
@@ -76,7 +91,9 @@ export default defineComponent({
name: 'BBCodeToolbar',
components: {
NcButton,
NcEmojiPicker,
BBCodeHelpDialog,
EmoticonIcon,
HelpCircleIcon,
},
props: {
@@ -89,12 +106,13 @@ export default defineComponent({
data() {
return {
showHelp: false,
strings: {
helpLabel: t('forum', 'BBCode Help'),
emojiLabel: t('forum', 'Insert emoji'),
},
}
},
computed: {
helpLabel(): string {
return t('forum', 'BBCode Help')
},
bbcodeButtons(): BBCodeButton[] {
return [
{
@@ -366,6 +384,34 @@ export default defineComponent({
// Otherwise, user simply canceled - no need to log
}
},
handleEmojiSelect(emoji: string): void {
if (!this.textareaRef) {
return
}
const textarea = this.textareaRef
const start = textarea.selectionStart
const end = textarea.selectionEnd
const beforeText = textarea.value.substring(0, start)
const afterText = textarea.value.substring(end)
const newText = beforeText + emoji + afterText
const cursorPos = beforeText.length + emoji.length
// Emit the insert event so the parent can update the model
this.$emit('insert', {
text: newText,
cursorPos,
selectedText: '',
})
// Focus the textarea after insertion
this.$nextTick(() => {
textarea.focus()
textarea.setSelectionRange(cursorPos, cursorPos)
})
},
},
})
</script>

View File

@@ -33,6 +33,10 @@ export default defineComponent({
.page-wrapper-container {
display: flex;
flex-direction: column;
@media screen and (max-width: 768px) {
padding: 0;
}
}
.page-wrapper-toolbar {

View File

@@ -14,48 +14,11 @@
</button>
<!-- Add custom reaction button -->
<div class="add-reaction">
<button
class="add-reaction-button"
:class="{ open: showPicker }"
:title="strings.addReaction"
@click="togglePicker"
>
<NcEmojiPicker @select="handleSelectEmoji" style="display: inline-block">
<button class="add-reaction-button" :title="strings.addReaction">
<span class="icon">+</span>
</button>
</div>
<!-- Emoji picker (teleported to body for proper fixed positioning) -->
<Teleport to="body">
<Transition name="fade">
<div v-if="showPicker" class="emoji-picker-overlay" @click="closePicker">
<div class="emoji-picker-container" @click.stop>
<button class="emoji-picker-close" :title="strings.close" @click="closePicker">
<Close :size="20" />
</button>
<div class="emoji-picker-content">
<h3>{{ strings.pickEmoji }}</h3>
<div class="emoji-categories">
<div v-for="group in emojiGroups" :key="group.name" class="emoji-category">
<h4 class="category-header">{{ group.name }}</h4>
<div class="emoji-grid">
<button
v-for="item in group.emojis"
:key="item.emoji"
class="emoji-option"
:title="item.title"
@click="handleSelectEmoji(item.emoji)"
>
{{ item.emoji }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</NcEmojiPicker>
</div>
</template>
@@ -64,13 +27,12 @@ import { defineComponent, type PropType } from 'vue'
import { t, n } from '@nextcloud/l10n'
import { getCurrentUser } from '@nextcloud/auth'
import { useReactions, type ReactionGroup } from '@/composables/useReactions'
import { EMOJI_GROUPS } from '@/constants/emojis'
import Close from '@icons/Close.vue'
import NcEmojiPicker from '@nextcloud/vue/components/NcEmojiPicker'
export default defineComponent({
name: 'PostReactions',
components: {
Close,
NcEmojiPicker,
},
props: {
postId: {
@@ -91,13 +53,9 @@ export default defineComponent({
return {
defaultEmojis: ['👍', '❤️', '😄', '🎉', '👏'],
reactionGroups: [...this.reactions] as ReactionGroup[],
showPicker: false,
strings: {
addReaction: t('forum', 'Add reaction'),
pickEmoji: t('forum', 'Pick an emoji'),
close: t('forum', 'Close'),
},
emojiGroups: EMOJI_GROUPS,
}
},
computed: {
@@ -153,25 +111,8 @@ export default defineComponent({
},
},
methods: {
togglePicker() {
this.showPicker = !this.showPicker
},
closePicker() {
this.showPicker = false
},
handleSelectEmoji(emoji: string) {
this.handleToggleReaction(emoji)
this.closePicker()
},
getEmojiTitle(emoji: string): string | null {
// Find the emoji title from the emoji groups
for (const group of this.emojiGroups) {
const item = group.emojis.find((e) => e.emoji === emoji)
if (item) {
return item.title
}
}
return null
},
getCount(emoji: string): number {
const group = this.reactionGroups.find((g) => g.emoji === emoji)
@@ -229,28 +170,27 @@ export default defineComponent({
getReactionTooltip(emoji: string): string {
const count = this.getCount(emoji)
const hasReacted = this.isReacted(emoji)
const title = this.getEmojiTitle(emoji) ?? emoji
if (count === 0) {
return t('forum', 'React with {title}', { title })
return t('forum', 'React with {emoji}', { emoji })
}
if (count === 1) {
return hasReacted
? t('forum', 'You reacted with {title}', { title })
: t('forum', '1 person reacted with {title}', { title })
? t('forum', 'You reacted with {emoji}', { emoji })
: t('forum', '1 person reacted with {emoji}', { emoji })
}
return hasReacted
? n(
'forum',
'You and %n other reacted with {title}',
'You and %n others reacted with {title}',
'You and %n other reacted with {emoji}',
'You and %n others reacted with {emoji}',
count - 1,
{ title },
{ emoji },
)
: n('forum', '%n person reacted with {title}', '%n people reacted with {title}', count, {
title,
: n('forum', '%n person reacted with {emoji}', '%n people reacted with {emoji}', count, {
emoji,
})
},
},
@@ -324,203 +264,40 @@ export default defineComponent({
}
}
.add-reaction {
position: relative;
.add-reaction-button {
display: flex;
align-items: center;
.add-reaction-button {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
min-width: 30px;
min-height: 30px;
border: 1px dashed var(--color-border);
background: transparent;
border-radius: 12px;
cursor: pointer;
transition: all 0.15s ease;
opacity: 0.6;
&:hover {
opacity: 1;
background: var(--color-background-hover);
border-color: var(--color-border-dark);
border-style: solid;
}
&:active {
transform: scale(0.95);
}
&.open {
opacity: 1;
background: var(--color-background-hover);
border-color: var(--color-primary-element);
border-style: solid;
}
.icon {
font-size: 1.2rem;
line-height: 1;
font-weight: bold;
color: var(--color-text-maxcontrast);
}
&:hover .icon,
&.open .icon {
color: var(--color-main-text);
}
}
}
}
// Transition animations
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<style lang="scss">
// Emoji picker overlay - not scoped because it's teleported to body
.emoji-picker-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
.emoji-picker-container {
background: var(--color-main-background);
justify-content: center;
padding: 4px 10px;
min-width: 30px;
min-height: 30px;
border: 1px dashed var(--color-border);
background: transparent;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
max-width: 90vw;
max-height: 80vh;
overflow: hidden;
position: relative;
cursor: pointer;
transition: all 0.15s ease;
opacity: 0.6;
.emoji-picker-close {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--color-text-maxcontrast);
cursor: pointer;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
z-index: 1;
padding: 0;
&:hover {
background: var(--color-background-hover);
color: var(--color-main-text);
}
&:active {
transform: scale(0.9);
}
&:hover {
opacity: 1;
background: var(--color-background-hover);
border-color: var(--color-border-dark);
border-style: solid;
}
.emoji-picker-content {
padding: 20px;
&:active {
transform: scale(0.95);
}
h3 {
margin: 0 0 16px 0;
font-size: 1.1rem;
color: var(--color-main-text);
}
.icon {
font-size: 1.2rem;
line-height: 1;
font-weight: bold;
color: var(--color-text-maxcontrast);
}
.emoji-categories {
max-height: 500px;
overflow-y: auto;
padding: 4px;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: var(--color-background-dark);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: var(--color-border-dark);
border-radius: 4px;
&:hover {
background: var(--color-text-maxcontrast);
}
}
.emoji-category {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.category-header {
margin: 0 0 12px 0;
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text-maxcontrast);
text-transform: uppercase;
letter-spacing: 0.5px;
padding-left: 4px;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
.emoji-option {
border: 1px solid transparent;
background: transparent;
border-radius: 8px;
padding: 8px;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
min-height: 40px;
&:hover {
background: var(--color-background-hover);
border-color: var(--color-border);
transform: scale(1.15);
}
&:active {
transform: scale(0.9);
}
}
}
}
}
&:hover .icon {
color: var(--color-main-text);
}
}
}

View File

@@ -137,6 +137,11 @@ export default defineComponent({
justify-content: space-between;
align-items: center;
gap: 16px;
@media (max-width: 768px) {
align-items: flex-start;
gap: 6px;
}
}
.unread-indicator {
@@ -212,16 +217,36 @@ export default defineComponent({
padding: 8px;
background: var(--color-background-hover);
border-radius: 6px;
@media (max-width: 768px) {
flex-direction: row;
padding: 6px 8px;
gap: 6px;
}
}
.stat-icon {
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
@media (max-width: 768px) {
:deep(svg) {
width: 20px;
height: 20px;
}
}
}
.stat-value {
font-weight: 600;
font-size: 1rem;
color: var(--color-main-text);
@media (max-width: 768px) {
font-size: 0.9rem;
}
}
.stat-label {
@@ -229,6 +254,10 @@ export default defineComponent({
color: var(--color-text-maxcontrast);
text-transform: uppercase;
letter-spacing: 0.5px;
@media (max-width: 768px) {
font-size: 0.65rem;
}
}
}
@@ -239,8 +268,10 @@ export default defineComponent({
.thread-stats {
flex-direction: row;
flex-wrap: wrap;
width: 100%;
justify-content: flex-start;
gap: 8px;
}
}
</style>

View File

@@ -1,190 +0,0 @@
import { t } from '@nextcloud/l10n'
/**
* Emoji groups with names and titles
*/
export interface EmojiItem {
emoji: string
title: string
}
export interface EmojiGroup {
name: string
emojis: EmojiItem[]
}
export const EMOJI_GROUPS: EmojiGroup[] = [
{
name: t('forum', 'Smileys & Emotion'),
emojis: [
{ emoji: '😀', title: t('forum', 'Grinning Face') },
{ emoji: '😃', title: t('forum', 'Grinning Face with Big Eyes') },
{ emoji: '😄', title: t('forum', 'Grinning Face with Smiling Eyes') },
{ emoji: '😁', title: t('forum', 'Beaming Face with Smiling Eyes') },
{ emoji: '😆', title: t('forum', 'Grinning Squinting Face') },
{ emoji: '😅', title: t('forum', 'Grinning Face with Sweat') },
{ emoji: '😂', title: t('forum', 'Face with Tears of Joy') },
{ emoji: '🤣', title: t('forum', 'Rolling on the Floor Laughing') },
{ emoji: '😊', title: t('forum', 'Smiling Face with Smiling Eyes') },
{ emoji: '😇', title: t('forum', 'Smiling Face with Halo') },
{ emoji: '🙂', title: t('forum', 'Slightly Smiling Face') },
{ emoji: '🙃', title: t('forum', 'Upside-Down Face') },
{ emoji: '😉', title: t('forum', 'Winking Face') },
{ emoji: '😌', title: t('forum', 'Relieved Face') },
{ emoji: '😍', title: t('forum', 'Smiling Face with Heart-Eyes') },
{ emoji: '🥰', title: t('forum', 'Smiling Face with Hearts') },
{ emoji: '😘', title: t('forum', 'Face Blowing a Kiss') },
{ emoji: '😗', title: t('forum', 'Kissing Face') },
{ emoji: '😙', title: t('forum', 'Kissing Face with Smiling Eyes') },
{ emoji: '😚', title: t('forum', 'Kissing Face with Closed Eyes') },
{ emoji: '😋', title: t('forum', 'Face Savoring Food') },
{ emoji: '😛', title: t('forum', 'Face with Tongue') },
{ emoji: '😝', title: t('forum', 'Squinting Face with Tongue') },
{ emoji: '😜', title: t('forum', 'Winking Face with Tongue') },
{ emoji: '🤪', title: t('forum', 'Zany Face') },
{ emoji: '🤨', title: t('forum', 'Face with Raised Eyebrow') },
{ emoji: '🧐', title: t('forum', 'Face with Monocle') },
{ emoji: '🤓', title: t('forum', 'Nerd Face') },
{ emoji: '😎', title: t('forum', 'Smiling Face with Sunglasses') },
{ emoji: '🤩', title: t('forum', 'Star-Struck') },
{ emoji: '🥳', title: t('forum', 'Partying Face') },
{ emoji: '😏', title: t('forum', 'Smirking Face') },
{ emoji: '😒', title: t('forum', 'Unamused Face') },
{ emoji: '😞', title: t('forum', 'Disappointed Face') },
{ emoji: '😔', title: t('forum', 'Pensive Face') },
{ emoji: '😟', title: t('forum', 'Worried Face') },
{ emoji: '😕', title: t('forum', 'Confused Face') },
{ emoji: '🙁', title: t('forum', 'Slightly Frowning Face') },
{ emoji: '😣', title: t('forum', 'Persevering Face') },
{ emoji: '😖', title: t('forum', 'Confounded Face') },
{ emoji: '😫', title: t('forum', 'Tired Face') },
{ emoji: '😩', title: t('forum', 'Weary Face') },
{ emoji: '🥺', title: t('forum', 'Pleading Face') },
{ emoji: '😢', title: t('forum', 'Crying Face') },
{ emoji: '😭', title: t('forum', 'Loudly Crying Face') },
{ emoji: '😤', title: t('forum', 'Face with Steam From Nose') },
{ emoji: '😠', title: t('forum', 'Angry Face') },
{ emoji: '😡', title: t('forum', 'Enraged Face') },
{ emoji: '🤬', title: t('forum', 'Face with Symbols on Mouth') },
{ emoji: '🤯', title: t('forum', 'Exploding Head') },
{ emoji: '😳', title: t('forum', 'Flushed Face') },
{ emoji: '🥵', title: t('forum', 'Hot Face') },
{ emoji: '🥶', title: t('forum', 'Cold Face') },
{ emoji: '😱', title: t('forum', 'Face Screaming in Fear') },
{ emoji: '😨', title: t('forum', 'Fearful Face') },
{ emoji: '😰', title: t('forum', 'Anxious Face with Sweat') },
{ emoji: '😥', title: t('forum', 'Sad but Relieved Face') },
{ emoji: '😓', title: t('forum', 'Downcast Face with Sweat') },
{ emoji: '🤗', title: t('forum', 'Smiling Face with Open Hands') },
{ emoji: '🤔', title: t('forum', 'Thinking Face') },
{ emoji: '🤭', title: t('forum', 'Face with Hand Over Mouth') },
{ emoji: '🤫', title: t('forum', 'Shushing Face') },
{ emoji: '🤥', title: t('forum', 'Lying Face') },
{ emoji: '😶', title: t('forum', 'Face Without Mouth') },
{ emoji: '😐', title: t('forum', 'Neutral Face') },
{ emoji: '😑', title: t('forum', 'Expressionless Face') },
{ emoji: '😬', title: t('forum', 'Grimacing Face') },
{ emoji: '🙄', title: t('forum', 'Face with Rolling Eyes') },
{ emoji: '😯', title: t('forum', 'Hushed Face') },
{ emoji: '😦', title: t('forum', 'Frowning Face with Open Mouth') },
{ emoji: '😧', title: t('forum', 'Anguished Face') },
{ emoji: '😮', title: t('forum', 'Face with Open Mouth') },
{ emoji: '😲', title: t('forum', 'Astonished Face') },
{ emoji: '🥱', title: t('forum', 'Yawning Face') },
{ emoji: '😴', title: t('forum', 'Sleeping Face') },
{ emoji: '🤤', title: t('forum', 'Drooling Face') },
{ emoji: '😪', title: t('forum', 'Sleepy Face') },
{ emoji: '😵', title: t('forum', 'Face with Crossed-Out Eyes') },
{ emoji: '🤐', title: t('forum', 'Zipper-Mouth Face') },
{ emoji: '🥴', title: t('forum', 'Woozy Face') },
],
},
{
name: t('forum', 'Gestures & Hands'),
emojis: [
{ emoji: '👋', title: t('forum', 'Waving Hand') },
{ emoji: '🤚', title: t('forum', 'Raised Back of Hand') },
{ emoji: '🖐', title: t('forum', 'Hand with Fingers Splayed') },
{ emoji: '✋', title: t('forum', 'Raised Hand') },
{ emoji: '🖖', title: t('forum', 'Vulcan Salute') },
{ emoji: '👌', title: t('forum', 'OK Hand') },
{ emoji: '🤌', title: t('forum', 'Pinched Fingers') },
{ emoji: '🤏', title: t('forum', 'Pinching Hand') },
{ emoji: '✌️', title: t('forum', 'Victory Hand') },
{ emoji: '🤞', title: t('forum', 'Crossed Fingers') },
{ emoji: '🤟', title: t('forum', 'Love-You Gesture') },
{ emoji: '🤘', title: t('forum', 'Sign of the Horns') },
{ emoji: '🤙', title: t('forum', 'Call Me Hand') },
{ emoji: '👈', title: t('forum', 'Backhand Index Pointing Left') },
{ emoji: '👉', title: t('forum', 'Backhand Index Pointing Right') },
{ emoji: '👆', title: t('forum', 'Backhand Index Pointing Up') },
{ emoji: '🖕', title: t('forum', 'Middle Finger') },
{ emoji: '👇', title: t('forum', 'Backhand Index Pointing Down') },
{ emoji: '☝️', title: t('forum', 'Index Pointing Up') },
{ emoji: '👍', title: t('forum', 'Thumbs Up') },
{ emoji: '👎', title: t('forum', 'Thumbs Down') },
{ emoji: '✊', title: t('forum', 'Raised Fist') },
{ emoji: '👊', title: t('forum', 'Oncoming Fist') },
{ emoji: '🤛', title: t('forum', 'Left-Facing Fist') },
{ emoji: '🤜', title: t('forum', 'Right-Facing Fist') },
{ emoji: '👏', title: t('forum', 'Clapping Hands') },
{ emoji: '🙌', title: t('forum', 'Raising Hands') },
{ emoji: '👐', title: t('forum', 'Open Hands') },
{ emoji: '🤲', title: t('forum', 'Palms Up Together') },
{ emoji: '🤝', title: t('forum', 'Handshake') },
{ emoji: '🙏', title: t('forum', 'Folded Hands') },
],
},
{
name: t('forum', 'Hearts & Love'),
emojis: [
{ emoji: '❤️', title: t('forum', 'Red Heart') },
{ emoji: '💛', title: t('forum', 'Yellow Heart') },
{ emoji: '💙', title: t('forum', 'Blue Heart') },
{ emoji: '💜', title: t('forum', 'Purple Heart') },
{ emoji: '🧡', title: t('forum', 'Orange Heart') },
{ emoji: '💚', title: t('forum', 'Green Heart') },
{ emoji: '🖤', title: t('forum', 'Black Heart') },
{ emoji: '🤍', title: t('forum', 'White Heart') },
{ emoji: '🤎', title: t('forum', 'Brown Heart') },
{ emoji: '💔', title: t('forum', 'Broken Heart') },
{ emoji: '❣️', title: t('forum', 'Heart Exclamation') },
{ emoji: '💕', title: t('forum', 'Two Hearts') },
{ emoji: '💞', title: t('forum', 'Revolving Hearts') },
{ emoji: '💓', title: t('forum', 'Beating Heart') },
{ emoji: '💗', title: t('forum', 'Growing Heart') },
{ emoji: '💖', title: t('forum', 'Sparkling Heart') },
{ emoji: '💘', title: t('forum', 'Heart with Arrow') },
{ emoji: '💝', title: t('forum', 'Heart with Ribbon') },
{ emoji: '💟', title: t('forum', 'Heart Decoration') },
],
},
{
name: t('forum', 'Symbols'),
emojis: [
{ emoji: '🎉', title: t('forum', 'Party Popper') },
{ emoji: '🎊', title: t('forum', 'Confetti Ball') },
{ emoji: '🎈', title: t('forum', 'Balloon') },
{ emoji: '🎁', title: t('forum', 'Wrapped Gift') },
{ emoji: '🏆', title: t('forum', 'Trophy') },
{ emoji: '🥇', title: t('forum', '1st Place Medal') },
{ emoji: '🥈', title: t('forum', '2nd Place Medal') },
{ emoji: '🥉', title: t('forum', '3rd Place Medal') },
{ emoji: '⭐', title: t('forum', 'Star') },
{ emoji: '🌟', title: t('forum', 'Glowing Star') },
{ emoji: '✨', title: t('forum', 'Sparkles') },
{ emoji: '💫', title: t('forum', 'Dizzy') },
{ emoji: '🔥', title: t('forum', 'Fire') },
{ emoji: '💯', title: t('forum', 'Hundred Points') },
{ emoji: '✅', title: t('forum', 'Check Mark Button') },
{ emoji: '❌', title: t('forum', 'Cross Mark') },
{ emoji: '⚠️', title: t('forum', 'Warning') },
{ emoji: '❗', title: t('forum', 'Exclamation Mark') },
{ emoji: '❓', title: t('forum', 'Question Mark') },
{ emoji: '💬', title: t('forum', 'Speech Balloon') },
{ emoji: '💭', title: t('forum', 'Thought Balloon') },
{ emoji: '👀', title: t('forum', 'Eyes') },
],
},
]

View File

@@ -363,11 +363,19 @@ export default defineComponent({
<style scoped lang="scss">
.profile-view {
@media (max-width: 768px) {
padding: 0 8px;
}
.center {
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
@media (max-width: 768px) {
padding: 16px;
}
}
.ml-8 {
@@ -380,6 +388,10 @@ export default defineComponent({
.mt-24 {
margin-top: 24px;
@media (max-width: 768px) {
margin-top: 16px;
}
}
.muted {
@@ -394,16 +406,38 @@ export default defineComponent({
background: var(--color-main-background);
border: 1px solid var(--color-border);
border-radius: 8px;
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 16px;
}
}
.user-avatar {
@media (max-width: 768px) {
align-self: center;
}
}
.user-info {
flex: 1;
width: 100%;
@media (max-width: 768px) {
text-align: center;
}
}
.user-name {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
@media (max-width: 768px) {
font-size: 20px;
}
}
.user-meta {
@@ -412,6 +446,17 @@ export default defineComponent({
gap: 8px;
color: var(--color-text-maxcontrast);
font-size: 14px;
flex-wrap: wrap;
@media (max-width: 768px) {
justify-content: center;
font-size: 13px;
}
}
.meta-item {
display: inline-flex;
align-items: center;
}
.meta-label {
@@ -425,6 +470,10 @@ export default defineComponent({
.meta-divider {
color: var(--color-text-maxcontrast);
@media (max-width: 480px) {
display: none;
}
}
.profile-tabs {
@@ -444,6 +493,12 @@ export default defineComponent({
color: var(--color-text-maxcontrast);
transition: all 0.2s;
border-radius: 0;
flex: 1;
@media (max-width: 768px) {
padding: 12px 16px;
font-size: 13px;
}
&:hover {
color: var(--color-text-light);
@@ -481,6 +536,10 @@ export default defineComponent({
cursor: pointer;
transition: all 0.2s;
@media (max-width: 768px) {
padding: 12px;
}
&:hover {
background: var(--color-background-hover);
border-color: var(--color-primary-element);
@@ -494,12 +553,24 @@ export default defineComponent({
margin-bottom: 12px;
font-size: 14px;
color: var(--color-text-maxcontrast);
gap: 8px;
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
gap: 4px;
font-size: 13px;
}
}
.post-thread {
strong {
color: var(--color-text-light);
}
@media (max-width: 768px) {
word-break: break-word;
}
}
.post-content {

View File

@@ -1 +1 @@
0.3.0
0.4.0