Commit 533a9be1 authored by Christophe Benz's avatar Christophe Benz
Browse files

Implement discourse data provider

parent 3b171063
......@@ -12,13 +12,13 @@ pages:
zones:
- pageSlug: challenges
buttonTitle: See all challenges
limit: 4
limit: 8
- pageSlug: tools
buttonTitle: See all tools
limit: 5
limit: 4
- pageSlug: data
buttonTitle: See all data
limit: 6
limit: 8
- slug: challenges
appBarTitle: Challenges
title: Challenges of the hackathon
......@@ -30,6 +30,10 @@ pages:
- slug: tools
appBarTitle: Tools
title: Tools of the hackathon
source:
provider: discourse
baseUrl: https://forum.datafin.fr
category: defis
- slug: data
appBarTitle: Data
title: Data of the hackathon
{
"name": "TODO",
"version": "0.0.1",
"name": "vitrine",
"version": "0.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -844,6 +844,12 @@
"purgecss": "^1.4.0"
}
},
"@overleaf/o-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-2.1.0.tgz",
"integrity": "sha512-Zd9sks9LrLw8ErHt/cXeWIkyxWAqNAvNGn7wIjLQJH6TTEEW835PWOhpch+hQwwWsTxWIx/JDj+IpZ3ouw925g==",
"dev": true
},
"@polka/url": {
"version": "1.0.0-next.3",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.3.tgz",
......
......@@ -26,6 +26,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/runtime": "^7.0.0",
"@fullhuman/postcss-purgecss": "^1.3.0",
"@overleaf/o-error": "^2.1.0",
"autoprefixer": "^9.6.1",
"cssnano": "^4.1.10",
"eslint": "^6.5.1",
......@@ -48,4 +49,4 @@
"tailwindcss": "^1.1.2",
"tailwindcss-elevation": "^0.3.3"
}
}
\ No newline at end of file
}
......@@ -6,7 +6,7 @@
<div class="cards px-2">
<div class="flex flex-wrap -mx-2">
{#each data as { id, title, text, url, count, countIcon, imageUrl } (id)}
{#each data as { id, html, title, text, url, count, countIcon, imageUrl } (id)}
<div class="w-full mb-4 sm:w-1/3 sm:px-2 lg:w-1/4">
<Card.Card class="w-full sm:h-full">
<div slot="title">
......@@ -18,10 +18,12 @@
<Image class="w-full" src={imageUrl} alt="image" />
</div>
<div slot="text">
{#if text}
<div class="p-5 pb-0 pt-3 text-gray-700 body-2 break-all">
{text}
{#if html}
<div class="p-5 pb-0 pt-3 text-gray-700 body-2">
{@html html}
</div>
{:else if text}
<div class="p-5 pb-0 pt-3 text-gray-700 body-2">{text}</div>
{/if}
</div>
<div slot="actions" class="mt-auto">
......
<script context="module">
import { fetchZoneCards } from "./_fetchers.js"
import { fetchZoneCards, ConfigError, FetchError } from "./_fetchers.js"
export async function preload(page, session) {
const { slug } = page.params
......@@ -9,9 +9,15 @@
}
let cardsData
if (pageConfig.source) {
// Fetch zones cards data
// TODO Handle errors
cardsData = await fetchZoneCards.bind(this)(pageConfig.source)
try {
cardsData = await fetchZoneCards.bind(this)(pageConfig.source)
} catch (e) {
if (e instanceof ConfigError || e instanceof FetchError) {
console.error(e)
return this.error(500, e.message)
}
throw e
}
}
return { ...pageConfig, cardsData }
}
......
......@@ -9,10 +9,12 @@
<title>{status}</title>
</svelte:head>
<h1>{status}</h1>
<div class="container mb-10 mx-auto p-8">
<h1>{status}</h1>
<p>{error.message}</p>
<p>{error.message}</p>
{#if dev && error.stack}
<pre>{error.stack}</pre>
{/if}
{#if dev && error.stack}
<pre>{error.stack}</pre>
{/if}
</div>
export function fetchZoneCards(source) {
import OError from "@overleaf/o-error"
export class ConfigError extends OError {
constructor(options) {
super({ message: "Config error", ...options })
}
}
export class FetchError extends OError {
constructor(options) {
super({ message: "Fetch error", ...options })
}
}
export function discourseTopicToCardData(source, topic) {
return {
...topic,
count: topic.posts_count,
countIcon: "reply", // Cf https://material.io/resources/icons
imageUrl: topic.image_url,
html: topic.excerpt,
// title already exist in `topic`
url: `${source.baseUrl}/t/${source.topic}.json`
}
}
export function gitlabProjectToCardData(project) {
return {
...project,
count: project.star_count,
countIcon: "star", // Cf https://material.io/resources/icons
imageUrl: project.avatar_url,
text: project.description,
title: project.name,
url: project.web_url,
}
}
export async function fetchZoneCards(source, zone = {}) {
let results
switch (source.provider) {
case "gitlab":
return fetchGitLabGroupProjects.bind(this)(source)
case "discourse":
if (source.category) {
const topicsResults = await fetchDiscourseCategoryTopics.bind(this)(source, zone)
if (topicsResults && topicsResults.topic_list && topicsResults.topic_list.topics) {
results = topicsResults.topic_list.topics.map(topic => discourseTopicToCardData(source, topic))
}
} else if (source.topic) {
results = discourseTopicToCardData(
source,
await fetchDiscourseTopic.bind(this)(source)
)
} else {
throw new ConfigError({
message: `Missing "category" or "topic" property in ${source.provider} not supported`,
info: { source, zone }
})
}
break
case "gitlab": {
const projectsResults = await fetchGitLabGroupProjects.bind(this)(source, zone)
results = projectsResults.map(gitlabProjectToCardData)
break
}
default:
throw new Error(`Provider ${source.provider} not supported`)
throw new ConfigError({
message: `Provider ${source.provider} not supported`,
info: { source, zone }
})
}
return results && results.sort((a, b) => (a.title.toLowerCase() > b.title.toLowerCase()) ? 1 : -1)
}
export async function fetchDiscourseCategoryTopics(source, zone) {
const { baseUrl, category } = source
const { limit = null } = zone
const url = `${baseUrl}/c/${category}.json`
let result = null
for (let page = 0; ; page++) {
const pageUrl = `${url}?page=${page}`
const info = { url, pageUrl, source, zone }
let response
try {
response = await this.fetch(pageUrl)
if (!response.ok) {
throw response
}
} catch (error) {
throw new FetchError({
message: `Error while fetching Discourse category "${category}"`,
info,
}).withCause(error)
}
let data
try {
data = await response.json()
} catch (error) {
throw new FetchError({
message: `Error while parsing JSON of Discourse category "${category}"`,
info,
}).withCause(error)
}
if (result === null) {
result = data
} else {
result.topic_list.topics = [
...result.topic_list.topics, // already fetched topics
...data.topic_list.topics // newly fetched topics
]
}
if (limit !== null && data.topic_list.topics.length >= limit) {
result.topic_list.topics = result.topic_list.topics.slice(0, limit)
return result
} else if (data.topic_list.topics.length < data.topic_list.per_page) {
return result
}
}
}
export async function fetchDiscourseTopic(source) {
const { baseUrl, topic, id } = source
const url = `${baseUrl}/t/${topic}/${id}.json`
const info = { url, source }
let response
try {
response = await this.fetch(url)
if (!response.ok) {
throw response
}
} catch (error) {
throw new FetchError({
message: `Error while fetching Discourse topic "${topic}/${id}"`,
info,
}).withCause(error)
}
try {
return await response.json()
} catch (error) {
throw new FetchError({
message: `Error while parsing JSON of Discourse topic "${topic}/${id}"`,
info,
}).withCause(error)
}
}
export async function fetchGitLabGroupProjects({ baseUrl, group }) {
const url = `${baseUrl}/api/v4/groups/${encodeURIComponent(group)}/projects`
let response = null
export async function fetchGitLabGroupProjects(source, zone) {
const { baseUrl, group } = source
const {
limit = -1, // means no limit in GitLab API
orderBy = "name"
} = zone
const url = `${baseUrl}/api/v4/groups/${encodeURIComponent(group)}/projects?per_page=${limit}&order_by=${orderBy}&sort=asc`
const info = { url, source }
let response
try {
response = await this.fetch(url)
if (!response.ok) {
throw response
}
} catch (error) {
throw new Error(`Error while fetching GitLab group at ${url}: ${error}`)
throw new FetchError({
message: `Error while fetching GitLab group "${group}"`,
info,
}).withCause(error)
}
let projects
try {
projects = await response.json()
} catch (error) {
throw new Error(`Error while converting GitLab group at ${url} to JSON: ${error}`)
throw new FetchError({
message: `Error while parsing JSON of GitLab group "${group}"`,
info,
}).withCause(error)
}
// Adapt properties names to what's expected in a Card.
return projects
.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1)
.map(project => ({
...project,
count: project.star_count,
countIcon: "star", // Cf https://material.io/resources/icons
imageUrl: project.avatar_url,
text: project.description,
title: project.name,
url: project.web_url,
}))
}
<script context="module">
export async function preload(page, session) {
const { config } = session
return { config }
}
</script>
<script>
import { stores } from "@sapper/app"
import {
......@@ -19,16 +12,16 @@
} from "smelte"
import { breakpoint } from "../stores.js"
const { preloading, page } = stores()
const { preloading, page, session } = stores()
export let segment
export let config
let showNav = false
let showNavMobile = false
const bp = breakpoint()
$: config = $session.config
$: path = $page.path
$: currentPageConfig = config.pages.find(page => path.includes(page.slug))
$: navMenu = config.pages.map(page => ({
......
<script context="module">
import { fetchZoneCards } from "./_fetchers.js"
import { fetchZoneCards, ConfigError, FetchError } from "./_fetchers.js"
async function loadZoneCardData(pages, zone) {
// Resolve pageSlug reference
const zonePageConfig = pages.find(page1 => page1.slug === zone.pageSlug)
let cardsData
const { source } = zonePageConfig
if (source) {
cardsData = await fetchZoneCards.bind(this)(source, zone)
}
return { ...zone, ...zonePageConfig, cardsData }
}
export async function preload(page, session) {
const pageConfig = session.config.pages.find(page1 => !page1.slug)
......@@ -7,21 +18,19 @@
return this.error(404, "Page not found")
}
if (pageConfig.zones) {
pageConfig.zones = await Promise.all(
pageConfig.zones.map(async zone => {
// Resolve pageSlug reference
const zonePageConfig = session.config.pages.find(
page1 => page1.slug === zone.pageSlug
try {
pageConfig.zonesCardData = await Promise.all(
pageConfig.zones.map(zone =>
loadZoneCardData.bind(this)(session.config.pages, zone)
)
let cardsData
if (zonePageConfig.source) {
// Fetch zones cards data
// TODO Handle errors
cardsData = await fetchZoneCards.bind(this)(zonePageConfig.source)
}
return { ...zone, ...zonePageConfig, cardsData }
})
)
)
} catch (e) {
if (e instanceof ConfigError || e instanceof FetchError) {
console.error(e)
return this.error(500, e.message)
}
throw e
}
}
return pageConfig
}
......@@ -39,7 +48,7 @@
export let callToActionButtonTitle
export let callToActionButtonURL
export let logoUrl
export let zones
export let zonesCardData
</script>
<style>
......@@ -77,7 +86,7 @@
<div class="container mb-10 mx-auto p-8">
{#each zones as { cardsData, pageSlug, buttonTitle, slug, title } (pageSlug)}
{#each zonesCardData as { cardsData, pageSlug, buttonTitle, slug, title } (pageSlug)}
<div class="zone my-16">
<h2 class="my-8">{title}</h2>
{#if cardsData && cardsData.length}
......
......@@ -20,7 +20,7 @@ if (process.env.VITRINE_BASE_PATH) {
config.basePath = config.basePath || "/"
// TODO Validate config
console.log("basePath", config.basePath)
console.log("config", JSON.stringify(config, null, 2))
polka() // You can also use Express
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment