Skip to content

Commit 3d73646

Browse files
authored
Merge pull request #480 from dev-protocol/feature/create-votes-page
投票一覧ページ
2 parents abb63dd + bc7b5f9 commit 3d73646

File tree

8 files changed

+285
-6570
lines changed

8 files changed

+285
-6570
lines changed

.preview/config.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,21 @@ export default () =>
5151
key: 'feeds',
5252
value: [
5353
{
54-
id: 'default-2',
54+
id: 'default-2-id',
55+
title: 'default-2-title',
56+
slugs: 'default-2-slug',
57+
database: {
58+
type: 'documents:redis',
59+
key: uuidv5(
60+
toUtf8Bytes('default-2'),
61+
uuidv5('EXAMPLE_NAMESPACE', uuidv5.URL),
62+
), // > posts::694666bb-b2ec-542b-a5d6-65b470e5c494
63+
},
64+
},
65+
{
66+
id: 'default-3',
67+
title: '',
68+
slugs: 'default-3-slug',
5569
database: {
5670
type: 'documents:redis',
5771
key: uuidv5(

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devprotocol/clubs-plugin-posts-voting",
3-
"version": "0.6.1",
3+
"version": "0.7.0",
44
"type": "module",
55
"description": "Template repository for using TypeScript",
66
"main": "dist/index.js",
@@ -40,7 +40,10 @@
4040
"ethers": "6.12.1",
4141
"ramda": "0.30.0",
4242
"sass": "1.77.2",
43-
"uuid": "^9.0.1"
43+
"uuid": "^9.0.1",
44+
"@boringer-avatars/vue3": "^0.2.1",
45+
"remark": "^15.0.1",
46+
"strip-markdown": "^6.0.0"
4447
},
4548
"resolutions": {
4649
"@devprotocol/util-ts": "4.0.0"

src/Pages/Votes.astro

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
import { Option } from '../types'
3+
import { ClubsPropsPages, Membership } from '@devprotocol/clubs-core'
4+
import Votes from '../components/Pages/Votes.vue'
5+
import type { OptionsDatabase } from '@devprotocol/clubs-plugin-posts'
6+
7+
interface Props extends ClubsPropsPages {
8+
options: Option[]
9+
propertyAddress: string
10+
adminRolePoints: number
11+
rpcUrl: string
12+
feeds: OptionsDatabase[]
13+
postsPluginId: string
14+
}
15+
16+
const { options, feeds, postsPluginId } = Astro.props
17+
---
18+
19+
<Votes
20+
client:load
21+
options={options}
22+
feeds={feeds}
23+
postsPluginId={postsPluginId}
24+
/>

src/assets/images/icon-lock.png

7.27 KB
Loading

src/components/Pages/Votes.vue

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<script lang="ts" setup>
2+
import type { Option } from '../../types.ts'
3+
import type { OptionsDatabase, Posts } from '@devprotocol/clubs-plugin-posts'
4+
import { type ClubsProfile, decode } from '@devprotocol/clubs-core'
5+
import { onMounted, ref } from 'vue'
6+
import Profile from '../Votes/Profile.vue'
7+
import IconLock from '../../assets/images/icon-lock.png'
8+
import { remark } from 'remark'
9+
import strip from 'strip-markdown'
10+
11+
type Props = {
12+
options: Option[]
13+
feeds: OptionsDatabase[]
14+
postsPluginId: string
15+
}
16+
const props = defineProps<Props>()
17+
18+
const polls = ref<any[]>([])
19+
20+
const { postsPluginId, feeds } = props
21+
22+
type Polls = {
23+
title: string
24+
values: PostsPlus[]
25+
}
26+
27+
type PostsPlus = Posts & {
28+
image: string
29+
profile: Promise<{
30+
readonly profile: ClubsProfile | undefined
31+
readonly error: Error | undefined
32+
}>
33+
stripedMarkdown: string
34+
}
35+
36+
const feedId = ref('')
37+
38+
const fetchPolls = async (feed: OptionsDatabase): Promise<Polls> => {
39+
feedId.value = feed.id
40+
const title = feed.title
41+
42+
const url = new URL(
43+
`/api/${postsPluginId}/${feedId.value}/search/has:option/%23poll`,
44+
window.location.origin,
45+
)
46+
47+
const res = await fetch(url.toString())
48+
const json = await res.json()
49+
50+
return {
51+
title: title ? title : feedId.value,
52+
values: decode(json.contents),
53+
}
54+
}
55+
56+
onMounted(async () => {
57+
await Promise.all(
58+
feeds.map(async (feed) => {
59+
const data = await fetchPolls(feed)
60+
61+
data.values = data.values.map((post) => {
62+
post.updated_at = new Date(post.updated_at).toLocaleString('ja-JP')
63+
64+
const images = post.options.find((item) => item.key === '#images')
65+
if (images && images.value.length > 0) {
66+
post.image = images.value[0]
67+
}
68+
69+
remark()
70+
.use(strip)
71+
.process(post.content)
72+
.then((text) => {
73+
post.stripedMarkdown = text.toString()
74+
})
75+
76+
return post
77+
})
78+
79+
polls.value = [...polls.value, data]
80+
}),
81+
)
82+
})
83+
</script>
84+
85+
<template>
86+
<div class="p-4">
87+
<div v-for="poll in polls" :key="poll.title">
88+
<h2 class="mb-4 text-xl">{{ poll.title }}</h2>
89+
<a
90+
v-for="post in poll.values"
91+
:key="post.id"
92+
:href="`/posts/${feedId}/${post.id}`"
93+
class="block mb-4 p-2 bg-gray-100 rounded"
94+
>
95+
<div class="flex justify-between gap-2">
96+
<div class="w-full">
97+
<p class="text-lg font-bold">Post_title: {{ post.title }}</p>
98+
<p class="mb-1 text-xs text-gray-400">{{ post.updated_at }}</p>
99+
<div class="flex justify-between gap-2">
100+
<Profile :address="post.created_by" />
101+
<p v-if="true" class="flex-grow flex-wrap text-lg truncate">
102+
{{ post.stripedMarkdown }}
103+
</p>
104+
<div
105+
v-else
106+
class="flex flex-col justify-center items-center flex-grow p-2 bg-gray-200 rounded"
107+
>
108+
<img
109+
class="mb-1 w-5"
110+
:src="IconLock.src"
111+
alt="paper-airplane"
112+
/>
113+
<p class="leading-none">Locked</p>
114+
</div>
115+
</div>
116+
</div>
117+
<figure v-if="!isMasked && post.image">
118+
<img
119+
:src="post.image"
120+
class="rounded max-w-20 max-h-20 object-cover object-center"
121+
alt="post image"
122+
/>
123+
</figure>
124+
</div>
125+
</a>
126+
<div v-if="poll.length < 1" class="mb-4 p-2 bg-gray-100 rounded">
127+
<p class="w-full text-gray-400 text-center">Empty :)</p>
128+
</div>
129+
</div>
130+
</div>
131+
</template>

src/components/Votes/Profile.vue

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script setup lang="ts">
2+
import { onMounted, ref } from 'vue'
3+
import { ZeroAddress } from 'ethers'
4+
import { Avatar } from '@boringer-avatars/vue3'
5+
import { fetchProfile } from '@devprotocol/clubs-core'
6+
7+
type Props = {
8+
address: string
9+
}
10+
11+
const props = defineProps<Props>()
12+
13+
const avatar = ref('')
14+
const name = ref('')
15+
16+
onMounted(() => {
17+
if (!props.address || props.address === ZeroAddress) {
18+
name.value = truncateEthAddress(props.address)
19+
return
20+
}
21+
22+
// fetch profile
23+
getProfile(props.address)
24+
})
25+
26+
const truncateEthAddress = (address: string) => {
27+
const match = address.match(
28+
/^(0x[a-zA-Z0-9]{4})[a-zA-Z0-9]+([a-zA-Z0-9]{4})$/,
29+
)
30+
if (!match) return address
31+
return `${match[1]}\u2026${match[2]}`
32+
}
33+
34+
const getProfile = async (address: string) => {
35+
const res = await fetchProfile(address)
36+
if (res?.error) {
37+
console.error(res.error)
38+
return
39+
}
40+
41+
avatar.value = res?.profile?.avatar || ''
42+
name.value = res?.profile?.username ?? truncateEthAddress(address)
43+
}
44+
</script>
45+
46+
<template>
47+
<div class="">
48+
<template v-if="avatar">
49+
<div
50+
class="h-8 w-8 rounded-full bg-cover bg-center bg-no-repeat"
51+
:style="`background-image: url(${avatar})`"
52+
/>
53+
</template>
54+
<template v-else>
55+
<Avatar
56+
class=""
57+
:title="false"
58+
:size="32"
59+
variant="beam"
60+
:name="props.address"
61+
:square="false"
62+
/>
63+
</template>
64+
</div>
65+
</template>
66+
67+
<style scoped>
68+
.posts-username {
69+
overflow: hidden;
70+
display: -webkit-box;
71+
-webkit-box-orient: vertical;
72+
-webkit-line-clamp: 2;
73+
}
74+
</style>

src/index.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type {
22
ClubsFunctionGetApiPaths,
3+
ClubsFunctionGetPagePaths,
34
ClubsFunctionGetSlots,
45
ClubsFunctionPlugin,
56
ClubsPluginMeta,
67
} from '@devprotocol/clubs-core'
78
import { ClubsPluginCategory } from '@devprotocol/clubs-core'
8-
import { SlotName } from '@devprotocol/clubs-plugin-posts'
9+
import { type OptionsDatabase, SlotName } from '@devprotocol/clubs-plugin-posts'
910
import { votingHandler } from './ApiHandler'
1011
import Icon from './assets/images/Voting.png'
1112
import Preview1 from './assets/images/voting-preview01.png'
@@ -14,6 +15,8 @@ import Preview3 from './assets/images/voting-preview03.png'
1415
import Readme from './readme.astro'
1516
import AfterContentForm from './components/edit-after-content-form.astro'
1617
import AfterPostContent from './components/feed-after-post-content.astro'
18+
import type { UndefinedOr } from '@devprotocol/util-ts'
19+
import Votes from './Pages/Votes.astro'
1720

1821
export const getSlots = (async () => {
1922
return [
@@ -50,8 +53,40 @@ export const getApiPaths = (async () => {
5053
]
5154
}) satisfies ClubsFunctionGetApiPaths
5255

56+
const getPagePaths = (async (
57+
options,
58+
{ propertyAddress, adminRolePoints, rpcUrl },
59+
{ getPluginConfigById },
60+
) => {
61+
const [postsPlugin] = getPluginConfigById('devprotocol:clubs:plugin:posts')
62+
63+
const feeds = postsPlugin?.options?.find(
64+
({ key }: Readonly<{ key: string }>) => key === 'feeds',
65+
)?.value as UndefinedOr<readonly OptionsDatabase[]>
66+
67+
const props = {
68+
options,
69+
propertyAddress,
70+
adminRolePoints,
71+
rpcUrl,
72+
feeds,
73+
postsPluginId: postsPlugin?.id,
74+
}
75+
76+
return [
77+
{
78+
paths: ['votes'],
79+
component: Votes,
80+
props: {
81+
...props,
82+
},
83+
},
84+
]
85+
}) satisfies ClubsFunctionGetPagePaths
86+
5387
export default {
5488
getSlots,
5589
meta,
5690
getApiPaths,
91+
getPagePaths,
5792
} satisfies ClubsFunctionPlugin

0 commit comments

Comments
 (0)