Browse Source

feat: support single feed endpoint

pull/9323/head
DarkPhoenix2704 2 months ago
parent
commit
431fa80018
  1. 2
      packages/nc-gui/assets/nc-icons/star.svg
  2. 79
      packages/nc-gui/components/feed/Changelog/Item.vue
  3. 13
      packages/nc-gui/components/feed/Changelog/index.vue
  4. 4
      packages/nc-gui/components/feed/Roadmap.vue
  5. 36
      packages/nc-gui/components/feed/Twitter/index.vue
  6. 7
      packages/nc-gui/components/feed/View.vue
  7. 23
      packages/nc-gui/components/feed/Youtube/Player.vue
  8. 20
      packages/nc-gui/components/feed/Youtube/index.vue
  9. 1
      packages/nc-gui/components/nc/Badge.vue
  10. 100
      packages/nc-gui/composables/useProductFeed.ts
  11. 11
      packages/nc-gui/lib/types.ts

2
packages/nc-gui/assets/nc-icons/star.svg

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="star"> <g id="star">
<path id="Vector" d="M7.99992 1.33333L10.0599 5.50666L14.6666 6.18L11.3333 9.42666L12.1199 14.0133L7.99992 11.8467L3.87992 14.0133L4.66659 9.42666L1.33325 6.18L5.93992 5.50666L7.99992 1.33333Z" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path id="Vector" d="M7.99992 1.33333L10.0599 5.50666L14.6666 6.18L11.3333 9.42666L12.1199 14.0133L7.99992 11.8467L3.87992 14.0133L4.66659 9.42666L1.33325 6.18L5.93992 5.50666L7.99992 1.33333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 410 B

79
packages/nc-gui/components/feed/Changelog/Item.vue

@ -5,18 +5,45 @@ import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype' import remarkRehype from 'remark-rehype'
import { unified } from 'unified' import { unified } from 'unified'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { ProductFeedItem } from '../../../lib/types'
const props = defineProps<{ const props = defineProps<{
body: string item: ProductFeedItem
date: string
}>() }>()
const {
item: { CreatedAt, Description, Url, Title, Tags },
} = props
const iconColorMap = {
'Hotfix': {
icon: iconMap.ncTool,
color: 'red',
},
'Feature': {
icon: iconMap.star,
color: 'purple',
},
'Bug fixes': {
icon: iconMap.ncTool,
color: 'green',
},
}
const tags = computed(() => {
return Tags?.split(',').map((tag) => ({
text: tag,
href: `/tags/${tag}`,
...(iconColorMap[tag as any] || {}),
}))
})
const renderMarkdown = async (markdown: string) => { const renderMarkdown = async (markdown: string) => {
return await unified().use(remarkParse).use(remarkRehype).use(rehypeSanitize).use(rehypeStringify).process(markdown) return await unified().use(remarkParse).use(remarkRehype).use(rehypeSanitize).use(rehypeStringify).process(markdown)
} }
const renderedText = computedAsync(async () => { const renderedText = computedAsync(async () => {
return await renderMarkdown(props.body) return await renderMarkdown(Description)
}) })
</script> </script>
@ -29,14 +56,52 @@ const renderedText = computedAsync(async () => {
</div> </div>
<div class="aside-inner"> <div class="aside-inner">
<div class="text-sm text-gray-700 leading-5"> <div class="text-sm text-gray-700 leading-5">
{{ dayjs(date).format('MMMM D, YYYY') }} {{ dayjs(CreatedAt).format('MMMM D, YYYY') }}
</div> </div>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<div class="flex flex-col py-6 gap-8"> <div class="flex flex-col py-6 gap-8">
<div class="prose max-w-none" v-html="renderedText"></div> <div class="flex items-center">
<div
v-for="tag in tags"
:key="tag.text"
:class="{
'bg-red-50': tag.color === 'red',
'bg-purple-50': tag.color === 'purple',
'bg-green-50': tag.color === 'green',
}"
:href="tag.href"
class="mr-4 flex gap-2 items-center px-1 rounded-md"
>
<component
:is="tag.icon"
:class="{
'fill-red-700 text-transparent': tag.color === 'red',
'fill-purple-700 text-transparent': tag.color === 'purple',
'fill-green-700 text-transparent': tag.color === 'green',
}"
class="w-4 h-4"
/>
<span
:class="{
'text-red-500': tag.color === 'red',
'text-purple-500': tag.color === 'purple',
'text-green-500': tag.color === 'green',
}"
class="leading-5"
>
{{ tag.text }}
</span>
</div>
</div>
<div class="flex flex-col gap-2">
<NcBadge :border="false" color="brand" class="font-semibold nc-title-badge">
{{ Title }}
</NcBadge>
<div class="prose max-w-none" v-html="renderedText"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -44,6 +109,10 @@ const renderedText = computedAsync(async () => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.nc-title-badge {
width: fit-content;
}
.content { .content {
@apply !pl-50; @apply !pl-50;
} }

13
packages/nc-gui/components/feed/Changelog/index.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const { loadGithubFeed, githubFeed } = useProductFeed() const { loadFeed, githubFeed } = useProductFeed()
const scrollContainer = ref<HTMLElement>() const scrollContainer = ref<HTMLElement>()
@ -7,9 +7,14 @@ const { isLoading } = useInfiniteScroll(
scrollContainer, scrollContainer,
async () => { async () => {
if (isLoading.value) return if (isLoading.value) return
await loadGithubFeed(true) const data = await loadFeed({
type: 'github',
loadMore: true,
})
githubFeed.value = [...githubFeed.value, ...data]
}, },
{ distance: 10 }, { distance: 1 },
) )
</script> </script>
@ -22,7 +27,7 @@ const { isLoading } = useInfiniteScroll(
class="overflow-y-auto nc-scrollbar-md mx-auto w-full" class="overflow-y-auto nc-scrollbar-md mx-auto w-full"
> >
<div class="max-w-260 mx-auto"> <div class="max-w-260 mx-auto">
<FeedChangelogItem v-for="feed in githubFeed" :key="feed.id" :date="feed.published_at" :body="feed?.body" /> <FeedChangelogItem v-for="feed in githubFeed" :item="feed" />
</div> </div>
</div> </div>
</template> </template>

4
packages/nc-gui/components/feed/Roadmap.vue

@ -34,6 +34,10 @@ const handleIframeLoad = () => {
'block h-full': isLoaded, 'block h-full': isLoaded,
}" }"
> >
<!--
src="https://w21dqb1x.nocodb.com/#/nc/kanban/d719962a-1666-464f-8789-054a13a747f7"
-->
<iframe <iframe
ref="iFrame" ref="iFrame"
src="http://localhost:3000/#/nc/kanban/dc9d297d-2d89-4a33-9804-87924148913a" src="http://localhost:3000/#/nc/kanban/dc9d297d-2d89-4a33-9804-87924148913a"

36
packages/nc-gui/components/feed/Twitter/index.vue

@ -0,0 +1,36 @@
<script setup lang="ts">
import Tweet from 'vue-tweet'
const { twitterFeed, loadFeed } = useProductFeed()
const scrollContainer = ref<HTMLElement>()
const { isLoading } = useInfiniteScroll(
scrollContainer,
async () => {
if (isLoading.value) return
const data = await loadFeed({
type: 'twitter',
loadMore: true,
})
twitterFeed.value = [...twitterFeed.value, ...data]
},
{ distance: 1 },
)
</script>
<template>
<div
ref="scrollContainer"
:style="{
height: 'calc(100dvh - var(--toolbar-height) - 3.25rem)',
}"
class="overflow-y-auto nc-scrollbar-md w-full"
>
<div class="flex items-center flex-col">
<Tweet v-for="feed in twitterFeed" :key="feed.Id" :tweet-url="feed.Url" />
</div>
</div>
</template>
<style scoped lang="scss"></style>

7
packages/nc-gui/components/feed/View.vue

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import FeedRecents from './Changelog/index.vue' import FeedRecents from './Changelog/index.vue'
import FeedYoutube from './Youtube/index.vue' import FeedYoutube from './Youtube/index.vue'
import FeedTwitter from './Twitter/index.vue'
import FeedRoadmap from './Roadmap.vue' import FeedRoadmap from './Roadmap.vue'
const { activeTab } = useProductFeed() const { activeTab } = useProductFeed()
@ -29,6 +30,12 @@ const tabs = [
title: 'Youtube', title: 'Youtube',
container: FeedYoutube, container: FeedYoutube,
}, },
{
key: 'twitter',
icon: 'ncTwitter',
title: 'Twitter',
container: FeedTwitter,
},
] ]
</script> </script>

23
packages/nc-gui/components/feed/Youtube/Player.vue

@ -1,15 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import Plyr from 'plyr' import Plyr from 'plyr'
import 'plyr/dist/plyr.css' import 'plyr/dist/plyr.css'
import type { ProductFeedItem } from '../../../lib/types'
defineProps<{ const props = defineProps<{
body: string item: ProductFeedItem
name: string
published_at: string
embed_url: string
html_url: string
}>() }>()
const {
item: { Title, Description, Url },
} = props
const videoPlayer = ref<HTMLElement>() const videoPlayer = ref<HTMLElement>()
const player = ref() const player = ref()
@ -18,6 +19,10 @@ onMounted(() => {
if (!videoPlayer.value) return if (!videoPlayer.value) return
player.value = new Plyr(videoPlayer.value, { player.value = new Plyr(videoPlayer.value, {
previewThumbnails: {}, previewThumbnails: {},
quality: {
default: 1080,
options: [720, 1080, 2160],
},
}) })
}) })
@ -33,7 +38,7 @@ onBeforeUnmount(() => {
<div class="aspect-video !rounded-lg mx-auto !h-[428px]"> <div class="aspect-video !rounded-lg mx-auto !h-[428px]">
<div id="player" ref="videoPlayer" class="plyr__video-embed"> <div id="player" ref="videoPlayer" class="plyr__video-embed">
<iframe <iframe
:src="`${embed_url}?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1`" :src="`${Url}?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1`"
allowfullscreen allowfullscreen
allowtransparency allowtransparency
allow="autoplay" allow="autoplay"
@ -41,10 +46,10 @@ onBeforeUnmount(() => {
</div> </div>
</div> </div>
<div class="text-gray-900 font-bold text-2xl"> <div class="text-gray-900 font-bold text-2xl">
{{ name }} {{ Title }}
</div> </div>
<div class="text-gray-900"> <div class="text-gray-900">
{{ body }} {{ Description.length > 200 ? `${Description.slice(0, 280)}...` : Description }}
</div> </div>
</div> </div>
</template> </template>

20
packages/nc-gui/components/feed/Youtube/index.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const { youtubeFeed, loadYoutubeFeed } = useProductFeed() const { youtubeFeed, loadFeed } = useProductFeed()
const scrollContainer = ref<HTMLElement>() const scrollContainer = ref<HTMLElement>()
@ -7,9 +7,13 @@ const { isLoading } = useInfiniteScroll(
scrollContainer, scrollContainer,
async () => { async () => {
if (isLoading.value) return if (isLoading.value) return
await loadYoutubeFeed(true) const data = await loadFeed({
type: 'youtube',
loadMore: true,
})
youtubeFeed.value = [...youtubeFeed.value, ...data]
}, },
{ distance: 10 }, { distance: 1 },
) )
const gotoChannel = () => { const gotoChannel = () => {
@ -32,15 +36,7 @@ const gotoChannel = () => {
</div> </div>
<div class="flex gap-2 flex-col"> <div class="flex gap-2 flex-col">
<FeedYoutubePlayer <FeedYoutubePlayer v-for="feed in youtubeFeed" :item="feed" />
v-for="feed in youtubeFeed"
:key="feed.id"
:html_url="feed.html_url"
:name="feed.name"
:body="feed.body"
:published_at="feed.published_at"
:embed_url="feed.embed_url"
/>
</div> </div>
</div> </div>
</div> </div>

1
packages/nc-gui/components/nc/Badge.vue

@ -25,6 +25,7 @@ const props = withDefaults(
'border-red-500 bg-red-100': props.color === 'red', 'border-red-500 bg-red-100': props.color === 'red',
'border-maroon-500 bg-maroon-50': props.color === 'maroon', 'border-maroon-500 bg-maroon-50': props.color === 'maroon',
'border-gray-500 bg-gray-50': props.color === 'grey', 'border-gray-500 bg-gray-50': props.color === 'grey',
'bg-brand-50 text-brand-500': props.color === 'brand',
'border-gray-300': !props.color, 'border-gray-300': !props.color,
'border-1': props.border, 'border-1': props.border,
'h-6': props.size === 'sm', 'h-6': props.size === 'sm',

100
packages/nc-gui/composables/useProductFeed.ts

@ -1,4 +1,5 @@
import axios from 'axios' import axios from 'axios'
import type { ProductFeedItem } from '../lib/types'
const axiosInstance = axios.create({ const axiosInstance = axios.create({
// baseURL: 'https://nocodb.com/api', // baseURL: 'https://nocodb.com/api',
@ -11,74 +12,43 @@ const axiosInstance = axios.create({
export const useProductFeed = createSharedComposable(() => { export const useProductFeed = createSharedComposable(() => {
const activeTab = ref('recents') const activeTab = ref('recents')
const youtubeFeed = ref< const youtubeFeed = ref<ProductFeedItem[]>([])
{
id: string
body: string
name: string
published_at: string
thumbnails: {
default: {
height: number
url: string
width: number
}
high: {
height: number
url: string
width: number
}
medium: {
height: number
url: string
width: number
}
}
embed_url: string
html_url: string
}[]
>([])
const githubFeed = ref<
{
body: string
html_url: string
id: string
name: string
published_at: string
}[]
>([])
const socialFeed = ref([]) const githubFeed = ref<ProductFeedItem[]>([])
const ytNextPageToken = ref('') const socialFeed = ref<ProductFeedItem[]>([])
const loadYoutubeFeed = async (loadMore?: boolean) => { const twitterFeed = ref<ProductFeedItem[]>([])
const { data } = await axiosInstance.get('/social/youtube', {
params: loadMore
? {
pageToken: ytNextPageToken.value,
per_page: 10,
}
: {
per_page: 10,
},
})
ytNextPageToken.value = data.nextPageToken
youtubeFeed.value = [...youtubeFeed.value, ...data.videos]
}
const loadGithubFeed = async (loadMore?: boolean) => { const loadFeed = async ({ loadMore, type }: { loadMore: boolean; type: 'youtube' | 'github' | 'all' | 'twitter' }) => {
const { data } = await axiosInstance.get('/social/github', { let page = 1
params: loadMore
? { if (loadMore) {
page: githubFeed.value.length / 10 + 1, switch (type) {
per_page: 10, case 'youtube':
} page = Math.ceil(youtubeFeed.value.length / 10) + 1
: { break
per_page: 10, case 'github':
}, page = Math.ceil(githubFeed.value.length / 10) + 1
break
case 'all':
page = Math.ceil(socialFeed.value.length / 10) + 1
break
case 'twitter':
page = Math.ceil(twitterFeed.value.length / 10) + 1
break
}
}
const response = await axiosInstance.get('/social/feed', {
params: {
per_page: 10,
page,
type,
},
}) })
githubFeed.value = [...githubFeed.value, ...data]
return response.data
} }
return { return {
@ -86,7 +56,7 @@ export const useProductFeed = createSharedComposable(() => {
youtubeFeed, youtubeFeed,
githubFeed, githubFeed,
socialFeed, socialFeed,
loadYoutubeFeed, twitterFeed,
loadGithubFeed, loadFeed,
} }
}) })

11
packages/nc-gui/lib/types.ts

@ -277,6 +277,16 @@ interface NcTableColumnProps {
[key: string]: any [key: string]: any
} }
interface ProductFeedItem {
Id: string
Title: string
Description: string
['Feed Source']: 'Youtube' | 'Github' | 'Twitter'
Url: string
Tags?: string
CreatedAt: string
}
type SordDirectionType = 'asc' | 'desc' | undefined type SordDirectionType = 'asc' | 'desc' | undefined
export type { export type {
@ -312,4 +322,5 @@ export type {
AuditLogsQuery, AuditLogsQuery,
NcTableColumnProps, NcTableColumnProps,
SordDirectionType, SordDirectionType,
ProductFeedItem,
} }

Loading…
Cancel
Save