feat: added personas while still maintaining the ability to post (#154)

This commit is contained in:
Vojtech Simetka 2023-02-09 14:10:34 +01:00 committed by GitHub
parent e24ab63af1
commit 9855038cac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 545 additions and 499 deletions

View File

@ -1,42 +0,0 @@
<script lang="ts">
let cls: string | undefined = undefined
export { cls as class }
</script>
<div class={`root header-description ${cls}`}>
<div class="subtitle">Public timeline</div>
</div>
<style lang="scss">
.root {
padding: var(--spacing-12);
transition: padding 0.2s, margin 0.2s;
@media (min-width: 1280px) {
padding-bottom: var(--spacing-12);
margin: 0;
transition: padding 0.2s, margin 0.2s;
}
@media (prefers-color-scheme: dark) {
&.scrolled {
box-shadow: 0 1px 5px 0 rgba(var(--color-body-bg-rgb), 0.75);
}
}
}
.subtitle {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 16px;
font-weight: 600;
@media (min-width: 1280px) {
padding: 0;
width: 1280px;
margin: 0 auto 0;
}
}
</style>

View File

@ -1,21 +1,28 @@
<script lang="ts">
import UserIcon from '$lib/components/icons/user.svelte'
import Button from './button.svelte'
import { goto } from '$app/navigation'
import { ROUTES } from '$lib/routes'
import Wallet from './icons/wallet.svelte'
import Edit from './icons/edit.svelte'
import UpToTop from './icons/up-to-top.svelte'
import { formatAddress } from '$lib/utils'
import { profile } from '$lib/stores/profile'
import { connectWallet } from '$lib/services'
let cls: string | undefined = undefined
export { cls as class }
let y: number
export let loggedin: boolean | undefined = undefined
export let address: string | undefined = undefined
function goTop() {
document.body.scrollIntoView()
const handleConnect = async () => {
try {
const signer = await connectWallet()
const address = await signer.getAddress()
$profile = { signer, address }
} catch (err) {
console.error(err)
}
}
</script>
@ -25,26 +32,25 @@
<div class="header-content">
<div class="header">
<div class="header-title-wrap">
<div class="top-button">
<Button icon={UpToTop} on:click={goTop} />
</div>
<span class={` ${y > 0 ? 'subtitle' : 'header-title'} ${cls}`}>
{y > 0 ? 'Public Timeline' : 'The Outlet'}
</span>
<span class={`header-title ${cls}`}>Kurate</span>
</div>
<div class="btns">
<div class="btn wallet">
{#if address}
<Button
icon={loggedin ? Edit : Wallet}
variant="primary"
on:click={() => goto(loggedin ? ROUTES.POST_NEW : ROUTES.PROFILE)}
icon={Wallet}
variant={'secondary'}
label={formatAddress(address)}
on:click={() => goto(ROUTES.PROFILE)}
/>
</div>
<div class="btn user">
<Button icon={UserIcon} variant="secondary" on:click={() => goto(ROUTES.PROFILE)} />
</div>
{:else}
<Button
icon={Wallet}
variant={'primary'}
label={'Connect'}
on:click={() => handleConnect()}
/>
{/if}
</div>
</div>
</div>
@ -122,13 +128,7 @@
flex-direction: row;
align-items: center;
gap: var(--spacing-12);
margin-left: -56px;
transition: margin 0.2s ease-in-out;
.top-button {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
}
&.scrolled {

View File

@ -1,228 +0,0 @@
<script lang="ts">
import UserIcon from '$lib/components/icons/user.svelte'
import Button from './button.svelte'
import { goto } from '$app/navigation'
import { ROUTES } from '$lib/routes'
import Wallet from './icons/wallet.svelte'
import Edit from './icons/edit.svelte'
import UpToTop from './icons/up-to-top.svelte'
let cls: string | undefined = undefined
export { cls as class }
let y: number
export let loggedin: boolean | undefined = undefined
function goTop() {
document.body.scrollIntoView()
}
</script>
<svelte:window bind:scrollY={y} />
<div class={`root ${y > 0 ? 'scrolled' : ''} ${cls}`}>
<div class="header-content">
<div class="header">
<div class="header-title-wrap">
<div class="top-button">
<Button icon={UpToTop} on:click={goTop} />
</div>
<span class={` ${y > 0 ? 'subtitle' : 'header-title'} ${cls}`}>
{y > 0 ? 'Public Timeline' : 'The Outlet'}
</span>
</div>
<div class="btns">
<div class="btn wallet">
<Button
icon={loggedin ? Edit : Wallet}
variant="primary"
on:click={() => goto(loggedin ? ROUTES.POST_NEW : ROUTES.PROFILE)}
/>
</div>
<div class="btn user">
<Button icon={UserIcon} variant="secondary" on:click={() => goto(ROUTES.PROFILE)} />
</div>
</div>
</div>
{#if y === 0}
<div class="header-description">
Milestone 1 shaman pitchfork typewriter single-origin coffee beard flannel, actually
chillwave.
</div>
<div class={`subtitle ${y > 0 ? 'hide' : ''} ${cls}`}>Public timeline</div>
{/if}
</div>
</div>
<style lang="scss">
.root {
position: sticky;
top: 0;
left: 0;
right: 0;
padding: var(--spacing-12);
border-bottom: 1px solid var(--grey-200);
background-color: rgba(var(--color-body-bg-rgb), 0.93);
backdrop-filter: blur(3px);
@media (min-width: 1280px) {
border-bottom: none;
padding-bottom: 0;
padding-top: var(--spacing-48);
transition: padding 0.2s;
}
.header-content {
max-width: 1280px;
margin: 0 auto 0;
@media (min-width: 1280px) {
padding-bottom: var(--spacing-12);
border-bottom: 1px solid var(--grey-200);
transition: padding 0.2s;
}
}
.btns {
position: relative;
height: 44px;
.btn {
position: absolute;
top: 0;
right: 0;
&.user {
opacity: 1;
transition: opacity 0.2s ease-in-out;
z-index: 10;
}
&.wallet {
opacity: 0;
transition: opacity 0.2s ease-in-out;
z-index: 1;
}
}
}
.hide {
opacity: 0;
font-size: 0;
transition: opacity 0.2s, font-size 0.2s;
}
.show {
opacity: 1;
transition: opacity 0.2s, font-size 0.2s;
}
&.scrolled {
box-shadow: 0 1px 5px 0 rgba(var(--color-body-text-rgb), 0.25);
@media (min-width: 1280px) {
padding: var(--spacing-12);
transition: padding 0.2s;
.header-content {
border-bottom: none;
padding-bottom: 0;
transition: padding 0.2s;
}
}
.btn.user {
opacity: 0;
transition: opacity 0.2s ease-in-out;
z-index: 1;
}
.btn.wallet {
opacity: 1;
transition: opacity 0.2s ease-in-out;
z-index: 10;
}
.header {
padding-bottom: 0;
transition: padding 0.2s ease-in-out;
}
.header-title-wrap {
margin-left: 0;
transition: margin 0.2s ease-in-out;
.top-button {
opacity: 1;
transition: opacity 0.2s ease-in-out;
}
}
.header-description,
.subtitle.hide {
height: 0;
opacity: 0;
padding: 0;
margin: 0;
font-size: 0;
transition: height 0.2s, opacity 0.2s, padding 0.2s, margin 0.2s, font-size 0.2s;
}
}
@media (prefers-color-scheme: dark) {
border-bottom-color: var(--grey-500);
&.scrolled {
box-shadow: 0 1px 5px 0 rgba(var(--color-body-bg-rgb), 0.75);
}
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0 var(--spacing-24);
transition: padding 0.2s ease-in-out;
}
.header-title {
font-family: var(--font-body);
font-weight: 600;
font-size: 18px;
font-style: normal;
text-align: left;
}
.header-title-wrap {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-12);
margin-left: -56px;
transition: margin 0.2s ease-in-out;
.top-button {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
}
.header-description {
padding: 0 0 var(--spacing-24);
opacity: 1;
}
.subtitle {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 16px;
font-weight: 600;
padding: 0;
overflow: hidden;
opacity: 1;
}
</style>

View File

@ -0,0 +1,13 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<polygon points="17,15 17,8 15,8 15,15 8,15 8,17 15,17 15,24 17,24 17,17 24,17 24,15 " />
</svg>

View File

@ -0,0 +1,15 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<path
d="M29,27.5859l-7.5521-7.5521a11.0177,11.0177,0,1,0-1.4141,1.4141L27.5859,29ZM4,13a9,9,0,1,1,9,9A9.01,9.01,0,0,1,4,13Z"
/>
</svg>

View File

@ -0,0 +1,15 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<path
d="M27.71,4.29a1,1,0,0,0-1.05-.23l-22,8a1,1,0,0,0,0,1.87l8.59,3.43L19.59,11,21,12.41l-6.37,6.37,3.44,8.59A1,1,0,0,0,19,28h0a1,1,0,0,0,.92-.66l8-22A1,1,0,0,0,27.71,4.29Z"
/>
</svg>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<circle cx="23" cy="23.9999" r="2" />
<path
d="M30.7769,23.4785A8.64,8.64,0,0,0,23,18a8.64,8.64,0,0,0-7.7769,5.4785L15,24l.2231.5215A8.64,8.64,0,0,0,23,30a8.64,8.64,0,0,0,7.7769-5.4785L31,24ZM23,28a4,4,0,1,1,4-4A4.0045,4.0045,0,0,1,23,28Z"
/>
<path d="M12.3989,20.8A6,6,0,1,1,22,16H20a4,4,0,1,0-6.4,3.2Z" />
<path
d="M29.3047,11.0439,26.9441,6.9561a1.9977,1.9977,0,0,0-2.3728-.8946l-2.4341.8233a11.0419,11.0419,0,0,0-1.312-.7583l-.5037-2.5186A2,2,0,0,0,18.36,2H13.64a2,2,0,0,0-1.9611,1.6079l-.5037,2.5186A10.9666,10.9666,0,0,0,9.8481,6.88L7.4287,6.0615a1.9977,1.9977,0,0,0-2.3728.8946L2.6953,11.0439a2.0006,2.0006,0,0,0,.4119,2.5025l1.9309,1.6968C5.021,15.4946,5,15.7446,5,16c0,.2578.01.5127.0278.7656l-1.9206,1.688a2.0006,2.0006,0,0,0-.4119,2.5025l2.3606,4.0878a1.9977,1.9977,0,0,0,2.3728.8946l2.4341-.8233a10.9736,10.9736,0,0,0,1.312.7583l.5037,2.5186A2,2,0,0,0,13.64,30H15V28H13.64l-.71-3.5508a9.0953,9.0953,0,0,1-2.6948-1.5713l-3.4468,1.166-2.36-4.0878L7.1528,17.561a8.9263,8.9263,0,0,1-.007-3.1279L4.4275,12.0439,6.7886,7.9561l3.4267,1.1591a9.0305,9.0305,0,0,1,2.7141-1.5644L13.64,4H18.36l.71,3.5508a9.0978,9.0978,0,0,1,2.6948,1.5713l3.4468-1.166,2.36,4.0878-2.7978,2.4522L26.0923,16l2.8-2.4536A2.0006,2.0006,0,0,0,29.3047,11.0439Z"
/>
</svg>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<path d="M30,30H28V25a5.0057,5.0057,0,0,0-5-5V18a7.0078,7.0078,0,0,1,7,7Z" />
<path
d="M22,30H20V25a5.0059,5.0059,0,0,0-5-5H9a5.0059,5.0059,0,0,0-5,5v5H2V25a7.0082,7.0082,0,0,1,7-7h6a7.0082,7.0082,0,0,1,7,7Z"
/>
<path d="M20,2V4a5,5,0,0,1,0,10v2A7,7,0,0,0,20,2Z" />
<path d="M12,4A5,5,0,1,1,7,9a5,5,0,0,1,5-5m0-2a7,7,0,1,0,7,7A7,7,0,0,0,12,2Z" />
</svg>

View File

@ -0,0 +1,57 @@
<script lang="ts">
import UserMultiple from './icons/user-multiple.svelte'
let cls: string | undefined = undefined
export { cls as class }
export let name: string
export let description: string
export let postsCount: number
export let picture: string | undefined
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={`root ${cls}`} on:click>
<div class="picture"><img src={picture} alt="persona" /></div>
<div class="details">
<div class="header">{name}</div>
<div class="description">{description}</div>
<div class="post-count"><UserMultiple size={18} /> {postsCount}</div>
</div>
</div>
<style lang="scss">
.root {
display: flex;
flex-direction: row;
padding: 24px;
cursor: pointer;
&:hover {
background-color: #f9f9f9;
}
}
.picture {
width: 100px;
height: 100px;
flex-basis: 100px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.details {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 24px;
}
.post-count {
display: flex;
flex-direction: row;
}
</style>

View File

@ -1,59 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { ROUTES } from '$lib/routes'
import Button from './button.svelte'
import WalletIcon from './icons/wallet.svelte'
let cls: string | undefined = undefined
let y: number
export { cls as class }
</script>
<svelte:window bind:scrollY={y} />
<div class={`root ${y > 0 ? 'scrolled' : ''} ${cls}`}>
<Button
icon={WalletIcon}
variant="primary"
label="Connect wallet to post"
on:click={() => goto(ROUTES.PROFILE)}
/>
<div class="description">Connect a wallet to access or create your account.</div>
</div>
<style lang="scss">
.root {
top: 0;
left: 0;
right: 0;
padding: var(--spacing-24) var(--spacing-12);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-top: 1px solid var(--grey-200);
border-bottom: 1px solid var(--grey-200);
transition: height 0.2s, padding 0.2s, margin 0.2s, font-size 0.2s;
@media (min-width: 640px) {
border-bottom: none;
}
@media (min-width: 1280px) {
border: none;
outline: 1px solid var(--grey-200);
outline-offset: -0.5px;
}
@media (prefers-color-scheme: dark) {
border-top-color: var(--grey-500);
border-left-color: var(--grey-500);
border-bottom-color: var(--grey-500);
outline-color: var(--grey-500);
}
}
.description {
margin-top: var(--spacing-12);
font-size: var(--font-size-sm);
font-weight: 400;
text-align: center;
}
</style>

View File

@ -1,5 +1,7 @@
export const ROUTES = {
HOME: '/',
PROFILE: '/profile',
POST_NEW: '/post/new',
PERSONA: (slug: string) => `/persona/${slug}`,
PERSONA_NEW: '/persona/new',
POST_NEW: (slug: string) => `/persona/${slug}/post/new`,
}

View File

@ -0,0 +1,15 @@
import { writable, type Writable } from 'svelte/store'
interface ChatData {
unread: number
}
export type ChatStore = Writable<ChatData>
function createChatStore(): ChatStore {
const store = writable<ChatData>({ unread: 3 })
return store
}
export const chats = createChatStore()

View File

@ -0,0 +1,86 @@
import { writable, type Writable } from 'svelte/store'
import type { Identity } from '@semaphore-protocol/identity'
interface Persona {
identity?: Identity
picture?: string
name: string
pitch: string
description: string
postsCount: number
}
export type PersonaStore = Writable<{
draft: Persona[]
favorite: Map<string, Persona>
all: Map<string, Persona>
loading: boolean
}>
function createPersonaStore(): PersonaStore {
const store = writable({ all: new Map(), draft: [], favorite: new Map(), loading: true })
setTimeout(() => {
const chitChat = {
identity: undefined,
name: 'Chit Chat',
pitch: 'We pretty much just say gm all the time.',
description: 'We pretty much just say gm all the time.',
postsCount: 125,
picture:
'https://upload.wikimedia.org/wikipedia/commons/4/42/Chit_chat_%28256889331%29.jpg?20191121211426',
}
const expats = {
identity: undefined,
name: 'Expats',
pitch: 'Different countries, same work...',
description: 'Different countries, same work...',
postsCount: 4,
picture: 'https://upload.wikimedia.org/wikipedia/commons/8/88/British_expats_countrymap.svg',
}
const cats = {
identity: undefined,
name: 'Cats',
pitch: "Yeah it's the internet, what did you expect?",
description: "Yeah it's the internet, what did you expect?",
postsCount: 5128,
picture:
'https://upload.wikimedia.org/wikipedia/commons/thumb/3/38/Adorable-animal-cat-20787.jpg/1599px-Adorable-animal-cat-20787.jpg?20180518085718',
}
const geoPolitics = {
identity: undefined,
name: 'Geo Politics',
pitch: `Group full of "seen it all's"`,
description: `Group full of "seen it all's"`,
postsCount: 53,
picture:
'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/World_geopolitical_chess.png/1600px-World_geopolitical_chess.png?20200226194321',
}
const controversy = {
identity: undefined,
name: 'Controversy',
pitch: '...',
description: '...',
postsCount: 9999,
picture:
'https://upload.wikimedia.org/wikipedia/en/e/ea/Controversy_legend.gif?20060220215816',
}
const all = new Map<string, Persona>()
const favorite = new Map<string, Persona>()
all.set('1', chitChat)
all.set('2', expats)
all.set('3', cats)
all.set('4', geoPolitics)
all.set('5', controversy)
favorite.set('3', cats)
favorite.set('4', controversy)
store.set({ draft: [], all, favorite, loading: false })
}, 1000)
return store
}
export const personas = createPersonaStore()

View File

@ -5,7 +5,6 @@ import { subscribeToPosts } from '$lib/services/posts'
export interface Post {
timestamp: number
text: string
tx: string
}
interface PostData {
@ -29,7 +28,6 @@ async function fetchPosts() {
posts.add({
text: post.text,
timestamp: Date.now(),
tx: '',
})
},
undefined,

View File

@ -1,16 +1,15 @@
import { writable, type Writable } from 'svelte/store'
import type { Signer } from 'ethers'
import type { Identity } from '@semaphore-protocol/identity'
export interface Profile {
signer?: Signer
identities: Record<string, Identity>
address?: string
}
export type ProfileStore = Writable<Profile>
function createProfileStore(): ProfileStore {
return writable<Profile>({ identities: {} })
return writable<Profile>({})
}
export const profile = createProfileStore()

View File

@ -1,115 +1,104 @@
<script lang="ts">
// import Header from '$lib/components/header.svelte'
import HeaderTop from '$lib/components/header-top.svelte'
import HeaderDescription from '$lib/components/header-description.svelte'
import Post from '$lib/components/post.svelte'
import Button from '$lib/components/button.svelte'
import WalletConnect from '$lib/components/wallet-connect.svelte'
import Edit from '$lib/components/icons/edit.svelte'
import Persona from '$lib/components/persona.svelte'
import { posts } from '$lib/stores/post'
import { profile } from '$lib/stores/profile'
import { personas } from '$lib/stores/persona'
import { goto } from '$app/navigation'
import { browser } from '$app/environment'
import Masonry from '$lib/masonry.svelte'
import { ROUTES } from '$lib/routes'
let windowWidth: number = browser ? window.innerWidth : 0
import Button from '$lib/components/button.svelte'
import Search from '$lib/components/icons/search.svelte'
import SettingsView from '$lib/components/icons/settings-view.svelte'
import { chats } from '$lib/stores/chat'
import Add from '$lib/components/icons/add.svelte'
function getMasonryColumnWidth(windowInnerWidth: number) {
if (windowInnerWidth < 739) {
return '100%'
}
if (windowInnerWidth < 1060) {
return 'minmax(min(100%/2, max(320px, 100%/2)), 1fr)'
}
if (windowInnerWidth < 1381) {
return 'minmax(min(100%/3, max(320px, 100%/3)), 1fr)'
}
if (windowInnerWidth < 1702) {
return 'minmax(min(100%/4, max(320px, 100%/4)), 1fr)'
}
if (windowInnerWidth < 2023) {
return 'minmax(min(100%/5, max(320px, 100%/5)), 1fr)'
}
if (windowInnerWidth < 2560) {
return 'minmax(min(100%/6, max(320px, 100%/6)), 1fr)'
}
if (windowInnerWidth < 3009) {
return 'minmax(min(100%/7, max(320px, 100%/7)), 1fr)'
}
return 'minmax(323px, 1fr)'
}
let filterText = ''
let showChat = false
</script>
<svelte:window bind:innerWidth={windowWidth} />
<div>
<!-- <Header loggedin={$profile.signer !== undefined} /> -->
<HeaderTop loggedin={$profile.signer !== undefined} />
<HeaderDescription />
<HeaderTop address={$profile.address} />
<div class="wrapper">
{#if $profile.signer !== undefined}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="new-post-button" on:click={() => goto('/post/new')}>
Share freely...
<Button variant="primary" label="Create post" icon={Edit} />
<div class="nav">
<div class={showChat ? '' : 'active'} on:click={() => (showChat = false)}>Personas</div>
<div class={showChat ? 'active' : ''} on:click={() => (showChat = true)}>
Chats
{#if $chats.unread > 0}
<div class="unread">{$chats.unread}</div>
{/if}
</div>
</div>
{:else}
<WalletConnect />
{/if}
{#if $posts.loading}
<p>Loading posts...</p>
{:else if $posts.posts.length == 0}
<p>There are no posts yet</p>
{#if showChat}
<div>Chat not implemented yet</div>
{:else if $personas.loading}
<p>Loading personas...</p>
{:else}
<Masonry gridGap="0" colWidth={getMasonryColumnWidth(windowWidth)} items={$posts.posts}>
{#each $posts.posts as post}
<Post {post} />
{#if $personas.draft.length !== 0 && $profile.signer !== undefined}
<div class="subtitle">Draft personas</div>
<Button icon={Add} label="Create persona" on:click={() => goto(ROUTES.PERSONA_NEW)} />
<div class="grid">
{#each $personas.draft as draftPersona, index}
<Persona
name={draftPersona.name}
description={draftPersona.description}
postsCount={draftPersona.postsCount}
on:click={() => goto(ROUTES.PERSONA(index.toFixed()))}
picture={draftPersona.picture}
/>
{/each}
</div>
{/if}
{#if $personas.favorite.size !== 0 && $profile.signer !== undefined}
<div class="subtitle">Favorites</div>
<div class="grid">
{#each [...$personas.favorite] as [groupId, data]}
<Persona
name={data.name}
description={data.description}
postsCount={data.postsCount}
on:click={() => goto(ROUTES.PERSONA(groupId))}
picture={data.picture}
/>
{/each}
</div>
{/if}
<div class="subtitle">All personas</div>
<Search />
<input bind:value={filterText} placeholder="Search..." />
{#if $profile.signer !== undefined}
<Button icon={Add} label="Create persona" on:click={() => goto(ROUTES.PERSONA_NEW)} />
{/if}
<Button icon={SettingsView} />
<div class="grid">
{#each [...$personas.all].filter(([, data]) => data.name
.toLowerCase()
.includes(filterText.toLowerCase())) as [groupId, data]}
<Persona
name={data.name}
description={data.pitch}
postsCount={data.postsCount}
on:click={() => goto(ROUTES.PERSONA(groupId))}
picture={data.picture}
/>
{:else}
<p>There are no personas yet</p>
{/each}
</Masonry>
</div>
{/if}
</div>
</div>
<style lang="scss">
.new-post-button {
font-family: var(--font-serif);
padding: var(--spacing-24) var(--spacing-12);
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--spacing-12);
align-items: center;
border-top: 1px solid var(--grey-200);
border-bottom: 1px solid var(--grey-200);
cursor: pointer;
@media (min-width: 640px) {
border-bottom: none;
}
@media (min-width: 1280px) {
border: none;
outline: 1px solid var(--grey-200);
outline-offset: -0.5px;
}
@media (prefers-color-scheme: dark) {
border-top-color: var(--grey-500);
border-left-color: var(--grey-500);
border-bottom-color: var(--grey-500);
outline-color: var(--grey-500);
}
}
.wrapper {
margin-left: -1px;
@ -118,4 +107,49 @@
margin: 0 auto 0;
}
}
.nav {
width: 450px;
height: 50px;
margin: auto;
border-radius: 25px;
background-color: #ececec;
display: flex;
align-items: center;
border: solid 3px #ececec;
font-family: var(--font-body);
font-size: 16px;
font-weight: 600;
div {
padding: 10px;
width: 50%;
border-radius: 25px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
div.active {
background-color: white;
}
.unread {
background-color: black;
color: white;
width: min-content;
margin-left: 6px;
font-size: 12px;
font-weight: bold;
}
}
.grid {
display: grid;
grid-auto-columns: auto;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
grid-auto-rows: auto;
}
</style>

View File

@ -0,0 +1,98 @@
<script lang="ts">
import HeaderTop from '$lib/components/header-top.svelte'
import Post from '$lib/components/post.svelte'
import Button from '$lib/components/button.svelte'
import Edit from '$lib/components/icons/edit.svelte'
import { posts } from '$lib/stores/post'
import { profile } from '$lib/stores/profile'
import { goto } from '$app/navigation'
import { browser } from '$app/environment'
import { page } from '$app/stores'
import Masonry from '$lib/masonry.svelte'
import { ROUTES } from '$lib/routes'
let windowWidth: number = browser ? window.innerWidth : 0
function getMasonryColumnWidth(windowInnerWidth: number) {
if (windowInnerWidth < 739) return '100%'
if (windowInnerWidth < 1060) return 'minmax(min(100%/2, max(320px, 100%/2)), 1fr)'
if (windowInnerWidth < 1381) return 'minmax(min(100%/3, max(320px, 100%/3)), 1fr)'
if (windowInnerWidth < 1702) return 'minmax(min(100%/4, max(320px, 100%/4)), 1fr)'
if (windowInnerWidth < 2023) return 'minmax(min(100%/5, max(320px, 100%/5)), 1fr)'
if (windowInnerWidth < 2560) return 'minmax(min(100%/6, max(320px, 100%/6)), 1fr)'
if (windowInnerWidth < 3009) return 'minmax(min(100%/7, max(320px, 100%/7)), 1fr)'
return 'minmax(323px, 1fr)'
}
const isDraft = $page.url.searchParams.has('draft')
</script>
<svelte:window bind:innerWidth={windowWidth} />
<div>
<HeaderTop address={$profile.address} />
<Button label="GO BACK" on:click={() => goto(ROUTES.HOME)} />
<div class="wrapper">
{#if $profile.signer !== undefined}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="new-post-button" on:click={() => goto(ROUTES.POST_NEW($page.params.id))}>
Share freely...
<Button variant="primary" label="Create post" icon={Edit} />
</div>
{/if}
{#if $posts.loading}
<p>Loading posts...</p>
{:else if $posts.posts.length == 0}
<p>There are no posts yet</p>
{:else}
<Masonry gridGap="0" colWidth={getMasonryColumnWidth(windowWidth)} items={$posts.posts}>
{#each $posts.posts as post}
<Post {post} />
{/each}
</Masonry>
{/if}
</div>
</div>
<style lang="scss">
.new-post-button {
font-family: var(--font-serif);
padding: var(--spacing-24) var(--spacing-12);
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--spacing-12);
align-items: center;
border-top: 1px solid var(--grey-200);
border-bottom: 1px solid var(--grey-200);
cursor: pointer;
@media (min-width: 640px) {
border-bottom: none;
}
@media (min-width: 1280px) {
border: none;
outline: 1px solid var(--grey-200);
outline-offset: -0.5px;
}
@media (prefers-color-scheme: dark) {
border-top-color: var(--grey-500);
border-left-color: var(--grey-500);
border-bottom-color: var(--grey-500);
outline-color: var(--grey-500);
}
}
.wrapper {
margin-left: -1px;
@media (min-width: 739px) {
padding: 0 var(--spacing-48);
margin: 0 auto 0;
}
}
</style>

View File

@ -7,10 +7,13 @@
import { goto } from '$app/navigation'
import { ROUTES } from '$lib/routes'
import {
createIdentity,
generateGroupProof,
getContractGroup,
getGlobalAnonymousFeed,
getRandomExternalNullifier,
joinGroupOffChain,
joinGroupOnChain,
} from '$lib/services/index'
import { posts } from '$lib/stores/post'
import { hashPost, createPost } from '$lib/services/posts'
@ -27,12 +30,21 @@
const signer = $profile.signer
if (!signer) throw new Error('no signer')
const identity = $profile.identities.anonymous
if (!identity) throw new Error('no identity')
const defaultIdentity = 'anonymous'
const identity = await createIdentity(signer, defaultIdentity)
const globalAnonymousFeed = getGlobalAnonymousFeed(signer)
const group = await getContractGroup(globalAnonymousFeed)
const commitment = identity.commitment
if (!group.members.includes(commitment)) {
joinGroupOffChain(group, commitment)
const txres = await joinGroupOnChain(globalAnonymousFeed, commitment)
console.log(txres)
}
const post = { text: postText }
const signal = hashPost(post)
@ -45,7 +57,6 @@
posts.add({
timestamp: Date.now(),
text: postText,
tx: '',
})
goto(ROUTES.HOME)
} catch (error) {

View File

@ -0,0 +1,35 @@
<script lang="ts">
import Undo from '$lib/components/icons/undo.svelte'
import Button from '$lib/components/button.svelte'
import { personas } from '$lib/stores/persona'
let name = ''
let pitch = ''
let description = ''
</script>
<Button icon={Undo} on:click={() => history.back()} />
<span>Create persona</span>
<label>
Persona name
<input type="text" bind:value={name} placeholder="Enter a short memorable name…" />
</label>
<label>
Persona pitch
<textarea bind:value={pitch} />
</label>
<label>
Persona description
<textarea bind:value={description} />
</label>
<Button label="Cancel" on:click={() => history.back()} />
<Button
label="Proceed"
on:click={() => {
$personas.draft
}}
/>

View File

@ -6,15 +6,7 @@
import Wallet from '$lib/components/icons/wallet.svelte'
import WalletInfo from '$lib/components/wallet-info.svelte'
import { formatAddress } from '$lib/utils'
import {
connectWallet,
canConnectWallet,
createIdentity,
getGlobalAnonymousFeed,
getContractGroup,
joinGroupOffChain,
joinGroupOnChain,
} from '$lib/services'
import { connectWallet, canConnectWallet } from '$lib/services'
import { profile } from '$lib/stores/profile'
let y: number
@ -25,24 +17,9 @@
const handleConnect = async () => {
try {
const signer = await connectWallet()
$profile.signer = signer
const address = await signer.getAddress()
const defaultIdentity = 'anonymous'
const identity = await createIdentity(signer, defaultIdentity)
$profile.identities = { ...$profile.identities, [defaultIdentity]: identity }
const globalAnonymousFeed = getGlobalAnonymousFeed(signer)
const group = await getContractGroup(globalAnonymousFeed)
const commitment = identity.commitment
if (!group.members.includes(commitment)) {
joinGroupOffChain(group, commitment)
const txres = await joinGroupOnChain(globalAnonymousFeed, commitment)
console.log(txres)
}
$profile = { signer, address }
} catch (err) {
error = err as Error
}
@ -98,20 +75,11 @@
variant="primary"
icon={Logout}
label="Logout"
on:click={() => ($profile.signer = undefined)}
on:click={() => ($profile = {})}
disabled={!$profile.signer}
/>
</div>
{/if}
<div class="info">
{#each Object.entries($profile.identities) as [name, identity]}
<div>{name}</div>
<div>commitment: {identity.getCommitment().toString(16)}</div>
<div>nullifier: {identity.getNullifier().toString(16)}</div>
<div>trapdoor: {identity.getTrapdoor().toString(16)}</div>
{/each}
</div>
</div>
<style lang="scss">
@ -192,13 +160,4 @@
text-align: center;
}
}
.info {
padding: var(--spacing-12);
max-width: 100%;
word-wrap: break-word;
> div:not(:first-child) {
margin-top: var(--spacing-12);
}
}
</style>

View File

@ -3,5 +3,5 @@ import { expect, test } from '@playwright/test'
test('index page has expected header', async ({ page }) => {
await page.goto(ROUTES.HOME)
expect(await page.textContent('span')).toBe('The Outlet')
expect(await page.textContent('span')).toBe('Kurate')
})