Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {
}

group = "de.chojo"
version = "2.7.0"
version = "2.8.0"

repositories {
mavenLocal()
Expand Down
57 changes: 57 additions & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,14 @@ class ApiClient {
await this.axiosInstance.patch('/user/settings', settings);
}

public async updateUserVoteGuild(voteGuild: string): Promise<void> {
await this.axiosInstance.patch('/user/settings/voteguild', { voteGuild: Number(voteGuild) });
}

public async updateUserPublicProfile(publicProfile: boolean): Promise<void> {
await this.axiosInstance.patch('/user/settings/publicprofile', { publicProfile });
}

// User Purchases (Ko-fi)
public async getUserPurchases(): Promise<Types.KofiPurchasePOJO[]> {
const response = await this.axiosInstance.get<Types.KofiPurchasePOJO[]>('/user/purchases');
Expand Down Expand Up @@ -788,6 +796,55 @@ class ApiClient {
return response.data;
}

// User Profile
public async getUserProfileMe(): Promise<Types.UserProfilePOJO> {
const response = await this.axiosInstance.get<Types.UserProfilePOJO>('/guild/user/profile/me');
return response.data;
}

public async getUserProfile(userId: string): Promise<Types.UserProfilePOJO> {
const response = await this.axiosInstance.get<Types.UserProfilePOJO>(`/guild/user/profile/${userId}`);
return response.data;
}

// Public Profile (unauthenticated)
public async getPublicProfile(guildId: string, userId: string): Promise<Types.PublicProfilePOJO> {
const response = await this.axiosInstance.get<Types.PublicProfilePOJO>('/public/profile', {
params: { guildId, userId }
});
return response.data;
}

// Guild Ranking
public async getGuildRankingGiven(page: number = 0, pageSize: number = 10, mode?: Types.ReputationMode): Promise<Types.RankingPagePOJO> {
const response = await this.axiosInstance.get<Types.RankingPagePOJO>('/guild/ranking/given', {
params: { page, pageSize, ...(mode !== undefined && { mode }) }
});
return response.data;
}

public async getGuildRankingReceived(page: number = 0, pageSize: number = 10, mode?: Types.ReputationMode): Promise<Types.RankingPagePOJO> {
const response = await this.axiosInstance.get<Types.RankingPagePOJO>('/guild/ranking/received', {
params: { page, pageSize, ...(mode !== undefined && { mode }) }
});
return response.data;
}

// User Ranking
public async getUserRankingGiven(page: number = 0, pageSize: number = 10, mode?: Types.ReputationMode, userId?: string): Promise<Types.RankingPagePOJO> {
const response = await this.axiosInstance.get<Types.RankingPagePOJO>('/guild/user/ranking/given', {
params: { page, pageSize, ...(mode !== undefined && { mode }), ...(userId !== undefined && { userId }) }
});
return response.data;
}

public async getUserRankingReceived(page: number = 0, pageSize: number = 10, mode?: Types.ReputationMode, userId?: string): Promise<Types.RankingPagePOJO> {
const response = await this.axiosInstance.get<Types.RankingPagePOJO>('/guild/user/ranking/received', {
params: { page, pageSize, ...(mode !== undefined && { mode }), ...(userId !== undefined && { userId }) }
});
return response.data;
}

public async startScan(): Promise<void> {
await this.axiosInstance.post('/guild/scan/start');
}
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,58 @@ export interface DashboardStatsPOJO {
topUsers: RankingEntryPOJO[]
}

export interface RankingPagePOJO {
pages: number;
page: number;
entries: RankingEntryPOJO[];
}

export interface RankingEntryStatPOJO {
rank: number;
value: number;
member: MemberPOJO;
}

export interface ChannelStatsPOJO {
channelId: string;
count: number;
}

export interface DetailedProfilePOJO {
topDonors: RankingEntryStatPOJO[];
topReceivers: RankingEntryStatPOJO[];
mostGivenChannels: ChannelStatsPOJO[];
mostReceivedChannels: ChannelStatsPOJO[];
}

export interface AdminProfilePOJO {
rawReputation: number;
repOffset: number;
donated: number;
}

export interface UserProfilePOJO {
member: MemberPOJO;
rank: number;
rankDonated: number;
reputation: number;
donated: number;
level: string | null;
currentProgress: number;
nextLevelReputation: number | null;
detailedProfile: DetailedProfilePOJO | null;
adminProfile: AdminProfilePOJO | null;
}

export interface PublicProfilePOJO {
member: MemberPOJO;
rank: number;
reputation: number;
}

export interface UserSettingsPOJO {
voteGuild: string;
publicProfile: boolean;
}

export interface SKUId {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/PremiumFeatureWarning.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const displayMessage = computed(() => {
<li v-for="sku in requiredSkus" :key="sku.id">{{ sku.name }}</li>
</ul>
</div>
<p class="text-sm text-yellow-700 italic">{{ t('premiumFeature.perServer') }}</p>
<p class="mt-3 text-sm text-yellow-700">
{{ t('premiumFeature.tokenShopHint') }}
<router-link to="/guild/token-shop" class="font-semibold text-indigo-600 hover:text-indigo-500 underline transition-colors">
Expand All @@ -93,6 +94,7 @@ const displayMessage = computed(() => {
<ul v-if="requiredSkus.length > 0" class="list-disc list-inside text-yellow-700 ml-2">
<li v-for="sku in requiredSkus" :key="sku.id">{{ sku.name }}</li>
</ul>
<p class="text-xs text-yellow-700 italic">{{ t('premiumFeature.perServer') }}</p>
<p class="mt-2 text-xs text-yellow-700 italic">
{{ t('premiumFeature.tokenShopHint') }}
<router-link to="/guild/token-shop" class="font-semibold text-indigo-600 hover:text-indigo-500 underline transition-colors">
Expand Down
88 changes: 33 additions & 55 deletions frontend/src/components/SettingsHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {useI18n} from 'vue-i18n'
import {onMounted, ref, watch} from 'vue'
import {api} from '@/api'
import {useSession} from '@/composables/useSession'
import SubHeader from '@/components/SubHeader.vue'
import SubHeaderTab from '@/components/SubHeaderTab.vue'

const {t} = useI18n()
const {userSession} = useSession()
Expand Down Expand Up @@ -45,59 +47,35 @@ watch(userSession, checkProblems)
</script>

<template>
<div v-if="isGuildAdmin" class="bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 transition-colors">
<div class="mx-auto px-4" style="max-width: 1600px;">
<nav class="flex">
<router-link
to="/settings/edit"
class="px-6 py-3 text-sm font-medium transition-colors border-b-2"
:class="[
$route.path.startsWith('/settings/edit') && !$route.path.endsWith('/problems') && !$route.path.endsWith('/audit-log')
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
]"
>
{{ t('navigation.settings') }}
</router-link>
<router-link
to="/settings/preset"
class="px-6 py-3 text-sm font-medium transition-colors border-b-2"
:class="[
$route.path.startsWith('/settings/preset')
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
]"
>
{{ t('navigation.presets') }}
</router-link>
<router-link
to="/settings/audit-log"
class="px-6 py-3 text-sm font-medium transition-colors border-b-2"
:class="[
$route.path.endsWith('/audit-log')
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
]"
>
{{ t('settings.auditLog') }}
</router-link>
<router-link
to="/settings/problems"
class="px-6 py-3 text-sm font-medium transition-colors border-b-2 flex items-center gap-2"
:class="[
$route.path.endsWith('/problems')
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
]"
>
<span>{{ t('settings.problems') }}</span>
<font-awesome-icon
v-if="hasProblems"
icon="circle-exclamation"
class="text-red-500"
/>
</router-link>
</nav>
</div>
</div>
<SubHeader v-if="isGuildAdmin">
<SubHeaderTab
to="/settings/edit"
:active="$route.path.startsWith('/settings/edit') && !$route.path.endsWith('/problems') && !$route.path.endsWith('/audit-log')"
>
{{ t('navigation.settings') }}
</SubHeaderTab>
<SubHeaderTab
to="/settings/preset"
:active="$route.path.startsWith('/settings/preset')"
>
{{ t('navigation.presets') }}
</SubHeaderTab>
<SubHeaderTab
to="/settings/audit-log"
:active="$route.path.endsWith('/audit-log')"
>
{{ t('settings.auditLog') }}
</SubHeaderTab>
<SubHeaderTab
to="/settings/problems"
:active="$route.path.endsWith('/problems')"
>
<span>{{ t('settings.problems') }}</span>
<font-awesome-icon
v-if="hasProblems"
icon="circle-exclamation"
class="text-red-500"
/>
</SubHeaderTab>
</SubHeader>
</template>
14 changes: 14 additions & 0 deletions frontend/src/components/SubHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
*
* Copyright (C) RainbowDashLabs and Contributor
*/
<template>
<div class="bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 transition-colors">
<div class="mx-auto px-4" style="max-width: 1600px;">
<nav class="flex">
<slot/>
</nav>
</div>
</div>
</template>
42 changes: 42 additions & 0 deletions frontend/src/components/SubHeaderTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
*
* Copyright (C) RainbowDashLabs and Contributor
*/
<script lang="ts" setup>
const props = defineProps<{
to?: string
active: boolean
}>()

const emit = defineEmits<{
click: []
}>()
</script>

<template>
<router-link
v-if="props.to"
:to="props.to"
class="px-6 py-3 text-sm font-medium transition-colors border-b-2 flex items-center gap-2 flex-1 text-center justify-center"
:class="[
active
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
]"
>
<slot/>
</router-link>
<button
v-else
class="px-6 py-3 text-sm font-medium transition-colors border-b-2 flex items-center gap-2 flex-1 text-center justify-center"
:class="[
active
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
]"
@click="emit('click')"
>
<slot/>
</button>
</template>
46 changes: 46 additions & 0 deletions frontend/src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,48 @@
"save": "Uložit"
},
"dashboard": {
"profile": "Profil",
"profileView": {
"detailedProfile": "Podrobný profil",
"error": "Nepodařilo se načíst profil.",
"given": "Vzhledem k tomu, že",
"maxLevel": "Byla dosažena maximální úroveň",
"mostGivenChannels": "Nejčastěji sledované kanály",
"mostReceivedChannels": "Nejčastěji sledované kanály",
"progress": "Postup na další úroveň",
"rank": "Hodnocení: {rank}",
"rankGiven": "Pořadí (zadané)",
"rankReceived": "Pořadí (obdržených)",
"rawReputation": "Raw Reputation",
"received": "Přijato",
"repOffset": "Kompenzace reputace",
"shareProfile": "Sdílet",
"shareProfileCopied": "Odkaz byl zkopírován do schránky!",
"topDonors": "Největší dárci",
"topReceivers": "Nejlepší přijímače"
},
"publicProfileView": {
"error": "Nepodařilo se načíst profil.",
"guildFooter": "Profil na {guild}",
"privateProfile": "Tento profil je soukromý.",
"rank": "Pořadí",
"reputation": "Pověst"
},
"rank": "Hodnost",
"ranking": "Žebříček",
"rankingView": {
"advancedRankings": "Pokročilé žebříčky",
"given": "Vzhledem k tomu, že",
"mode": "Režim",
"pageSize": "Velikost stránky",
"received": "Přijato"
},
"reputation": "Reputace",
"tabs": {
"overview": "Přehled",
"profile": "Profil",
"ranking": "Žebříček"
},
"title": "Přístrojová deska",
"todayReputation": "Dnešní reputace",
"topChannel": "Nejlepší kanál",
Expand Down Expand Up @@ -676,6 +716,7 @@
"kofiHint": "nebo nás podpořte na",
"kofiLink": "Ko-fi",
"message": "{name} je vyhrazeno pro příznivce. Staňte se příznivcem a můžete jej používat.",
"perServer": "Funkce se odemykají pro jednotlivé servery.",
"requiredTierPlural": "Je vyžadována jedna z následujících úrovní podpory:",
"requiredTierSingular": "Požadovaná úroveň podpory:",
"title": "Funkce pro příznivce",
Expand Down Expand Up @@ -1034,6 +1075,7 @@
"noFeatures": "V současné době nejsou k dispozici žádné funkce.",
"purchase": "Nákup",
"purchaseSuccess": "Funkce byla úspěšně zakoupena!",
"requiredTiers": "Úrovně podpory",
"subscribe": "Přihlásit se k odběru",
"subscribeSuccess": "Předplatné aktivováno!",
"subscriptionActiveNote": "Tato funkce je v současné době aktivována aktivním předplatným.",
Expand Down Expand Up @@ -1075,6 +1117,10 @@
"success": "Váš e-mail byl úspěšně ověřen."
}
},
"publicProfile": {
"description": "Umožnit ostatním uživatelům zobrazit váš profil reputace, i když s vámi nesdílejí stejný server.",
"label": "Veřejný profil"
},
"title": "Nastavení uživatele",
"voteGuild": {
"description": "Vyberte, která gilda by měla dostat žetony, když hlasujete pro bota. To bude mít vliv pouze na budoucí hlasování.",
Expand Down
Loading