mirror of
https://github.com/acid-info/Kurate.git
synced 2025-02-15 01:07:00 +00:00
feat: homepage with dummy data and reactive stores (#12)
This commit is contained in:
parent
4ff6bb9040
commit
ee438b7f4b
BIN
src/lib/assets/default-avatar.png
Normal file
BIN
src/lib/assets/default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
18
src/lib/components/avatar.svelte
Normal file
18
src/lib/components/avatar.svelte
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
67
src/lib/components/post.svelte
Normal file
67
src/lib/components/post.svelte
Normal 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>
|
28
src/lib/components/wallet-connect.svelte
Normal file
28
src/lib/components/wallet-connect.svelte
Normal 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
29
src/lib/stores/post.ts
Normal 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
35
src/lib/stores/user.ts
Normal 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
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
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
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
59
src/lib/temp/index.svelte
Normal 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
22
src/lib/utils.ts
Normal 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`
|
||||
}
|
@ -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>
|
||||
|
19
src/routes/profile/[address]/+page.svelte
Normal file
19
src/routes/profile/[address]/+page.svelte
Normal 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>
|
15
src/routes/profile/[address]/+page.ts
Normal file
15
src/routes/profile/[address]/+page.ts
Normal 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
|
||||
}
|
@ -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>
|
@ -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}`)
|
||||
}
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user