pleroma-like client for Bluesky pl.hexmani.ac
bluesky pleroma social-media

Create proper dashboard layout with mini profile

hexmani.ac 7eb5c43c d3765b7d

verified
+6
bun.lock
···
"": {
"name": "vite-template-solid",
"dependencies": {
+
"@atcute/bluesky": "^3.2.8",
+
"@atcute/client": "^4.0.5",
"@atcute/lexicons": "^1.2.2",
"@atcute/oauth-browser-client": "^1.0.27",
"@solidjs/router": "^0.15.3",
···
},
},
"packages": {
+
"@atcute/atproto": ["@atcute/atproto@3.1.8", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-Miu+S7RSgAYbmQWtHJKfSFUN5Kliqoo4YH0rILPmBtfmlZieORJgXNj9oO/Uive0/ulWkiRse07ATIcK8JxMnw=="],
+
+
"@atcute/bluesky": ["@atcute/bluesky@3.2.8", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-wxEnSOvX7nLH4sVzX9YFCkaNEWIDrTv3pTs6/x4NgJ3AJ3XJio0OYPM8tR7wAgsklY6BHvlAgt3yoCDK0cl1CA=="],
+
"@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="],
"@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="],
+2
package.json
···
"vite-plugin-solid": "^2.11.8"
},
"dependencies": {
+
"@atcute/bluesky": "^3.2.8",
+
"@atcute/client": "^4.0.5",
"@atcute/lexicons": "^1.2.2",
"@atcute/oauth-browser-client": "^1.0.27",
"@solidjs/router": "^0.15.3",
+7 -3
src/components/container.tsx
···
const Container = (props: ContainerProps) => {
return (
<div class="container">
-
<div class="container-header">
-
<span>{props.title}</span>
-
</div>
+
{props.title ? (
+
<div class="container-header">
+
<span>{props.title}</span>
+
</div>
+
) : (
+
<></>
+
)}
{props.children}
</div>
);
+58
src/components/miniProfile.tsx
···
+
import { Component, Match, Show, Switch, createResource } from "solid-js";
+
import { Client } from "@atcute/client";
+
import { agent } from "./login";
+
+
type MiniProfileProps = {
+
did: `did:${string}:${string}`;
+
};
+
+
async function getProfileDetails(did: `did:${string}:${string}`) {
+
const rpc = new Client({ handler: agent });
+
+
const res = await rpc.get("app.bsky.actor.getProfile", {
+
params: {
+
actor: did,
+
},
+
});
+
+
if (!res.ok) {
+
throw new Error(`Failed to fetch profile details: ${res.status}`);
+
}
+
+
return res.data;
+
}
+
+
const MiniProfile = (props: MiniProfileProps) => {
+
const [profileInfo] = createResource(agent.sub, getProfileDetails);
+
+
return (
+
<>
+
<Show when={profileInfo.loading}>
+
<p>loading...</p>
+
</Show>
+
<Switch>
+
<Match when={profileInfo.error}>
+
<p>Error: {profileInfo.error.message}</p>
+
</Match>
+
<Match when={profileInfo()}>
+
<div
+
class="mini-profile"
+
// todo: add banner fade
+
style={`background-image: linear-gradient(to bottom, rgba(15, 22, 30, 0.85)), url(${profileInfo()?.banner}); background-size: cover; background-repeat: no-repeat;`}
+
>
+
<img
+
src={profileInfo()?.avatar}
+
alt={`Profile picture for ${profileInfo()?.handle}`}
+
/>
+
<div class="mini-profile-info">
+
<p>{profileInfo()?.displayName}</p>
+
<p>@{profileInfo()?.handle}</p>
+
</div>
+
</div>
+
</Match>
+
</Switch>
+
</>
+
);
+
};
+
+
export default MiniProfile;
+2 -1
src/components/navbar.tsx
···
import { A } from "@solidjs/router";
import { Component } from "solid-js/types/server/rendering.js";
+
import { loginState } from "./login";
const Navbar: Component = () => {
return (
<>
<nav id="nav">
<div class="center-nav">
-
<A href="/">
+
<A href={loginState() ? "/dash" : "/"}>
<img src="favicon.png" />
</A>
</div>
+24
src/components/postForm.tsx
···
+
import { Component } from "solid-js";
+
+
const PostForm: Component = () => {
+
return (
+
<>
+
<form
+
autocomplete="off"
+
onclick={(e) => e.preventDefault()}
+
class="post-form"
+
>
+
<textarea
+
id="post-textbox"
+
name="post-textbox"
+
rows="1"
+
cols="1"
+
placeholder="The car's on fire, and there's no driver at the wheel..."
+
></textarea>
+
<button type="submit">Post</button>
+
</form>
+
</>
+
);
+
};
+
+
export default PostForm;
+30 -5
src/routes/dashboard.tsx
···
-
import { killSession, loginState } from "../components/login";
+
import Container from "../components/container";
+
import { agent, killSession, loginState } from "../components/login";
+
import MiniProfile from "../components/miniProfile";
+
import PostForm from "../components/postForm";
const Dashboard = () => {
if (!loginState()) {
···
}
return (
-
<div>
-
<h1>Dashboard</h1>
-
<button onclick={killSession}>Log out</button>
-
</div>
+
<>
+
<div id="sidebar">
+
<Container
+
title=""
+
children={
+
<>
+
<MiniProfile did={agent.sub} />
+
<PostForm />
+
<button onClick={killSession}>Log out</button>
+
</>
+
}
+
/>
+
</div>
+
<div id="content">
+
<Container
+
title="Following"
+
children={
+
<div class="container-content">
+
<div class="dashboard-feed">
+
<p>No more posts</p>
+
</div>
+
</div>
+
}
+
/>
+
</div>
+
</>
);
};
+5 -1
src/routes/splash.tsx
···
import blueskyLogo from "/bluesky.svg?url";
import tangledLogo from "/tangled.svg?url";
import Container from "../components/container";
-
import { Login } from "../components/login";
+
import { Login, loginState } from "../components/login";
const Splash: Component = () => {
+
if (loginState()) {
+
location.href = "/dash";
+
}
+
return (
<>
<div id="sidebar">
+1 -2
src/styles/container.scss
···
@use "./vars";
.container {
-
background-color: rgba(15, 22, 30, 1);
+
background-color: #24262d;
border-radius: vars.$containerBorderRadius;
margin: 1em;
padding: 0 0 1em 0;
···
}
.container-header {
-
font-weight: 500;
background-color: vars.$foregroundColor;
text-align: left;
padding: 1em;
+4 -35
src/styles/main.scss
···
+
@use "./button";
@use "./container";
+
@use "./nav";
+
@use "./profile";
+
@use "./routes/dashboard";
@use "./routes/login";
@use "./vars";
-
@use "./button";
-
@use "./nav";
-
/* Core page format */
body {
text-align: center;
color: vars.$textColor;
···
font-weight: bold;
font-style: italic;
}
-
-
/* Dashboard */
-
-
.post-form {
-
display: flex;
-
flex-direction: column;
-
place-content: center;
-
-
button {
-
}
-
}
-
-
#post-textbox {
-
background-color: vars.$foregroundColor;
-
border: 0;
-
border-radius: containerBorderRadius;
-
box-shadow:
-
0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset,
-
0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset,
-
0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
-
color: vars.$textColor;
-
font-family: inherit;
-
font-size: 14px;
-
resize: none;
-
max-width: 90%;
-
}
-
-
.dashboard-feed {
-
p {
-
color: #8d8d8d;
-
}
-
}
+30
src/styles/profile.scss
···
+
@use "vars";
+
+
.mini-profile {
+
display: flex;
+
flex-direction: row;
+
align-items: center;
+
gap: 1rem;
+
padding: 1rem;
+
margin-bottom: 1rem;
+
border-radius: vars.$containerBorderRadius;
+
+
img {
+
max-height: 64px;
+
box-shadow: 10px 5px 5px rgba(0, 0, 0, 0.2);
+
border-radius: 3px;
+
}
+
}
+
+
.mini-profile-info {
+
text-align: left;
+
display: flex;
+
flex-direction: column;
+
align-items: flex-start;
+
justify-content: center;
+
gap: 0.5rem;
+
+
p {
+
margin: 0;
+
}
+
}
+39
src/styles/routes/dashboard.scss
···
+
@use "../vars";
+
+
// todo: fix small width
+
.post-form {
+
display: grid;
+
grid-template-columns: auto;
+
grid-template-rows: 5rem auto;
+
margin: 0 1rem;
+
+
button {
+
width: 35%;
+
padding: 0.5rem 0.5rem;
+
justify-self: end;
+
}
+
+
textarea {
+
background-color: vars.$foregroundColor;
+
border: 0;
+
border-radius: 3px;
+
box-shadow:
+
0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset,
+
0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset,
+
0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
+
box-sizing: border-box;
+
color: vars.$textColor;
+
font-family: inherit;
+
font-size: 14px;
+
resize: none;
+
padding: 0.5rem 0.5rem;
+
hyphens: none;
+
width: 100%;
+
}
+
}
+
+
.dashboard-feed {
+
p {
+
color: #8d8d8d;
+
}
+
}
+1 -1
tsconfig.json
···
// Type Checking & Safety
"strict": true,
-
"types": ["vite/client"]
+
"types": ["vite/client", "@atcute/bluesky"]
}
}