All checks were successful
Queue Release Build / prepare (push) Successful in 23s
Deploy Web Apps / deploy (push) Successful in 5m54s
Queue Release Build / build-windows (push) Successful in 16m19s
Queue Release Build / build-linux (push) Successful in 30m13s
Queue Release Build / finalize (push) Successful in 47s
255 lines
9.1 KiB
HTML
255 lines
9.1 KiB
HTML
<div
|
|
appThemeNode="profileCardSurface"
|
|
class="w-72 rounded-lg border border-border bg-card shadow-xl"
|
|
style="animation: profile-card-in 120ms cubic-bezier(0.2, 0, 0, 1) both"
|
|
>
|
|
@let profileUser = displayedUser();
|
|
@let isEditable = editable();
|
|
@let activeField = editingField();
|
|
@let statusColor = currentStatusColor();
|
|
@let statusLabel = currentStatusLabel();
|
|
|
|
<div
|
|
appThemeNode="profileCardBanner"
|
|
class="h-20 rounded-t-lg bg-gradient-to-r from-primary/30 to-primary/10"
|
|
></div>
|
|
|
|
<div class="relative px-4">
|
|
<div class="-mt-8">
|
|
<button
|
|
type="button"
|
|
class="rounded-full"
|
|
(click)="pickAvatar(avatarInput)"
|
|
>
|
|
<app-user-avatar
|
|
[name]="profileUser.displayName"
|
|
[avatarUrl]="profileUser.avatarUrl"
|
|
size="xl"
|
|
[status]="profileUser.status"
|
|
[showStatusBadge]="true"
|
|
ringClass="ring-4 ring-card"
|
|
/>
|
|
</button>
|
|
<input
|
|
#avatarInput
|
|
type="file"
|
|
class="hidden"
|
|
[accept]="avatarAccept"
|
|
(change)="onAvatarSelected($event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
appThemeNode="profileCardBody"
|
|
class="px-4 pb-3 pt-2.5"
|
|
>
|
|
@if (isEditable) {
|
|
<div class="space-y-2">
|
|
<div>
|
|
@if (activeField === 'displayName') {
|
|
<input
|
|
type="text"
|
|
class="w-full rounded-md border border-border bg-background/70 px-2 py-1.5 text-base font-semibold text-foreground outline-none focus:border-primary/70"
|
|
[value]="displayNameDraft()"
|
|
(input)="onDisplayNameInput($event)"
|
|
(blur)="finishEdit('displayName')"
|
|
/>
|
|
} @else {
|
|
<button
|
|
type="button"
|
|
class="block w-full py-0.5 text-left text-base font-semibold text-foreground"
|
|
(click)="startEdit('displayName')"
|
|
>
|
|
{{ profileUser.displayName }}
|
|
</button>
|
|
}
|
|
|
|
<p class="truncate text-sm text-muted-foreground">{{ profileUser.username }}</p>
|
|
@if (profileUser.gameActivity; as activity) {
|
|
<p class="mt-1 flex items-center gap-1 truncate text-xs text-muted-foreground">
|
|
<ng-icon
|
|
name="lucideGamepad2"
|
|
class="h-3 w-3 shrink-0"
|
|
/>
|
|
@if (activity.store?.url) {
|
|
<button
|
|
type="button"
|
|
class="truncate text-left hover:text-foreground hover:underline"
|
|
(click)="openGameStore(activity, $event)"
|
|
>
|
|
Playing {{ activity.name }}
|
|
</button>
|
|
} @else {
|
|
<span class="truncate">Playing {{ activity.name }}</span>
|
|
}
|
|
<span class="shrink-0">{{ gameActivityElapsed() }}</span>
|
|
</p>
|
|
}
|
|
</div>
|
|
|
|
<div>
|
|
@if (activeField === 'description') {
|
|
<textarea
|
|
rows="3"
|
|
class="w-full resize-none rounded-md border border-border bg-background/70 px-2 py-2 text-sm leading-5 text-foreground outline-none focus:border-primary/70"
|
|
[value]="descriptionDraft()"
|
|
placeholder="Add a description"
|
|
(input)="onDescriptionInput($event)"
|
|
(blur)="finishEdit('description')"
|
|
></textarea>
|
|
} @else {
|
|
<button
|
|
type="button"
|
|
class="block w-full py-1 text-left text-sm leading-5"
|
|
(click)="startEdit('description')"
|
|
>
|
|
@if (profileUser.description) {
|
|
<span class="whitespace-pre-line text-muted-foreground">{{ profileUser.description }}</span>
|
|
} @else {
|
|
<span class="text-muted-foreground/70">Add a description</span>
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
@if (profileUser.gameActivity; as activity) {
|
|
<div class="flex items-center gap-2 rounded-md border border-border bg-background/40 px-2.5 py-2">
|
|
@if (activity.iconUrl) {
|
|
<img
|
|
class="h-9 w-9 rounded-md object-cover"
|
|
[src]="activity.iconUrl"
|
|
[alt]="activity.name"
|
|
/>
|
|
} @else {
|
|
<div class="grid h-9 w-9 place-items-center rounded-md bg-secondary text-muted-foreground">
|
|
<ng-icon
|
|
name="lucideGamepad2"
|
|
class="h-4 w-4"
|
|
/>
|
|
</div>
|
|
}
|
|
<div class="min-w-0 flex-1">
|
|
<p class="truncate text-xs text-muted-foreground">Playing</p>
|
|
@if (activity.store?.url) {
|
|
<button
|
|
type="button"
|
|
class="block max-w-full truncate text-left text-sm font-medium text-foreground hover:underline"
|
|
(click)="openGameStore(activity, $event)"
|
|
>
|
|
{{ activity.name }}
|
|
</button>
|
|
} @else {
|
|
<p class="truncate text-sm font-medium text-foreground">{{ activity.name }}</p>
|
|
}
|
|
<p class="text-xs text-muted-foreground">{{ gameActivityElapsed() }}</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
} @else {
|
|
<p class="truncate text-base font-semibold text-foreground">{{ profileUser.displayName }}</p>
|
|
<p class="truncate text-sm text-muted-foreground">{{ profileUser.username }}</p>
|
|
|
|
@if (profileUser.gameActivity; as activity) {
|
|
<div class="mt-3 flex items-center gap-2 rounded-md border border-border bg-background/40 px-2.5 py-2">
|
|
@if (activity.iconUrl) {
|
|
<img
|
|
class="h-9 w-9 rounded-md object-cover"
|
|
[src]="activity.iconUrl"
|
|
[alt]="activity.name"
|
|
/>
|
|
} @else {
|
|
<div class="grid h-9 w-9 place-items-center rounded-md bg-secondary text-muted-foreground">
|
|
<ng-icon
|
|
name="lucideGamepad2"
|
|
class="h-4 w-4"
|
|
/>
|
|
</div>
|
|
}
|
|
<div class="min-w-0 flex-1">
|
|
<p class="truncate text-xs text-muted-foreground">Playing</p>
|
|
@if (activity.store?.url) {
|
|
<button
|
|
type="button"
|
|
class="block max-w-full truncate text-left text-sm font-medium text-foreground hover:underline"
|
|
(click)="openGameStore(activity, $event)"
|
|
>
|
|
{{ activity.name }}
|
|
</button>
|
|
} @else {
|
|
<p class="truncate text-sm font-medium text-foreground">{{ activity.name }}</p>
|
|
}
|
|
<p class="text-xs text-muted-foreground">{{ gameActivityElapsed() }}</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (profileUser.description) {
|
|
<p class="mt-2 whitespace-pre-line text-sm leading-5 text-muted-foreground">{{ profileUser.description }}</p>
|
|
}
|
|
}
|
|
|
|
@if (avatarError()) {
|
|
<div class="mt-2.5 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2 text-xs text-red-200">
|
|
{{ avatarError() }}
|
|
</div>
|
|
}
|
|
|
|
@if (isEditable) {
|
|
<div class="relative mt-2.5">
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center gap-2 rounded-md border border-border px-2.5 py-1.5 text-xs transition-colors hover:bg-secondary/60"
|
|
(click)="toggleStatusMenu()"
|
|
>
|
|
<span
|
|
class="h-2 w-2 rounded-full"
|
|
[class]="statusColor"
|
|
></span>
|
|
<span class="flex-1 text-left text-foreground">{{ statusLabel }}</span>
|
|
<ng-icon
|
|
name="lucideChevronDown"
|
|
class="h-3 w-3 text-muted-foreground"
|
|
/>
|
|
</button>
|
|
|
|
@if (showStatusMenu()) {
|
|
<div class="absolute bottom-full left-0 z-10 mb-1 w-full rounded-md border border-border bg-card py-1 shadow-lg">
|
|
@for (opt of statusOptions; track opt.label) {
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs hover:bg-secondary"
|
|
[class.bg-secondary]="isStatusOptionSelected(opt.value)"
|
|
[class.text-foreground]="isStatusOptionSelected(opt.value)"
|
|
[class.text-muted-foreground]="!isStatusOptionSelected(opt.value)"
|
|
(click)="setStatus(opt.value)"
|
|
>
|
|
<span
|
|
class="h-2 w-2 rounded-full"
|
|
[class]="opt.color"
|
|
></span>
|
|
<span class="flex-1">{{ opt.label }}</span>
|
|
@if (isStatusOptionSelected(opt.value)) {
|
|
<ng-icon
|
|
name="lucideCheck"
|
|
class="h-3.5 w-3.5 text-primary"
|
|
/>
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
} @else {
|
|
<div class="mt-2 flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
<span
|
|
class="h-2 w-2 rounded-full"
|
|
[class]="statusColor"
|
|
></span>
|
|
<span>{{ statusLabel }}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|