feat: homepage with dummy data and reactive stores (#12)

This commit is contained in:
Vojtech Simetka 2022-11-25 20:35:12 +01:00 committed by GitHub
parent 4ff6bb9040
commit ee438b7f4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 352 additions and 35 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,18 @@
<script lang="ts">
import defaultAvatar from '$lib/assets/default-avatar.png'
export let src: string = defaultAvatar
</script>
<img class="root" {src} alt="Avatar" />
<style>
.root {
width: 44px;
height: 44px;
border-radius: 22px;
background-color: var(--color-light-grey-background);
object-fit: cover;
overflow: hidden;
}
</style>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { ComponentConstructor, IconProps } from '$lib/types'
export let variant: 'light' | 'dark' = 'light'
export let variant: 'secondary' | 'primary' = 'secondary'
export let icon: ComponentConstructor<IconProps> | undefined = undefined
export let click: svelte.JSX.MouseEventHandler<HTMLButtonElement> | null | undefined = undefined
export let label: string | undefined = undefined
@ -12,7 +12,7 @@
<div class="wrapper">
<svelte:component
this={icon}
fill={variant === 'light' ? 'var(--color-primary)' : 'var(--color-secondary)'}
fill={variant === 'secondary' ? 'var(--color-primary)' : 'var(--color-secondary)'}
/>
</div>
{/if}
@ -48,14 +48,14 @@
.icon-only .wrapper {
margin-right: 0px;
}
.light {
color: var(--color-primary);
background-color: var(--color-secondary);
border-color: var(--color-spacer);
}
.dark {
color: var(--color-secondary);
.primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-secondary);
}
.secondary {
background-color: var(--color-secondary);
border-color: var(--color-grey-border);
color: var(--color-primary);
}
</style>

View File

@ -4,12 +4,22 @@
</script>
<div class="root">
<span class="title">The Outlet</span>
<Button icon={UserIcon} />
<div class="header">
<span class="title">The Outlet</span>
<Button icon={UserIcon} />
</div>
<div class="subheader">
Milestone 1 shaman pitchfork typewriter single-origin coffee beard flannel, actually chillwave.
</div>
</div>
<style>
.root {
top: 0;
left: 0;
right: 0;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
@ -22,4 +32,9 @@
font-style: normal;
text-align: left;
}
.subheader {
padding: var(--spacing-12);
padding-bottom: var(--spacing-24);
}
</style>

View File

@ -0,0 +1,67 @@
<script lang="ts">
import Avatar from './avatar.svelte'
import { formatAddress, formatDateFromNow } from '$lib/utils'
import type { Post } from '$lib/stores/post'
import type { User } from '$lib/stores/user'
export let post: Post
export let onUserClick: ((user: User) => void) | undefined = undefined
</script>
<div class="root">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="content-left" on:click={() => onUserClick && onUserClick(post.user)}>
<Avatar src={post.user.avatar} />
</div>
<div class="content-right">
<div class="post-info">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="username" on:click={() => onUserClick && onUserClick(post.user)}>
{post.user.name ?? 'Anonymous'}
</div>
{#if post.user.address !== undefined}
<div>{formatAddress(post.user.address)}</div>
{/if}
<div class="color-grey"></div>
<div class="color-grey">{formatDateFromNow(post.timestamp)}</div>
</div>
{post.text}
</div>
</div>
<style>
.root {
border-top: 1px solid var(--color-grey-background);
padding: var(--spacing-12);
display: flex;
flex-direction: row;
}
.content-left {
flex-shrink: 0;
}
.content-right {
margin-left: var(--spacing-12);
flex-grow: 1;
}
.post-info {
display: flex;
flex-direction: row;
margin-bottom: var(--spacing-3);
}
.post-info > div {
margin-right: var(--spacing-6);
font-family: 'Source Code Pro';
font-size: 14px;
}
.post-info > div.username {
font-family: 'Source Sans Pro';
font-weight: 600;
}
.post-info > div:last-child {
margin-right: 0px;
}
.color-grey {
color: #909090;
}
</style>

View File

@ -0,0 +1,28 @@
<script lang="ts">
import Button from './button.svelte'
import WalletIcon from './icons/wallet.svelte'
</script>
<div class="root">
<Button icon={WalletIcon} variant="primary" label="Connect wallet to post" />
<div class="explanation">Connect a wallet to access or create your account.</div>
</div>
<style>
.root {
top: 0;
left: 0;
right: 0;
background-color: var(--color-grey-background);
padding: var(--spacing-24) var(--spacing-12);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.explanation {
margin-top: var(--spacing-12);
font-size: 14;
font-weight: 400;
}
</style>

29
src/lib/stores/post.ts Normal file
View File

@ -0,0 +1,29 @@
import { writable, type Writable } from 'svelte/store'
import type { User } from './user'
export interface Post {
timestamp: number
text: string
user: User
}
export interface PostStore extends Writable<Post[]> {
add: (post: Post) => void
reset: () => void
}
function createPostStore(): PostStore {
const store = writable<Post[]>([])
return {
...store,
add: (post: Post) => {
store.update((posts) => [...posts, post])
},
reset: () => {
store.set([])
},
}
}
export const posts = createPostStore()

35
src/lib/stores/user.ts Normal file
View File

@ -0,0 +1,35 @@
import { writable, type Writable } from 'svelte/store'
export interface User {
name?: string
address: string
avatar?: string
}
export interface UserStore extends Writable<User[]> {
add: (user: User) => void
reset: () => void
find: (address: string) => Promise<User | undefined>
}
function createUserStore(): UserStore {
const store = writable<User[]>([])
return {
...store,
add: (user: User) => {
store.update((users) => [...users, user])
},
reset: () => {
store.set([])
},
find: (address: string) =>
new Promise((resolve) => {
store.subscribe((v) => {
resolve(v.find((u) => u.address === address))
})
}),
}
}
export const users = createUserStore()

BIN
src/lib/temp/assets/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
src/lib/temp/assets/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
src/lib/temp/assets/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

59
src/lib/temp/index.svelte Normal file
View File

@ -0,0 +1,59 @@
<script lang="ts">
import type { Post } from '$lib/stores/post'
import { users } from '$lib/stores/user'
import { posts } from '$lib/stores/post'
import Button from '$lib/components/button.svelte'
import image1 from '$lib/temp/assets/1.png'
import image2 from '$lib/temp/assets/2.png'
const testPosts: Post[] = [
{
timestamp: new Date().setHours(new Date().getHours() - 6),
text: 'Decision making by awkward silence.',
user: {
name: 'CoyoteRide',
avatar: image1,
address: '0x2d37a46fad14c4fcaba66660da6a5d99af88bf71',
},
},
{
timestamp: new Date().setHours(new Date().getHours() - 6),
text: "It's an amazing trigger within human nature: the minute someone acknowledges their flaws, not only do we tend to forgive them, but we actually come to admire them.",
user: {
name: 'Lemur',
avatar: image2,
address: '0x032b3c7a6af9bbf38838f582acc8e4074932f2b8',
},
},
{
timestamp: new Date().setDate(new Date().getDate() - 2),
text: 'What were the skies like when you were young?',
user: {
address: '0x547172511e83121ea8b27f25dc00e7294d9b8b6d',
},
},
]
function populateWithData() {
posts.set(testPosts)
users.set(testPosts.map((p) => p.user))
}
</script>
<div class="root">
<span>There are no posts yet</span>
<Button click={populateWithData} label="populate with testdata" />
</div>
<style>
.root {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-top: 1px solid var(--color-grey-background);
padding: var(--spacing-12);
}
span {
margin-bottom: var(--spacing-12);
}
</style>

22
src/lib/utils.ts Normal file
View File

@ -0,0 +1,22 @@
export function formatAddress(address: string) {
return `${address.substring(0, 6)}${address.substring(address.length - 5)}`
}
export function formatDateFromNow(timestamp: number) {
const delta = Math.round((Date.now() - timestamp) / 1000)
const minute = 60
const hour = minute * 60
const day = hour * 24
const week = day * 7
const month = day * 30
const year = day * 365
if (delta < minute) return `${Math.round(delta)}s`
else if (delta < hour) return `${Math.round(delta / minute)}m`
else if (delta < day) return `${Math.round(delta / hour)}h`
else if (delta < week) return `${Math.round(delta / day)}d`
else if (delta < month) return `${Math.round(delta / week)}w`
else if (delta < year) return `${Math.round(delta / month)}m`
return `${Math.round(delta / year)}y`
}

View File

@ -1,11 +1,38 @@
<script lang="ts">
import { goto } from '$app/navigation'
import Header from '$lib/components/header.svelte'
import Button from '$lib/components/button.svelte'
import WalletIcon from '$lib/components/icons/wallet.svelte'
import Post from '$lib/components/post.svelte'
import WalletConnect from '$lib/components/wallet-connect.svelte'
import Populate from '$lib/temp/index.svelte'
import { posts } from '$lib/stores/post'
</script>
<div>
<Header />
<Button label="Light Button" />
<Button variant="dark" label="Dark Button" icon={WalletIcon} />
<WalletConnect />
<div class="title">Public timeline</div>
{#if $posts.length < 1}
<Populate />
{:else}
{#each $posts as post}
<Post
{post}
onUserClick={(user) => {
goto(`/profile/${user.address}`)
}}
/>
{/each}
{/if}
</div>
<style>
.title {
height: 20px;
font-size: 16px;
font-weight: 600;
padding: var(--spacing-24) var(--spacing-12);
}
</style>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import Avatar from '$lib/components/avatar.svelte'
import type { PageData } from './$types'
export let data: PageData
</script>
<div class="root">
<div><Avatar src={data.avatar} /></div>
<div>{data.name}</div>
<div>{data.address}</div>
</div>
<style>
.root {
display: flex;
flex-direction: column;
}
</style>

View File

@ -0,0 +1,15 @@
import { error } from '@sveltejs/kit'
import type { PageLoad } from './$types'
import { users } from '$lib/stores/user'
export const load: PageLoad = async ({ params }) => {
if (!/0x[a-z0-9]{40}/i.test(params.address)) {
throw error(404, `Incorrect user address format ${params.address}`)
}
const user = await users.find(params.address)
if (!user) throw error(404, `Could not find user with address ${params.address}`)
return user
}

View File

@ -1,7 +0,0 @@
<script lang="ts">
import type { PageData } from './$types'
export let data: PageData
</script>
<div>The profile id is {data.id}</div>

View File

@ -1,11 +0,0 @@
import { error } from '@sveltejs/kit'
import type { PageLoad } from './$types'
export const load: PageLoad = ({ params }) => {
if (/0x[a-z0-9]{40}/i.test(params.id))
return {
id: params.id,
}
throw error(404, `Incorrect user id format ${params.id}`)
}

View File

@ -4,7 +4,9 @@
--color-secondary: #fff;
--color-primary: #000;
--color-spacer: #ddd;
--color-grey-border: #ddd;
--color-grey-background: #f0f0f0;
--color-light-grey-background: #ececec;
--color-text: #000;
@ -18,7 +20,6 @@
body {
width: 100vw;
min-height: 100vh;
overflow-x: hidden;
margin: 0;
padding: 0;