feat: Rename to Toju and add translation
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped

This commit is contained in:
2026-06-05 17:13:03 +02:00
parent 8ecfc9a1fe
commit ee293d7daf
301 changed files with 8247 additions and 2218 deletions

View File

@@ -4,12 +4,12 @@
<header class="min-w-0 space-y-1">
<h1 class="text-2xl font-semibold text-foreground">
@if (currentUser()) {
Welcome back, {{ currentUser()!.displayName || 'there' }}
{{ 'dashboard.welcomeBack' | translate: { name: currentUser()!.displayName || ('dashboard.welcomeGuestFallback' | translate) } }}
} @else {
Welcome to MetoYou
{{ 'dashboard.welcomeTitle' | translate }}
}
</h1>
<p class="text-sm text-muted-foreground">Find people, discover servers, or start your own community.</p>
<p class="text-sm text-muted-foreground">{{ 'dashboard.subtitle' | translate }}</p>
</header>
<div class="min-w-0">
@@ -21,9 +21,9 @@
<input
#searchInput
type="text"
aria-label="Search people, servers, and invites"
[attr.aria-label]="'dashboard.searchAriaLabel' | translate"
class="h-12 w-full min-w-0 rounded-xl border border-border bg-secondary py-2 pl-11 pr-4 text-base text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary sm:pr-20"
[placeholder]="isMobile() ? 'Search people, servers, invites...' : 'Search for people, servers, or paste an invite...'"
[placeholder]="isMobile() ? ('dashboard.searchPlaceholderMobile' | translate) : ('dashboard.searchPlaceholderDesktop' | translate)"
[ngModel]="searchQuery()"
(ngModelChange)="onSearchChange($event)"
(keydown.enter)="submitSearch()"
@@ -31,13 +31,13 @@
<kbd
class="pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 items-center gap-1 rounded border border-border bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:flex"
>
Ctrl K
{{ 'dashboard.searchShortcut' | translate }}
</kbd>
</div>
@if (!isSearchMode() && recentSearches().length > 0) {
<div class="mt-3 flex flex-wrap items-center gap-2">
<span class="text-xs font-medium text-muted-foreground">Recent:</span>
<span class="text-xs font-medium text-muted-foreground">{{ 'dashboard.recent' | translate }}</span>
@for (term of recentSearches(); track term) {
<span
class="group inline-flex items-center gap-1 rounded-full border border-border bg-secondary py-1 pl-3 pr-1 text-xs text-foreground"
@@ -52,7 +52,7 @@
<button
type="button"
class="grid h-4 w-4 place-items-center rounded-full text-muted-foreground hover:bg-card hover:text-foreground"
[attr.aria-label]="'Remove ' + term"
[attr.aria-label]="'dashboard.removeRecent' | translate: { term }"
(click)="removeRecentSearch(term)"
>
<ng-icon
@@ -67,7 +67,7 @@
class="text-xs font-medium text-muted-foreground hover:text-foreground hover:underline"
(click)="clearRecentSearches()"
>
Clear
{{ 'common.clear' | translate }}
</button>
</div>
}
@@ -77,7 +77,7 @@
<section class="space-y-5">
@if (inviteResult(); as invite) {
<div>
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Invite</h2>
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ 'dashboard.invite' | translate }}</h2>
<button
type="button"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
@@ -90,7 +90,7 @@
/>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">Open invite</p>
<p class="truncate text-sm font-semibold text-foreground">{{ 'dashboard.openInvite' | translate }}</p>
<p class="truncate text-xs text-muted-foreground">{{ invite }}</p>
</div>
<ng-icon
@@ -104,11 +104,11 @@
@if (topServerResults().length > 0) {
<div>
<div class="mb-2 flex items-center justify-between">
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Servers</h2>
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ 'dashboard.servers' | translate }}</h2>
<a
routerLink="/servers"
class="text-xs font-medium text-primary hover:underline"
>View all</a
>{{ 'common.viewAll' | translate }}</a
>
</div>
<div class="space-y-2">
@@ -148,11 +148,11 @@
@if (topPeopleResults().length > 0) {
<div>
<div class="mb-2 flex items-center justify-between">
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">People</h2>
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ 'dashboard.people' | translate }}</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>View all</a
>{{ 'common.viewAll' | translate }}</a
>
</div>
<div class="space-y-2">
@@ -170,7 +170,7 @@
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">{{ personLabel(person) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
<p class="text-xs text-muted-foreground">{{ (isOnline(person) ? 'common.online' : 'common.offline') | translate }}</p>
</div>
<app-friend-button [user]="person" />
</a>
@@ -181,7 +181,7 @@
@if (hasNoQuickResults() && !isSearching()) {
<div class="rounded-lg border border-border bg-card px-4 py-8 text-center text-sm text-muted-foreground">
No people, servers, or invites match
{{ 'dashboard.noResults' | translate }}
<span class="font-medium text-foreground">{{ searchQuery() }}</span
>.
</div>
@@ -201,8 +201,8 @@
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Find People</p>
<p class="mt-0.5 text-xs text-muted-foreground">Connect with friends.</p>
<p class="text-sm font-semibold text-foreground">{{ 'dashboard.findPeople.title' | translate }}</p>
<p class="mt-0.5 text-xs text-muted-foreground">{{ 'dashboard.findPeople.subtitle' | translate }}</p>
</div>
<ng-icon
name="lucideArrowRight"
@@ -221,8 +221,8 @@
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Find Servers</p>
<p class="mt-0.5 text-xs text-muted-foreground">Browse communities.</p>
<p class="text-sm font-semibold text-foreground">{{ 'dashboard.findServers.title' | translate }}</p>
<p class="mt-0.5 text-xs text-muted-foreground">{{ 'dashboard.findServers.subtitle' | translate }}</p>
</div>
<ng-icon
name="lucideArrowRight"
@@ -241,8 +241,8 @@
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Create Server</p>
<p class="mt-0.5 text-xs text-muted-foreground">Start your own.</p>
<p class="text-sm font-semibold text-foreground">{{ 'dashboard.createServer.title' | translate }}</p>
<p class="mt-0.5 text-xs text-muted-foreground">{{ 'dashboard.createServer.subtitle' | translate }}</p>
</div>
<ng-icon
name="lucideArrowRight"
@@ -259,9 +259,9 @@
class="h-6 w-6 text-muted-foreground"
/>
</div>
<h2 class="text-base font-semibold text-foreground">Get started</h2>
<h2 class="text-base font-semibold text-foreground">{{ 'dashboard.getStarted.title' | translate }}</h2>
<p class="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
You have not joined any servers yet. Find a community to join, or create your own to invite friends.
{{ 'dashboard.getStarted.description' | translate }}
</p>
</section>
}
@@ -270,11 +270,11 @@
<section class="grid min-w-0 gap-4 lg:grid-cols-2">
<div class="min-w-0 rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">People you might know</h2>
<h2 class="text-sm font-semibold text-foreground">{{ 'dashboard.peopleYouMightKnow' | translate }}</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>See all</a
>{{ 'common.seeAll' | translate }}</a
>
</div>
@if (peopleYouMightKnow().length > 0) {
@@ -290,7 +290,7 @@
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(person) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
<p class="text-xs text-muted-foreground">{{ (isOnline(person) ? 'common.online' : 'common.offline') | translate }}</p>
</div>
<app-friend-button
class="shrink-0"
@@ -300,17 +300,17 @@
}
</div>
} @else {
<p class="py-6 text-center text-sm text-muted-foreground">No people to suggest yet.</p>
<p class="py-6 text-center text-sm text-muted-foreground">{{ 'dashboard.noPeopleSuggestions' | translate }}</p>
}
</div>
<div class="min-w-0 rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">Popular Servers</h2>
<h2 class="text-sm font-semibold text-foreground">{{ 'dashboard.popularServers' | translate }}</h2>
<a
routerLink="/servers"
class="text-xs font-medium text-primary hover:underline"
>See all</a
>{{ 'common.seeAll' | translate }}</a
>
</div>
@if (popularServers().length > 0) {
@@ -339,13 +339,13 @@
class="shrink-0 rounded-md bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
(click)="openServer(server)"
>
Join
{{ 'common.join' | translate }}
</button>
</div>
}
</div>
} @else {
<p class="py-6 text-center text-sm text-muted-foreground">No popular servers right now.</p>
<p class="py-6 text-center text-sm text-muted-foreground">{{ 'dashboard.noPopularServers' | translate }}</p>
}
</div>
</section>
@@ -354,11 +354,11 @@
@if (friends().length > 0) {
<section>
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">Your Friends</h2>
<h2 class="text-sm font-semibold text-foreground">{{ 'dashboard.yourFriends' | translate }}</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>Manage</a
>{{ 'common.manage' | translate }}</a
>
</div>
<div class="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
@@ -373,7 +373,7 @@
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(friend) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(friend) ? 'Online' : 'Offline' }}</p>
<p class="text-xs text-muted-foreground">{{ (isOnline(friend) ? 'common.online' : 'common.offline') | translate }}</p>
</div>
<app-friend-button [user]="friend" />
</div>
@@ -385,7 +385,7 @@
<!-- Recently active servers -->
@if (recentlyActiveServers().length > 0) {
<section>
<h2 class="mb-3 text-sm font-semibold text-foreground">Recently Active Servers</h2>
<h2 class="mb-3 text-sm font-semibold text-foreground">{{ 'dashboard.recentlyActiveServers' | translate }}</h2>
<div class="grid min-w-0 grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
@for (room of recentlyActiveServers(); track room.id) {
<button
@@ -407,7 +407,7 @@
}
</div>
<p class="w-full truncate text-sm font-medium text-foreground">{{ room.name }}</p>
<p class="text-xs text-muted-foreground">{{ room.userCount }} members</p>
<p class="text-xs text-muted-foreground">{{ 'dashboard.roomMembers' | translate: { count: room.userCount } }}</p>
</button>
}
</div>

View File

@@ -27,6 +27,7 @@ import {
import { selectAllUsers, selectCurrentUser } from '../../store/users/users.selectors';
import type { ServerInfo } from '../../domains/server-directory/domain/models/server-directory.model';
import type { Room, User } from '../../shared-kernel';
import { AppI18nService } from '../../core/i18n';
interface HarnessOptions {
searchResults?: ServerInfo[];
@@ -81,6 +82,19 @@ function createHarness(options: HarnessOptions = {}) {
const serverDirectory = { getFeaturedServers, getTrendingServers } as unknown as ServerDirectoryFacade;
const friendIds = new Set<string>(options.friendIds ?? []);
const friendService = { friendIds: () => friendIds, friends: () => [] } as unknown as FriendService;
const appI18n = {
instant: (key: string, params?: Record<string, unknown>) => {
if (key === 'dashboard.serverMeta.member') {
return `${params?.count} member`;
}
if (key === 'dashboard.serverMeta.members') {
return `${params?.count} members`;
}
return key;
}
} as unknown as AppI18nService;
const injector = Injector.create({
providers: [
DashboardComponent,
@@ -88,7 +102,8 @@ function createHarness(options: HarnessOptions = {}) {
{ provide: Router, useValue: router },
{ provide: ServerDirectoryFacade, useValue: serverDirectory },
{ provide: FriendService, useValue: friendService },
{ provide: ViewportService, useValue: { isMobile: signal(options.isMobile ?? false) } }
{ provide: ViewportService, useValue: { isMobile: signal(options.isMobile ?? false) } },
{ provide: AppI18nService, useValue: appI18n }
]
});
const component = runInInjectionContext(injector, () => injector.get(DashboardComponent));

View File

@@ -45,6 +45,7 @@ import { FriendService } from '../../domains/direct-message/application/services
import { FriendButtonComponent } from '../../domains/direct-message/feature/friend-button/friend-button.component';
import { UserAvatarComponent } from '../../shared/components/user-avatar/user-avatar.component';
import { parseInviteQuery } from './invite-query.util';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../core/i18n';
/** Maximum quick-search rows shown per group on the dashboard. */
const QUICK_RESULT_LIMIT = 5;
@@ -70,7 +71,8 @@ const RECENT_SEARCHES_STORAGE_KEY = 'metoyou_dashboard_recent_searches';
RouterLink,
NgIcon,
FriendButtonComponent,
UserAvatarComponent
UserAvatarComponent,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [
provideIcons({
@@ -90,6 +92,7 @@ const RECENT_SEARCHES_STORAGE_KEY = 'metoyou_dashboard_recent_searches';
}
})
export class DashboardComponent implements OnInit {
private readonly appI18n = inject(AppI18nService);
private store = inject(Store);
private router = inject(Router);
private serverDirectory = inject(ServerDirectoryFacade);
@@ -267,7 +270,11 @@ export class DashboardComponent implements OnInit {
}
serverMetaLabel(server: ServerInfo): string {
const members = `${server.userCount ?? 0} ${server.userCount === 1 ? 'member' : 'members'}`;
const count = server.userCount ?? 0;
const members = this.appI18n.instant(
count === 1 ? 'dashboard.serverMeta.member' : 'dashboard.serverMeta.members',
{ count }
);
const detail = server.description?.trim();
return detail ? `${members}${detail}` : members;