pleroma-like client for Bluesky

Create proper dashboard layout with mini profile

hexmani.ac 7eb5c43c d3765b7d

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