campfire/src/routes/[server]/[account]/+page.svelte

224 lines
5.9 KiB
Svelte
Raw Normal View History

2025-07-13 18:22:03 +01:00
<script>
import Button from '@cf/ui/Button.svelte';
2025-07-13 22:00:33 +01:00
import HomeIcon from '@cf/icons/unlisted.svg';
import MoreIcon from '@cf/icons/more.svg';
import Lang from '$lib/lang';
2025-07-14 00:19:42 +01:00
import * as api from '$lib/api.js';
import { server, createServer } from '$lib/client/server.js';
import { app } from '$lib/client/app.js';
import { parseAccount } from '$lib/account.js';
import { account } from '$lib/stores/account';
import { goto, afterNavigate } from '$app/navigation';
import { base } from '$app/paths';
export let data;
2025-07-13 22:00:33 +01:00
const lang = Lang('en_GB');
2025-07-14 00:19:42 +01:00
let profile = fetchProfile(data.account_handle);
let error = false;
let previous_page = base;
afterNavigate(({from}) => {
previous_page = from?.url.pathname || previous_page;
profile = fetchProfile(data.account_handle);
})
async function fetchProfile(handle) {
let token = $app ? $app.token : null;
if (!$server || $server.host !== data.server_host) {
server.set(await createServer(data.server_host));
if (!$server) {
error = lang.string('error.connection_failed', data.server_host);
throw new Error(lang.string('logs.connection_failed', data.server_host));
}
}
let profile_data;
try {
profile_data = await api.lookupUser($server.host, token, handle);
console.debug(profile_data);
} catch (error) {
throw error;
}
if (!profile_data || profile_data.error) {
error = lang.string('error.profile_fetch_failed_id', handle);
throw new Error(lang.string('logs.profile_fetch_failed_id', handle));
}
let profile = await parseAccount(profile_data, 0);
return profile;
}
2025-07-13 18:22:03 +01:00
</script>
2025-07-14 00:19:42 +01:00
{#await profile}
<div class="loading throb">
<span>{lang.string('profile.loading')}</span>
2025-07-13 18:22:03 +01:00
</div>
2025-07-14 00:19:42 +01:00
{:then profile}
<header data-banner="{profile.banner_url}">
<img src="{profile.banner_url}" class="profile-banner" alt="">
<div class="profile-tag">
<!-- svelte-ignore a11y-img-redundant-alt -->
<img src="{profile.avatar_url}" alt="">
<div class="profile-tag-names">
<h1>{@html profile.rich_name}</h1>
<p>{profile.fqn}</p>
</div>
</div>
</header>
<div class="profile-info">
<p class="profile-bio">{@html profile.bio}</p>
<ul class="profile-counts">
<li><b>{lang.string('profile.followers')}</b> {profile.followers_count}</li>
<li><b>{lang.string('profile.following')}</b> {profile.following_count}</li>
<li><b>{lang.string('profile.posts')}</b> {profile.posts_count}</li>
</ul>
2025-07-13 18:22:03 +01:00
<div class="profile-actions">
2025-07-14 00:19:42 +01:00
{#if $account && profile.fqn !== $account.fqn}
<Button filled label="{lang.string('profile.follow')} {profile.nickname}" class="profile-btn-follow">
2025-07-14 00:19:42 +01:00
{lang.string('profile.follow')}
</Button>
{/if}
<Button label="{lang.string('profile.home_instance')}" href="{profile.url}">
2025-07-13 18:22:03 +01:00
<HomeIcon width="24px"/>
</Button>
<Button>
<MoreIcon width="24px"/>
</Button>
</div>
</div>
2025-07-14 00:19:42 +01:00
<div class="profile-post-categories">
<Button active>
{lang.string('profile.posts')}
</Button>
<Button>
{lang.string('profile.replies')}
</Button>
<Button>
{lang.string('profile.media')}
</Button>
</div>
{:catch error}
<p class="error">{error}</p>
{/await}
2025-07-13 18:22:03 +01:00
<style>
header {
margin-top: 1rem;
width: 100%;
height: 215px;
position: relative;
}
2025-07-14 00:19:42 +01:00
.profile-banner {
width: 100%;
height: 100%;
object-fit: cover;
background-color: var(--bg-700);
}
2025-07-13 18:22:03 +01:00
.profile-tag {
position: absolute;
bottom: 16px;
left: 16px;
2025-07-13 22:00:33 +01:00
background: color-mix(in srgb, transparent, var(--bg-1000) 50%);
2025-07-13 18:22:03 +01:00
backdrop-filter: blur(8px);
width: fit-content;
display: flex;
height: 64px;
border-radius: 8px;
}
.profile-tag img {
aspect-ratio: 1;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.profile-tag-names {
padding: 8px 16px;
align-self: center;
}
.profile-tag-names * {
margin: 0;
}
.profile-tag-names h1 {
font-size: 1.15rem
}
.profile-info {
background-color: var(--bg-800);
padding: 16px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.profile-bio {
margin: 0;
/* !! may not be required in prod */
white-space: pre-line;
}
2025-07-14 00:19:42 +01:00
:global(.profile-bio p:first-of-type) {
margin: 0;
}
2025-07-13 18:22:03 +01:00
2025-07-14 00:19:42 +01:00
.profile-counts {
padding: 0;
}
.profile-counts li {
display: inline-block;
}
2025-07-13 18:22:03 +01:00
.profile-counts > *:not(.profile-counts:first-child) {
margin-right: 16px;
}
.profile-actions {
width: fit-content;
display: flex;
align-items: center;
gap: .5rem;
}
.profile-actions :global(button.profile-btn-follow) {
2025-07-13 18:22:03 +01:00
padding: 0 32px;
}
.profile-actions :global(a) {
width: fit-content;
height: 16px;
}
2025-07-13 18:22:03 +01:00
.profile-actions :global(button) {
width: fit-content;
height: 42px;
2025-07-13 18:22:03 +01:00
}
.profile-post-categories {
display: flex;
padding: 1rem 0;
gap: 1rem;
}
2025-07-14 00:19:42 +01:00
.loading {
width: 100%;
height: 80vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 2em;
font-weight: bold;
}
.error {
padding: 4em 0;
width: 100%;
text-align: center;
}
2025-07-13 22:00:33 +01:00
</style>