web4, se guardo indietro vedo il futuro

This commit is contained in:
lesion 2022-01-21 11:15:39 +01:00
commit c69ba7e497
59 changed files with 12330 additions and 0 deletions

13
.editorconfig Normal file
View file

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

90
.gitignore vendored Normal file
View file

@ -0,0 +1,90 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp

69
README.md Normal file
View file

@ -0,0 +1,69 @@
# chew
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
## Special Directories
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
### `assets`
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
### `components`
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
### `layouts`
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
### `pages`
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
### `plugins`
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
### `static`
This directory contains your static files. Each file inside this directory is mapped to `/`.
Example: `/static/robots.txt` is mapped as `/robots.txt`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
### `store`
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).

4
assets/variables.scss Normal file
View file

@ -0,0 +1,4 @@
// Ref: https://github.com/nuxt-community/vuetify-module#customvariables
//
// The variables you want to modify
// $font-size-root: 20px;

58
components/AddCohort.vue Normal file
View file

@ -0,0 +1,58 @@
<template>
<article class='card inline-block'>
<input type="text" v-model='name' placeholder="Name">
<input type="text" v-model='description' placeholder="Description">
<ul>
<li class="p-2 bg-slate-700 " v-for='source, id in sources' :key="id">
<div class="font-bold text-xl">{{ source.name }}</div>
<span class="text-red-500">{{source.description}}</span>
</li>
</ul>
<button @click='addCohort' class="p-2 text-xl font-bold border-2 rounded mt-5">Create +</button>
</article>
</template>
<script>
export default {
props: {
modelValue: String
},
data: () => {
return {
name: '',
sources: '',
url: '',
error: '' }
},
methods: {
async addCohort () {
const cohort = await $fetch('/api/cohort', { method: 'POST', body: {
name: this.name,
sources: this.sources.map(s => s.id)
}})
this.$emit("addCohort", cohort)
},
async addSource() {
console.error('dentro add source', this.url, this.name)
this.error = ''
// invio un url al backend
// se e' un feed valido, lo aggiungo ai sources e all cohort appena creata
// se non e' valido provo a cercare il feed dentro quell'url
try {
const source = await $fetch('/api/source', { method: 'POST', body: { URL: this.url } })
this.sources.push( source )
// this.$emit('addCohort', { id: 1, title: this.title })
this.url = ''
} catch (e) {
this.error = String(e)
}
}
}
}
</script>
<style lang="scss" scoped>
.card {
@apply w-full rounded-md border-2 border-dashed p-2 mt-3
max-w-sm border-pink-500;
}
</style>

60
components/AddSource.vue Normal file
View file

@ -0,0 +1,60 @@
<template>
<article class='card inline-block'>
<h3 class='text-xl font-bold'>Add source</h3>
<span>Aggiungi una sorgente, un sito, un feed rss/atom/json, un account mastodon, twitter, facebook</span>
<p>{{search}} {{items.length}}</p>
<input type="text" v-model='url' placeholder="Feed URL" @keydown.enter="addSource">
<span v-if='error' class="text-red-500">{{error}}</span>
<span v-else class="font-light">Add your feed</span>
<!-- <ul>
<li class="p-2 bg-slate-700 " v-for='source, id in sources' :key="id">
<div class="font-bold text-xl">{{ source.name }}</div>
<span class="text-red-500">{{source.description}}</span>
</li>
</ul> -->
<button @click='addCohort' class="p-2 text-xl font-bold border-2 rounded mt-5">Create +</button>
</article>
</template>
<script>
export default {
// emits: ['update:modelValue', 'addSource'],
// async function runSearch() {
// console.error('sono dentro run search', search.value)
// items.value = await $fetch('/api/source/search', { params: { search: search.value }})
// console.error(items)
// }
// let items = reactive([])
// return { source, url, error, search, items }
// },
// asyncComputed: {
// async items () {
// console.error('dentro items vado di search', this.search)
// return await $fetch('/api/source/search', { search: this.search })
// }
// },
methods: {
async addSource() {
console.error('dentro add source', this.url, this.name)
this.error = ''
// invio un url al backend
// se e' un feed valido, lo aggiungo ai sources e all cohort appena creata
// se non e' valido provo a cercare il feed dentro quell'url
try {
const source = await $fetch('/api/source', { method: 'POST', body: { URL: this.url } })
this.source.push( source )
// this.$emit('addCohort', { id: 1, title: this.title })
this.url = ''
} catch (e) {
this.error = String(e)
}
}
}
}
</script>
<style lang="scss" scoped>
.card {
@apply w-full rounded-md border-2 border-dashed p-2 mt-3
max-w-sm border-pink-500;
}
</style>

59
components/Cohort.vue Normal file
View file

@ -0,0 +1,59 @@
<template>
<v-card>
<v-card-title>{{cohort.name}}</v-card-title>
<v-card-subtitle>{{cohort.description}}</v-card-subtitle>
<v-card-text>
<span>{{cohort}} {{posts}}</span>
</v-card-text>
</v-card>
<!-- <h3 class="font-bold text-2xl text-gray-600 bg-gray-100 p-2">{{cohort.name}}</h3>
<span class="float-right text-xs">{{cohort.dailyView}} <RssIcon class='h-8'/></span>
<ul v-if='withContent'>
<a v-for='post in posts' :key="post.URL" target="_blank" :href='post.URL' class="hover:bg-zinc-200 block pt-2 px-2">
<div class="font-bold text-black" v-html='post.title'></div>
<span class="font-base text-xs">
{{new Date(post.date).toLocaleString()}}
<b>{{post.source.name}}</b>
</span>
<span v-html='post.summary'></span>
</a>
</ul>
</div> -->
</template>
<script>
// import { RssIcon } from '@heroicons/vue/solid'
export default {
// components: { RssIcon },
data: () => ({
posts: []
}),
props: {
cohort: {
type: Object,
required: true
},
withContent: {
type: Boolean,
default: false
},
maxPosts: {
type: Number,
default: 10
}
},
async fetch() {
console.error('dentro fetch')
console.error(this.cohort)
this.posts = await this.$http.$get(`http://localhost:3000/api/cohort/${this.cohort.id}`,
{ params: { maxPosts: this.maxPosts }})
// console.error(res)
// .then(res => res.json())
},
}
</script>
<style lang="scss" scoped>
.card {
@apply w-full rounded-md mt-3 inline-block mr-1
border-pink-500;
}
</style>

50
components/Post.vue Normal file
View file

@ -0,0 +1,50 @@
<template>
<a class='post' :href='post.URL' target='_blank'>
<v-row class='mb-5'>
<v-col cols=12 lg=4>
<v-img class='rounded-lg' max-height="250" :src="post.image" alt=""></v-img>
</v-col>
<v-col cols=12 lg=8>
<span>{{post.source.name}}</span>
<h1 class='text-h5 font-weight-bold'>{{post.title}}</h1>
<p class='font-weight-light'>{{new Date(post.date).toLocaleString('it-IT', { weekday: 'long', month: 'long', day: 'numeric'})}}</p>
</v-col>
</v-row>
</a>
<!-- <div class='post p-2 mb-10 bg-white'>
<div role='title' class="block pt-2 mb-3 border-b-2" >
<a target="_blank" :href='post.URL' v-html='post.title' class='text-xl font-bold text-violet-500'></a>
<span class="font-base text-base float-right">{{new Date(post.date).toLocaleString()}} - <b><a :href='post.source.link'>{{post.source.name}} <img class="inline h-5 w-5 rounded-full" :src="post.source.image || post.source.link + 'favicon.ico'" alt="" /></a></b></span>
</div>
<span class='text-xl' v-html='post.summary || post.content'></span>
</div> -->
</template>
<script>
export default {
props: {
post: Object
},
// async setup(props) {
// const res = await fetch(`http://localhost:3000/api/cohort/${props.cohort.id}`).then(res => res.json())
// return { cohort: res.cohort, posts: res.posts }
// },
}
</script>
<style>
.post img {
max-width: 500px;
max-height: 400px;
}
a.post{
color: black;
text-decoration: none;
}
a.post:hover {
background-color: #fff;
}
</style>

37
components/TextCohort.vue Normal file
View file

@ -0,0 +1,37 @@
<template>
<div v-if='posts'>
<h3 class="font-extrabold text-3xl text-pink-500 p-2 mt-10 rounded border-2 border-pink-500">{{cohort.name}}</h3>
<div v-for='post in posts' :key="post.URL" class='p-2 mb-10 bg-white'>
<div role='title' class="block pt-2 mb-3 border-b-2" >
<a target="_blank" :href='post.URL' v-html='post.title' class='text-xl font-bold text-violet-500'></a>
<span class="font-base text-base float-right">{{new Date(post.date).toLocaleString()}} - <b><a :href='post.source.link'>{{post.source.name}} <img class="inline h-5 w-5 rounded-full" :src="post.source.image || post.source.link + 'favicon.ico'" alt="" /></a></b></span>
</div>
<span class='text-xl' v-html='post.summary'></span>
</div>
</div>
</template>
<script>
export default {
props: {
cohort: Object
},
async fetch(props) {
const res = await fetch(`http://localhost:3000/api/cohort/${props.cohort.id}`).then(res => res.json())
return { cohort: res.cohort, posts: res.posts }
},
}
</script>
<style lang="scss" scoped>
.card {
@apply w-full rounded-md border-2 bg-stone-800 mt-3 inline-block
max-w-sm border-pink-500;
.title {
@apply text-lime-300 p-0 m-2;
}
}
</style>

View file

@ -0,0 +1,60 @@
<template>
<div class="rounded overflow-hidden shadow-lg">
<div class="px-6 py-4">
<div class="font-bold text-xl mb-2">{{cohort.name}}</div>
<p class="text-gray-700 text-base">{{cohort.description}}</p>
</div>
<ul class="px-6 py-4 bg-grey-300 text-pink-600 p-1">
<li class="font-monospace">Last update</li>
<li class="font-monospace">Views</li>
</ul>
</div>
<!-- <h3 class="font-bold text-2xl text-gray-600 bg-gray-100 p-2">{{cohort.name}}</h3>
<span class="float-right text-xs">{{cohort.dailyView}} <RssIcon class='h-8'/></span>
<ul v-if='withContent'>
<a v-for='post in posts' :key="post.URL" target="_blank" :href='post.URL' class="hover:bg-zinc-200 block pt-2 px-2">
<div class="font-bold text-black" v-html='post.title'></div>
<span class="font-base text-xs">
{{new Date(post.date).toLocaleString()}}
<b>{{post.source.name}}</b>
</span>
<span v-html='post.summary'></span>
</a>
</ul>
</div> -->
</template>
<script>
// import { RssIcon } from '@heroicons/vue/solid'
export default ({
// components: { RssIcon },
props: {
cohort: {
type: Object,
required: true
},
withContent: {
type: Boolean,
default: false
},
maxPosts: {
type: Number,
default: 10
}
},
data: () => ({
// cohort:
}),
async fetch () {
const res = await $fetch(`http://localhost:3000/api/cohort/${props.cohort.id}`,
{ params: { maxPosts: props.maxPosts }})
// .then(res => res.json())
return res
}
})
</script>
<style lang="scss" scoped>
.card {
@apply w-full rounded-md mt-3 inline-block mr-1
border-pink-500;
}
</style>

21
layouts/default.vue Normal file
View file

@ -0,0 +1,21 @@
<template>
<v-app>
<v-main light>
<v-tabs centered>
<v-tab to='/'>Home</v-tab>
<v-tab to='/groups'>Groups</v-tab>
<v-tab to='/add'>Add</v-tab>
<v-tab to='/embed'>Widget</v-tab>
</v-tabs>
<v-container>
<Nuxt />
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
name: 'DefaultLayout'
}
</script>

45
layouts/error.vue Normal file
View file

@ -0,0 +1,45 @@
<template>
<v-app dark>
<h1 v-if="error.statusCode === 404">
{{ pageNotFound }}
</h1>
<h1 v-else>
{{ otherError }}
</h1>
<NuxtLink to="/">
Home page
</NuxtLink>
</v-app>
</template>
<script>
export default {
name: 'EmptyLayout',
layout: 'empty',
props: {
error: {
type: Object,
default: null
}
},
data () {
return {
pageNotFound: '404 Not Found',
otherError: 'An error occurred'
}
},
head () {
const title =
this.error.statusCode === 404 ? this.pageNotFound : this.otherError
return {
title
}
}
}
</script>
<style scoped>
h1 {
font-size: 20px;
}
</style>

60
lit/display-feed.js Normal file
View file

@ -0,0 +1,60 @@
import { LitElement, html } from 'lit'
import { until } from 'lit/directives/until.js'
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
export class DisplayFeed extends LitElement {
static get properties () {
return {
max: { type: Boolean },
posts: { type: Array, state: true },
feed: { type: String },
title: { type: String }
}
}
constructor () {
super()
this.title = 'Display Feed'
}
_post (post) {
return html`<div class="df-item"><h3 class="df-title">${post.title}</h3><div class="df-content">${unsafeHTML(post.content)}</div></div>`
}
// connectedCallback () {
// super.connectedCallback()
// console.error
// this.posts = fetch(this.feed)
// .then(async res => {
// const posts = await res.json()
// return posts.posts.map(this._post)
// })
// }
updated (changedProperties) {
console.error('dentro changed ', changedProperties)
console.error(this.feed)
console.error(changedProperties)
if (changedProperties.has('feed')) {
console.error('feed cambiato')
this.posts = fetch(this.feed)
.then(async res => {
const posts = await res.json()
return posts.posts.map(this._post)
})
}
}
render() {
// const loading = html`<span>Loading...</span>`
// const title = html`<h2>${this.title}</h2>`
return html`<h2>${this.title}</h2>${until(this.posts, 'Loading...')}`
}
// do not create a shadowDOM (we want style pollution from outside)
createRenderRoot() { return this }
}
customElements.define('display-feed', DisplayFeed)

13
lit/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="module" src="./dist/main.js"></script>
</head>
<body>
<display-feed feed='http://localhost:3000/api/source/33'></display-feed>
</body>
</html>

1
lit/node_modules Symbolic link
View file

@ -0,0 +1 @@
../node_modules/

95
nuxt.config.js Normal file
View file

@ -0,0 +1,95 @@
import colors from 'vuetify/es5/util/colors'
export default {
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
titleTemplate: '%s - chew',
title: 'chew',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' }
],
script: [{ src: '/display-feed.js' }],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
],
render: {
static: {
// Add CORS header to static files.
setHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
},
},
},
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/vuetify
'@nuxtjs/vuetify',
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
'@nuxt/http',
'@/server/initialize.server.js'
],
serverMiddleware: ['server/index'],
vue: {
config: {
ignoredElements: ['display-feed']
}
},
// Vuetify module configuration: https://go.nuxtjs.dev/config-vuetify
vuetify: {
customVariables: ['~/assets/variables.scss'],
theme: {
dark: false,
themes: {
dark: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
}
}
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
build: {
corejs: 3,
cache: true,
hardSource: true
}
}
}

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "chew",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
},
"dependencies": {
"@nuxt/http": "^0.6.4",
"@prisma/client": "^3.8.1",
"body-parser": "^1.19.1",
"bull": "^4.2.1",
"core-js": "^3.20.3",
"cors": "^2.8.5",
"dompurify": "^2.3.4",
"express": "^4.17.2",
"feed": "^4.2.2",
"feedparser": "^2.2.10",
"jsdom": "^19.0.0",
"linkedom": "^0.13.0",
"lit": "^2.1.1",
"nuxt": "^2.15.8",
"vue": "^2.6.14",
"vue-server-renderer": "^2.6.14",
"vue-template-compiler": "^2.6.14",
"vuetify": "^2.6.1",
"webpack": "^4.46.0"
},
"devDependencies": {
"@nuxtjs/vuetify": "^1.12.3",
"prisma": "^3.8.1",
"webpack-cli": "^4.9.1"
}
}

93
pages/add.vue Normal file
View file

@ -0,0 +1,93 @@
<template>
<section>
<v-data-table dense
:items="sources"
:headers="headers">
<template v-slot:item.updatedAt="{ item }">
<span>{{new Date(item.updatedAt).toLocaleString()}}</span>
</template>
<template v-slot:item.actions="{ item }">
<v-btn target="_blank" :href='`/api/source/${item.id}.rss`' small text color='orange' label='rss feed'><v-icon>mdi-rss</v-icon> Rss</v-btn>
<v-btn @click='copy($event, item)' small text color='primary' label='embedd'><v-icon>mdi-application-brackets-outline</v-icon> Embed</v-btn>
</template>
</v-data-table>
<p class='text-xl'>Add a website or a feed rss/atom/jsonfeed</p>
<v-text-field v-model='url' outlined label='Add URL' required></v-text-field>
<v-btn @click='addSource' :loading='loading' :error-messages='error' :disabled='loading'>Add</v-btn>
</section>
</template>
<script>
export default {
data: () => ({
url: '',
error: '',
loading: false,
sources: [],
headers: [
{
text: 'Actions',
value: 'actions'
},
{
text: 'Name',
value: 'name',
},
{
text: 'Description',
value: 'description'
},
{
text: 'Updated',
value: 'updatedAt'
},
]
}),
async fetch () {
this.sources = await this.$http.$get('/api/source')
},
methods: {
copy (ev, item) {
console.error('dentro copy')
const str = `<display-feed feed='http://localhost:3000/api/source/${item.id}'></display-feed>`
try {
navigator.clipboard.writeText(str)
} catch (e) {
const el = document.createElement('textarea')
el.addEventListener('focusin', e => e.stopPropagation())
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
},
async addSource() {
this.loading = true
this.error = false
// add http if not specified
this.url = this.url.match(/^https?:\/\//) ? this.url : 'http://' + this.url
console.error(this.url)
this.error = ''
// invio un url al backend
// se e' un feed valido, lo aggiungo ai sources e all cohort appena creata
// se non e' valido provo a cercare il feed dentro quell'url
try {
const source = await this.$http.$post('/api/source', { URL: this.url })
this.sources.unshift( source )
// this.$emit('addCohort', { id: 1, title: this.title })
// this.url = ''
} catch (e) {
this.error = String(e)
console.error(this.error)
}
this.loading = false
}
}
}
</script>
<style lang="scss" scoped>
.card {
@apply w-full rounded-md border-2 border-dashed p-2 mt-3
max-w-sm border-pink-500;
}
</style>

70
pages/embed.vue Normal file
View file

@ -0,0 +1,70 @@
<template>
<section>
<v-card>
<v-card-title>Embed to your website</v-card-title>
<v-card-text>
<v-autocomplete
v-model='source'
:search-input.sync="search"
label='Source'
hide-no-data
item-value="id"
item-text="name"
:items="sources"
:disabled='loading'
:loading='loading'
prepend-icon="mdi-magnify"
placeholder="Start typing to search for a source to add"
clearable
return-object
no-filter>
<template v-slot:item="{ item }">
<v-list-item-content three-line>
<v-list-item-title>{{item.name}}</v-list-item-title>
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
<v-list-item-subtitle>Last update: {{ item.updatedAt }} / Tags: / Link: {{item.link}}</v-list-item-subtitle>
</v-list-item-content>
</template>
</v-autocomplete>
<v-alert class='white--text blue-grey darken-2' v-text='code'></v-alert>
<display-feed :feed="feed"></display-feed>
</v-card-text>
</v-card>
</section>
</template>
<script>
export default {
data: () => ({
source: {},
loading: false,
search: '',
sources: [],
cohorts: []
}),
watch: {
async search (value) {
if (!value) return
this.makeSearch()
}
},
computed: {
code () {
return `<display-feed feed="http://localhost:3000/api/source/${this.source && this.source.id}"></display-feed>`
},
feed () {
if (this.source && this.source.id) {
return `http://localhost:3000/api/source/${this.source.id}`
} else {
return ''
}
}
},
methods: {
async makeSearch () {
this.loading = true
this.sources = await this.$http.$get(`/api/source/search?search=${encodeURIComponent(this.search)}`)
this.loading = false
},
}
}
</script>

54
pages/group/_id.vue Normal file
View file

@ -0,0 +1,54 @@
<template>
<section class='mt-4'>
<!-- <h3 class='text-3xl font-extrabold'>{{cohort.name}}</h3> -->
<div class="font-bold text-3xl">{{cohort.name}}</div>
<div class='font-light mb-2'>{{cohort.dailyView}} views/day, last update: {{posts[0].updatedAt}}</div>
<button class='p-2 rounded bg-orange-100 hover:bg-orange-200 mr-1 shadow'>Feed <RssIcon class='fill-orange-600 inline h-5'/></button>
<button class='p-2 rounded bg-orange-100 hover:bg-orange-200 mr-1 shadow'>Embedd <ShareIcon class='inline h-5'/></button>
<p class="text-gray-700 text-base">{{cohort.description}}</p>
<ul>
<Post v-for='post in posts' :key='post.id' :post='post'/>
<!-- <a v-for='post in posts' :key="post.URL" target="_blank" :href='post.URL' class="hover:bg-zinc-200 block pt-2 px-2">
<div class="font-bold text-black" v-html='post.title'></div>
<span class="font-base text-xs">
{{new Date(post.date).toLocaleString()}}
<b>{{post.source.name}}</b>
</span>
<span v-html='post.summary'></span>
</a> -->
</ul>
</section>
</template>
<script>
export default {
}
// import { RssIcon, ShareIcon } from '@heroicons/vue/solid'
// const route = useRoute()
// const cohort = await $fetch('/api/cohort/' + route.params.id)
// const { cohort, posts } = await $fetch('http://localhost:3000/api/cohort/' + route.params.id)
// console.error(cohort)
// import Cohort from '@/components/Cohort.vue'
// export default {
// components: { Cohort },
// async setup () {
// }
// }
// data: () => ({ cohorts: [] }),
// async setup() {
// // const lastPosts = await fetch('http://localhost:3000/api/posts').then(res => res.json())
// const cohorts = await fetch('http://localhost:3000/api/cohorts').then(res => res.json())
// console.error(cohorts)
// return { cohorts }
// },
// methods: {
// addCohort (cohort) {
// console.error('dentro add cohort', cohort)
// this.cohorts.push(cohort)
// }
// }
// })
</script>

136
pages/groups.vue Normal file
View file

@ -0,0 +1,136 @@
<template>
<section class='mt-4'>
<v-data-table dense
:items="cohorts"
:headers="headers">
<template v-slot:item.actions="{ item }">
<v-btn target="_blank" :href='`/api/cohort/${item.id}.rss`' small text color='orange' label='rss feed'><v-icon>mdi-rss</v-icon> Rss</v-btn>
<v-btn @click='copy($event, item)' small text color='primary' label='embedd'><v-icon>mdi-application-brackets-outline</v-icon> Embed</v-btn>
</template>
</v-data-table>
<v-card>
<v-card-title>Add your group</v-card-title>
<v-card-subtitle>Search for a source to add</v-card-subtitle>
<v-card-text>
<v-text-field label='Name' v-model='cohort.name'></v-text-field>
<v-text-field label='Description' v-model='cohort.description'></v-text-field>
<v-autocomplete
v-model='source'
:search-input.sync="search"
label='Source'
hide-no-data
item-value="id"
item-text="name"
:items="sources"
:disabled='loading'
:loading='loading'
prepend-icon="mdi-magnify"
placeholder="Start typing to search for a source to add"
clearable
return-object
no-filter>
<template v-slot:append-outer>
<v-btn text link :disabled='!source' @click='addSource'>Add this source</v-btn>
</template>
<template v-slot:item="{ item }">
<v-list-item-content three-line>
<v-list-item-title>{{item.name}}</v-list-item-title>
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
<v-list-item-subtitle>Last update: {{ item.updatedAt }} / Tags: / Link: {{item.link}}</v-list-item-subtitle>
</v-list-item-content>
</template>
</v-autocomplete>
<v-btn @click='addCohort' text color='primary'>Create group</v-btn>
<v-list>
<v-list-item v-for='source in cohortSources' :key='source.id'>
<v-list-item-content>
<v-list-item-title>{{source.name}} <small>{{source.link}}</small></v-list-item-title>
<v-list-item-subtitle>{{source.description}}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<!-- <section class="flex place-content-center gap-5">
<add-cohort></add-cohort>
</section> -->
<!-- <v-row>
<v-col>
<nuxt-link :to='`/group/${cohort.id}`' v-for='cohort in cohorts' :key='cohort.id'>
<cohort :cohort='cohort' :max-posts='5'/>
</nuxt-link>
</v-col>
</v-row> -->
</section>
</template>
<script>
// import AddCohort from '@/components/AddCohort.vue'
// import debounce from 'lodash/debounce'
import Cohort from '@/components/Cohort.vue'
// import Post from '@/components/Post.vue'
// import TextCohort from '@/components/TextCohort.vue'
export default {
components: { Cohort },
data: () => ({
cohort: {},
loading: true,
source: null,
search: '',
cohorts: [],
cohortSources: [],
sources: [],
loading: false,
headers: [
{ text: 'Actions', value: 'actions' },
{ text: 'Name', value: 'name' },
{ text: 'Description', value: 'description' },
{ text: 'Updated', value: 'updatedAt' },
{ text: 'Sources', value: 'sources' },
]
}),
async fetch() {
// const lastPosts = await fetch('http://localhost:3000/api/posts').then(res => res.json())
this.cohorts = await this.$http.$get('http://localhost:3000/api/cohorts')
},
watch: {
async search (value) {
if (!value) return
this.makeSearch()
}
},
methods: {
async makeSearch () {
this.loading = true
this.sources = await this.$http.$get(`/api/source/search?search=${encodeURIComponent(this.search)}`)
this.loading = false
},
async addCohort () {
const cohort = await this.$http.$post('/api/cohort', { ...this.cohort, sources: this.cohortSources.map(s => s.id) })
this.cohorts.unshift(cohort)
this.cohort = {}
},
async addSource () {
this.cohortSources.unshift(this.source)
this.source = null
this.sources = []
},
copy (ev, item) {
console.error('dentro copy')
const str = `<display-feed feed='http://localhost:3000/api/cohort/${item.id}'></display-feed>`
try {
navigator.clipboard.writeText(str)
} catch (e) {
const el = document.createElement('textarea')
el.addEventListener('focusin', e => e.stopPropagation())
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
},
}
}
</script>

41
pages/index.vue Normal file
View file

@ -0,0 +1,41 @@
<template>
<section>
<h3 class="font-weight-bold text-h4 p-2 mt-10 rounded border-2 border-pink-500">Latest posts</h3>
<v-container class='px-6 mx-6 max-w-80'>
<Post v-for='post in lastPosts' :key='post.URL' :post='post'/>
</v-container>
<!-- //- TextCohort.mt-1(v-for='cohort in cohorts' :key='cohort.id' :cohort='cohort')
//- li(v-for='post, id in lastPosts' :key='id')
//- .title {{post.title}}
//- <div>
//- <p></p>
//- <ul v-for='post, id in posts' :key='id'>
//- <li>
//- <span>{{post.title}} {{post.date}} {{post.url}}</span>
//- <p v-html='post.content' />
//- </li>
//- </ul>
//- </div> -->
</section>
</template>
<script>
// import AddCohort from '@/components/AddCohort.vue'
// import Cohort from '@/components/Cohort.vue'
// import TextCohort from '@/components/TextCohort.vue'
export default {
// components: { AddCohort, Cohort, TextCohort, Post },
data: () => ({ lastPosts: [], cohorts: [] }),
async fetch() {
this.lastPosts = await this.$http.$get('http://localhost:3000/api/posts')
// this.cohorts = await this.$http.$get('http://localhost:3000/api/cohorts')
// const cohorts = []
},
methods: {
addCohort (cohort) {
console.error('dentro add cohort', cohort)
this.cohorts.push(cohort)
}
}
}
</script>

View file

@ -0,0 +1,82 @@
-- CreateTable
CREATE TABLE `Cohort` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Post` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`date` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`title` VARCHAR(191) NOT NULL,
`content` TEXT NOT NULL,
`summary` VARCHAR(191) NOT NULL DEFAULT '',
`sourceId` INTEGER NOT NULL,
`URL` VARCHAR(191) NOT NULL,
UNIQUE INDEX `Post_URL_key`(`URL`),
INDEX `Post_sourceId_fkey`(`sourceId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Source` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`type` ENUM('FEED') NOT NULL DEFAULT 'FEED',
`URL` VARCHAR(191) NOT NULL,
`updatedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`lastError` VARCHAR(191) NULL,
`status` ENUM('OK', 'WARNING', 'ERROR') NOT NULL DEFAULT 'OK',
`nErrors` INTEGER NOT NULL DEFAULT 0,
`description` VARCHAR(191) NULL,
`image` LONGTEXT NULL,
`link` VARCHAR(191) NULL,
UNIQUE INDEX `Source_URL_key`(`URL`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Tag` (
`name` VARCHAR(191) NOT NULL,
PRIMARY KEY (`name`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `_CohortToSource` (
`A` INTEGER NOT NULL,
`B` INTEGER NOT NULL,
UNIQUE INDEX `_CohortToSource_AB_unique`(`A`, `B`),
INDEX `_CohortToSource_B_index`(`B`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `_PostToTag` (
`A` INTEGER NOT NULL,
`B` VARCHAR(191) NOT NULL,
UNIQUE INDEX `_PostToTag_AB_unique`(`A`, `B`),
INDEX `_PostToTag_B_index`(`B`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Post` ADD CONSTRAINT `Post_sourceId_fkey` FOREIGN KEY (`sourceId`) REFERENCES `Source`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_CohortToSource` ADD FOREIGN KEY (`A`) REFERENCES `Cohort`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_CohortToSource` ADD FOREIGN KEY (`B`) REFERENCES `Source`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_PostToTag` ADD FOREIGN KEY (`A`) REFERENCES `Post`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_PostToTag` ADD FOREIGN KEY (`B`) REFERENCES `Tag`(`name`) ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,11 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `Cohort` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE `Cohort` ADD COLUMN `dailyView` INTEGER NOT NULL DEFAULT 0;
-- CreateIndex
CREATE UNIQUE INDEX `Cohort_name_key` ON `Cohort`(`name`);

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Post` MODIFY `summary` TEXT NOT NULL;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE `Post` MODIFY `updatedAt` DATETIME(3) NULL,
MODIFY `summary` TEXT NULL;

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `Post` ADD COLUMN `pippo` VARCHAR(191) NOT NULL DEFAULT '';
-- AlterTable
ALTER TABLE `Source` MODIFY `updatedAt` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP(3);

View file

@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `pippo` on the `Post` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE `Post` DROP COLUMN `pippo`,
MODIFY `content` TEXT NULL;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Cohort` ADD COLUMN `description` VARCHAR(191) NULL;

View file

@ -0,0 +1,17 @@
-- AlterTable
ALTER TABLE `Post` ADD COLUMN `image` VARCHAR(191) NULL;
-- CreateTable
CREATE TABLE `_CohortToTag` (
`A` INTEGER NOT NULL,
`B` VARCHAR(191) NOT NULL,
UNIQUE INDEX `_CohortToTag_AB_unique`(`A`, `B`),
INDEX `_CohortToTag_B_index`(`B`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `_CohortToTag` ADD FOREIGN KEY (`A`) REFERENCES `Cohort`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_CohortToTag` ADD FOREIGN KEY (`B`) REFERENCES `Tag`(`name`) ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"

62
prisma/schema.bk Normal file
View file

@ -0,0 +1,62 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = "mysql://root:diocane@localhost:3306/chewer"
}
// A source to get information from
model Source {
id Int @id @default(autoincrement())
name String
description String?
image Json?
type SourceType @default(FEED)
link String?
URL String @unique
status Status @default(OK)
nErrors Int @default(0)
lastError String?
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
cohorts Cohort[]
posts Post[]
}
model Tag {
name String @id
posts Post[]
}
// generic post
model Post {
id Int @id @default(autoincrement())
date DateTime @default(now())
updatedAt DateTime @updatedAt
title String
URL String @unique
content String @db.Text
summary String @default("")
source Source @relation(fields: [sourceId], references: [id])
tags Tag[]
sourceId Int
}
model Cohort {
id Int @id @default(autoincrement())
name String @unique
sources Source[]
dailyViews Int? @default(0)
}
enum SourceType {
FEED
}
enum Status {
OK
WARNING
ERROR
}

66
prisma/schema.prisma Normal file
View file

@ -0,0 +1,66 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = "mysql://root:diocane@localhost:3306/chewer"
}
model Cohort {
id Int @id @default(autoincrement())
name String @unique
dailyView Int @default(0)
description String?
sources Source[]
tags Tag[]
}
model Post {
id Int @id @default(autoincrement())
date DateTime @default(now())
updatedAt DateTime? @updatedAt
title String
content String? @db.Text
summary String? @db.Text
sourceId Int
URL String @unique
image String?
source Source @relation(fields: [sourceId], references: [id])
tags Tag[]
@@index([sourceId], map: "Post_sourceId_fkey")
}
model Source {
id Int @id @default(autoincrement())
name String
type Source_type @default(FEED)
URL String @unique
updatedAt DateTime? @default(now())
createdAt DateTime @default(now())
lastError String?
status Source_status @default(OK)
nErrors Int @default(0)
description String?
image String? @db.LongText
link String?
posts Post[]
cohorts Cohort[]
}
model Tag {
name String @id
cohorts Cohort[]
posts Post[]
}
enum Source_type {
FEED
}
enum Source_status {
OK
WARNING
ERROR
}

152
server/chew.js Normal file
View file

@ -0,0 +1,152 @@
// import p from '@prisma/client'
// const { PrismaClient } = p
import fetch from 'node-fetch'
import FeedParser from 'feedparser'
// const prisma = new PrismaClient()
import { getParams, maybeTranslate, parseContent } from './helper.js'
// let fetch
// import('node-fetch').then(f => {
// fetch = f
// })
// import fetch from 'node-fetch'
// const FeedParser = require('feedparser')
// // get('https://cavallette.noblogs.org/feed/atom')
const manager = {
/**
* check if post is new, updated or unknown
* @param {Post} post
*/
async isPostNew (post) {
// console.error(post.link)
const ret = await prisma.post.findUnique({ where: { URL: post.link } })
return !ret
},
isValid (post) {
return true
},
async sourceError (err, source) {
try {
await prisma.source.update({ where: { id: source.id }, data: { status: 'WARNING', lastError: String(err) } })
} catch (e) {
console.error(source, e)
}
},
async sourceCompleted (source) {
try {
await prisma.source.update({ where: { id: source.id }, data: { status: 'OK', lastError: null }})
} catch(e) {
console.error(source, e)
}
},
async get (source) {
console.error('dentro get!')
try {
// Get a response stream
const res = await fetch(source.URL,
{
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36',
'accept': 'text/html,application/xhtml+xml'
})
// Setup feedparser stream
const feedparser = new FeedParser()
feedparser.on('error', e => manager.sourceError(e, source))
feedparser.on('end', e => manager.sourceCompleted(source))
feedparser.on('readable', async () => {
let post
while(post = feedparser.read()) {
// validate post
if (!manager.isValid(post)) return
// check if already exist and is not updated
if (!await manager.isPostNew(post)) return
try {
// dompurify
let { html, image } = parseContent(post.description || post.summary)
// console.error(post.enclosures)
const enclosuresImages = post.enclosures.filter(e => e.type.includes('image'))
image = enclosuresImages.length ? enclosuresImages[0].url : image
await prisma.post.create({ include: { tags: true }, data: {
date: post.pubdate,
title: post.title,
URL: post.link,
content: html,
image,
summary: post.summary,
sourceId: source.id,
// tags: {
// connect: post.categories.map(name => ({ name })),
// create: post.categories.map(name => ({ name }))
// // create: { name },
// // where: { name }
// // }))
// // // create: post.categories.map( name => ({ name }))
// }
}})
} catch (e) { console.error(e) }
// console.log(post.title)
// console.log(JSON.stringify(post, ' ', 4))
}
})
// Handle our response and pipe it to feedparser
if (res.status !== 200) throw new Error('Bad status code')
const charset = getParams(res.headers.get('content-type') || '').charset
let responseStream = res.body
responseStream = maybeTranslate(responseStream, charset)
// And boom goes the dynamite
responseStream.pipe(feedparser)
// res.body.pipe(feedparser)
} catch (e) {
// console.error('sono qui', e)
manager.sourceError(e, source)
}
}
}
// const Queue = require('bull');
// import pkg from 'bullmq'
import Queue from 'bull'
import { PrismaClient } from '@prisma/client'
// import fetch from 'node-fetch'
// import FeedParser from 'feedparser'
const prisma = new PrismaClient()
// import { getParams, maybeTranslate } from './helpers.mjs'
let queue
export async function add (s) {
queue.add(s, { jobId: s.id, repeat: { every: 10000 } })
}
async function main () {
queue = new Queue('foo6', { limiter: { max : 10, duration: 20000 } })
console.error('dentro main')
queue.clean(1000)
await queue.obliterate({ force: true });
queue.process(job => manager.get(job.data))
const sources = await prisma.source.findMany()
console.error(sources)
// sources.forEach(manager.get)
// manager.get()
// console.error(sources.map(s => s.URL))
sources.forEach( s => queue.add(s, { jobId: s.id, repeat: { every: 10000 } }))
}
main()
// manager.main()

103
server/cohort.js Normal file
View file

@ -0,0 +1,103 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
import accepts from 'accepts'
import { Feed } from 'feed'
import express from 'express'
import cors from 'cors'
const app = express.Router()
const cohortController = {
async create (req, res) {
// const { name, sources } = await useBody(req)
console.error(req.body)
const { name, description, sources } = req.body
const cohort = await prisma.cohort.create({ include: { sources: true }, data: {
name,
description,
sources: {
connect: sources.map(id => ({ id }))
}
}})
return res.json(cohort)
},
feed (cohort, posts) {
const feed = new Feed({
title: cohort.name,
description: cohort.description,
id: cohort.id,
// link: cohort.id,
generator: 'Chew'
})
posts.forEach(post => {
feed.addItem({
title: post.title,
id: post.URL,
link: post.URL,
description: post.summary,
content: post.content,
image: post.image,
date: post.updatedAt
})
})
return feed.atom1()
}
}
app.post('/', cohortController.create)
app.get('/:id\.?:format(json|rss|atom|xml)?', cors(), async (req, res) => {
const format = req.params.format || 'json'
const id = Number(req.params.id)
const maxPosts = Number(req.query.maxPosts) || 10
console.error('id', id)
const cohort = await prisma.cohort.findUnique({ where: { id }, include: { sources: { select: { id: true }} } })
if (!id || !cohort || !cohort.sources) return res.sendStatus(404)
await prisma.cohort.update({ where: { id }, data: { dailyView: { increment: 1 } } })
const posts = await prisma.post.findMany({
orderBy: [ { date: 'desc'} ],
take: maxPosts,
where: {
sourceId: {
in: cohort.sources.map(s => s.id)
}
},
include: {
source: true
}
})
const accept = accepts(req)
console.error(accept.types())
switch (format) {
case 'xml':
case 'rss':
return res
.contentType('application/rss+xml')
// .setHeader('Last-Modified', new Date(cohort.updatedAt).toUTCString())
// .set('ETag', Math.round(new Date(cohort.updatedAt).getTime() / 1000))
.send(cohortController.feed(cohort, posts))
case 'json':
default:
return res.json({ cohort, posts })
}
})
// app.use((err, req, res, next) => {
// console.error('sono asodifaosdijf');
// console.error(err);
// res.status(500).json({error: 'an error occurred'});
// });
// if (req.method === 'POST') {
// return cohortController.create(req)
// }
// }
export default app

11
server/cohorts.js Normal file
View file

@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
import express from 'express'
const app = express.Router()
app.get('/', async (req, res) => {
res.json(await prisma.cohort.findMany({ take: 10 }))
})
export default app

1
server/feed/index.js Normal file
View file

@ -0,0 +1 @@
export default async () => 'ciao'

160
server/helper.js Normal file
View file

@ -0,0 +1,160 @@
// const iconv = require('iconv-lite')
import iconv from 'iconv-lite'
import FeedParser from 'feedparser'
import { parseHTML } from 'linkedom'
import fetch from 'node-fetch'
import createDOMPurify from 'dompurify'
import { JSDOM } from 'jsdom'
const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)
export function getParams (str) {
const params = str.split(';').reduce((params, param) => {
const parts = param.split('=').map(part => part.trim())
if (parts.length === 2) {
params[parts[0]] = parts[1]
}
return params
}, {})
return params
}
DOMPurify.addHook('beforeSanitizeElements', node => {
if (node.hasAttribute && node.hasAttribute('href')) {
const href = node.getAttribute('href')
const text = node.textContent
// remove FB tracking param
if (href.includes('fbclid=')) {
try {
const url = new URL.URL(href)
url.searchParams.delete('fbclid')
node.setAttribute('href', url.href)
if (text.includes('fbclid=')) {
node.textContent = url.href
}
} catch (e) {
return node
}
}
}
return node
})
export function parseContent (html) {
console.error(html)
const saneHTML = DOMPurify.sanitize(html, {
CUSTOM_ELEMENT_HANDLING: {
tagNameCheck: /^(gancio-.*|display-feed)/,
attributeNameCheck: /(feed|id|theme)/,
allowCustomizedBuiltInElements: true, // allow customized built-ins
},
ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'br', 'i', 'span', 'img', 'figure',
'h6', 'b', 'a', 'li', 'ul', 'ol', 'code', 'blockquote', 'u', 's', 'strong'],
ALLOWED_ATTR: ['href', 'target', 'src']
})
console.error(saneHTML)
// const images = window.document.getElementsByTagName('img')
const { document } = new JSDOM(html).window
const img = document.querySelector('img[src]')
console.error('sono dentro il parsing!')
console.error(img)
let image
if (img) {
image = img.getAttribute('src')
}
return { html: saneHTML, image }
}
export function maybeTranslate (res, charset) {
let iconvStream
// Decode using iconv-lite if its not utf8 already.
if (!iconvStream && charset && !/utf-*8/i.test(charset)) {
try {
iconvStream = iconv.decodeStream(charset)
console.log('Converting from charset %s to utf-8', charset)
// iconvStream.on('error', done)
// If we're using iconvStream, stream will be the output of iconvStream
// otherwise it will remain the output of request
res = res.pipe(iconvStream)
} catch(err) {
res.emit('error', err)
}
}
return res
}
/**
* @param {*} URL
* @description Check if URL is a valid atom/rss feed or in case it's an html search for a public feed
* then retrieve feed detailed information
* @returns An object with feed information (title, url)
*/
export async function getFeedDetails (URL) {
// Get a response stream
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0
const res = await fetch(URL,
{
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36',
'accept': 'text/html,application/xhtml+xml'
})
// Handle our response and pipe it to feedparser
console.error(res.status)
if (res.status !== 200) throw new Error('Bad status code')
const contentType = res.headers.get('content-type')
if (contentType.includes('html')) {
console.error('parse html')
const { document } = parseHTML(await res.text())
const links = document.querySelectorAll('link[rel=alternate]')
const feeds = []
links.forEach(link => {
const type = link.getAttribute('type')
const href = link.getAttribute('href')
if (type && href) {
feeds[type] = feeds[type] || href
}
})
console.error(feeds)
if (feeds['application/atom+xml']) {
return getFeedDetails(feeds['application/atom+xml'])
} else if (feeds['application/rss+xml']) {
return getFeedDetails(feeds['application/rss+xml'])
} else {
throw new Error(feeds)
}
}
console.error('parse atom feed')
// feedparser.on('error', e => manager.sourceError(e, source))
// feedparser.on('end', e => manager.sourceCompleted(source))
return new Promise((resolve, reject) => {
const feedparser = new FeedParser()
feedparser.on('readable', () => {
// console.error('sono dentro readable!', feedparser.read())
feedparser.meta.URL = URL
return resolve(feedparser.meta)
})
feedparser.on('error', reject)
feedparser.on('end', resolve)
// Handle our response and pipe it to feedparser
const charset = getParams(res.headers.get('content-type') || '').charset
console.error('chartset -> ', charset)
let responseStream = maybeTranslate(res.body, charset)
// And boom goes the dynamite
responseStream.pipe(feedparser)
})
}
// module.exports = { getParams, getFeedDetails, maybeTranslate }

35
server/index.js Normal file
View file

@ -0,0 +1,35 @@
import express from 'express'
import bodyParser from 'body-parser'
import cohort from './cohort.js'
import cohorts from './cohorts.js'
import posts from './posts.js'
import source from './source.js'
const app = express()
app.use((req, res, next) => {
console.error(req.path)
next()
})
const api = express.Router()
api.use(bodyParser.json())
app.use('/api', api)
api.use('/cohort', cohort)
api.use('/cohorts', cohorts)
api.use('/posts', posts)
api.use('/source', source)
// app.use((req, res, next) => {
// console.error('404!')
// return res.status(404).json(err)
// next()
// })
app.use((err, req, res, next) => {
console.error('sono asodifaosdijf');
console.error(err.stack);
// return res.status(500).json({error: 'an error occurred'});
})
import './chew'
export default app

View file

@ -0,0 +1,16 @@
export default async function () {
async function start (nuxt) {
// close connections/port/unix socket
async function shutdown () {
process.off('SIGTERM', shutdown)
process.off('SIGINT', shutdown)
nuxt.close()
process.exit()
}
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
}
this.nuxt.hook('listen', start)
}

10
server/posts.js Normal file
View file

@ -0,0 +1,10 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
import express from 'express'
const app = express.Router()
app.get('/', async (req, res) => {
res.json(await prisma.post.findMany({ take: 10, orderBy: { date: 'desc' }, include: { source: true }}))
})
export default app

152
server/source.js Normal file
View file

@ -0,0 +1,152 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// import { useBody } from 'h3'
import { getFeedDetails } from './helper.js'
import cors from 'cors'
import express from 'express'
import { Feed } from 'feed'
import { add } from './chew'
const app = express.Router()
const sourceController = {
feed (source, posts) {
const feed = new Feed({
title: source.name,
description: source.description,
id: source.id,
// link: source.id,
generator: 'Chew'
})
posts.forEach(post => {
feed.addItem({
title: post.title,
id: post.URL,
link: post.URL,
description: post.summary,
content: post.content,
image: post.image,
date: post.updatedAt
})
})
return feed.atom1()
}
}
app.get('/search', async (req, res) => {
const search = req.query.search || ''
console.error('sono dentro search', search)
const sources = await prisma.source.findMany({ where: {
OR: [
{ name: { contains: search } },
{ URL: { contains: search } }
]
}})
return res.json(sources)
})
app.get('/', async (req, res) => {
const sources = await prisma.source.findMany()
return res.json(sources)
})
const corsOptions = { exposedHeaders: ['Accept-Language',
'Access-Control-Allow-Origin',
'Connection', 'Content-Length', 'Content-Type', 'Date',
'Etag', 'Server', 'Via', 'X-Powered-By']
}
app.get('/:id\.?:format(json|rss|atom|xml)?', cors(corsOptions), async (req, res) => {
const format = req.params.format || 'json'
const sourceId = Number(req.params.id)
const maxPosts = Number(req.query.maxPosts) || 10
const source = await prisma.source.findUnique({ where: { id: sourceId }})
if (!sourceId || !source) return res.sendStatus(404)
// await prisma..update({ where: { id }, data: { dailyView: { increment: 1 } } })
const posts = await prisma.post.findMany({
orderBy: [ { date: 'desc'} ],
take: maxPosts,
where: {
sourceId
},
include: {
source: true
}
})
// const accept = accepts(req)
// console.error(accept.types())
switch (format) {
case 'xml':
case 'rss':
case 'atom':
return res
.contentType('application/rss+xml')
.setHeader('Last-Modified', new Date(source.updatedAt).toUTCString())
.set('ETag', Math.round(new Date(source.updatedAt).getTime() / 1000))
.send(sourceController.feed(source, posts))
case 'json':
default:
return res.json({ source, posts })
}
})
app.post('/', async (req, res) => {
const URL = req.body.URL
// const URL = req.query.URL || req.params.URL || req.body.URL
// return
// check if URL already exists
let dbsource = await prisma.source.findFirst( {
where: { URL },
include: {
_count: {
select: { posts: true }
}
}
})
if (dbsource) return res.json(dbsource)
let source
try {
source = await getFeedDetails(URL)
} catch (e) {
console.error(String(e))
return res.sendStatus(404)
}
if (!source) {
return res.sendStatus(404)
}
dbsource = await prisma.source.findFirst( {
where: { URL: source.URL },
include: {
_count: {
select: { posts: true }
}
}
})
if (dbsource) return res.json(dbsource)
const ret = await prisma.source.create({ data: {
name: source.title,
description: source.description,
URL: source.URL,
link: source.link,
updatedAt: source.date || undefined,
// image: source.image
}})
add(ret)
return res.json(ret)
// return prisma.cohort.findMany({ take: 10 })
// return { status: 'OK'}
})
export default app

655
static/display-feed.js Normal file
View file

@ -0,0 +1,655 @@
function noop() {
}
function run(fn) {
return fn();
}
function blank_object() {
return Object.create(null);
}
function run_all(fns) {
fns.forEach(run);
}
function is_function(thing) {
return typeof thing === "function";
}
function safe_not_equal(a, b) {
return a != a ? b == b : a !== b || (a && typeof a === "object" || typeof a === "function");
}
let src_url_equal_anchor;
function src_url_equal(element_src, url) {
if (!src_url_equal_anchor) {
src_url_equal_anchor = document.createElement("a");
}
src_url_equal_anchor.href = url;
return element_src === src_url_equal_anchor.href;
}
function is_empty(obj) {
return Object.keys(obj).length === 0;
}
function append(target, node) {
target.appendChild(node);
}
function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
function detach(node) {
node.parentNode.removeChild(node);
}
function destroy_each(iterations, detaching) {
for (let i = 0; i < iterations.length; i += 1) {
if (iterations[i])
iterations[i].d(detaching);
}
}
function element(name) {
return document.createElement(name);
}
function text(data) {
return document.createTextNode(data);
}
function space() {
return text(" ");
}
function empty() {
return text("");
}
function attr(node, attribute, value) {
if (value == null)
node.removeAttribute(attribute);
else if (node.getAttribute(attribute) !== value)
node.setAttribute(attribute, value);
}
function children(element2) {
return Array.from(element2.childNodes);
}
function set_data(text2, data) {
data = "" + data;
if (text2.wholeText !== data)
text2.data = data;
}
function attribute_to_object(attributes) {
const result = {};
for (const attribute of attributes) {
result[attribute.name] = attribute.value;
}
return result;
}
let current_component;
function set_current_component(component) {
current_component = component;
}
function get_current_component() {
if (!current_component)
throw new Error("Function called outside component initialization");
return current_component;
}
function onMount(fn) {
get_current_component().$$.on_mount.push(fn);
}
const dirty_components = [];
const binding_callbacks = [];
const render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = Promise.resolve();
let update_scheduled = false;
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
function add_render_callback(fn) {
render_callbacks.push(fn);
}
let flushing = false;
const seen_callbacks = new Set();
function flush() {
if (flushing)
return;
flushing = true;
do {
for (let i = 0; i < dirty_components.length; i += 1) {
const component = dirty_components[i];
set_current_component(component);
update(component.$$);
}
set_current_component(null);
dirty_components.length = 0;
while (binding_callbacks.length)
binding_callbacks.pop()();
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
seen_callbacks.add(callback);
callback();
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
flushing = false;
seen_callbacks.clear();
}
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback);
}
}
const outroing = new Set();
function transition_in(block, local) {
if (block && block.i) {
outroing.delete(block);
block.i(local);
}
}
function mount_component(component, target, anchor, customElement) {
const { fragment, on_mount, on_destroy, after_update } = component.$$;
fragment && fragment.m(target, anchor);
if (!customElement) {
add_render_callback(() => {
const new_on_destroy = on_mount.map(run).filter(is_function);
if (on_destroy) {
on_destroy.push(...new_on_destroy);
} else {
run_all(new_on_destroy);
}
component.$$.on_mount = [];
});
}
after_update.forEach(add_render_callback);
}
function destroy_component(component, detaching) {
const $$ = component.$$;
if ($$.fragment !== null) {
run_all($$.on_destroy);
$$.fragment && $$.fragment.d(detaching);
$$.on_destroy = $$.fragment = null;
$$.ctx = [];
}
}
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[i / 31 | 0] |= 1 << i % 31;
}
function init(component, options, instance2, create_fragment2, not_equal, props, append_styles, dirty = [-1]) {
const parent_component = current_component;
set_current_component(component);
const $$ = component.$$ = {
fragment: null,
ctx: null,
props,
update: noop,
not_equal,
bound: blank_object(),
on_mount: [],
on_destroy: [],
on_disconnect: [],
before_update: [],
after_update: [],
context: new Map(options.context || (parent_component ? parent_component.$$.context : [])),
callbacks: blank_object(),
dirty,
skip_bound: false,
root: options.target || parent_component.$$.root
};
append_styles && append_styles($$.root);
let ready = false;
$$.ctx = instance2 ? instance2(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i])
$$.bound[i](value);
if (ready)
make_dirty(component, i);
}
return ret;
}) : [];
$$.update();
ready = true;
run_all($$.before_update);
$$.fragment = create_fragment2 ? create_fragment2($$.ctx) : false;
if (options.target) {
if (options.hydrate) {
const nodes = children(options.target);
$$.fragment && $$.fragment.l(nodes);
nodes.forEach(detach);
} else {
$$.fragment && $$.fragment.c();
}
if (options.intro)
transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor, options.customElement);
flush();
}
set_current_component(parent_component);
}
let SvelteElement;
if (typeof HTMLElement === "function") {
SvelteElement = class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
const { on_mount } = this.$$;
this.$$.on_disconnect = on_mount.map(run).filter(is_function);
for (const key in this.$$.slotted) {
this.appendChild(this.$$.slotted[key]);
}
}
attributeChangedCallback(attr2, _oldValue, newValue) {
this[attr2] = newValue;
}
disconnectedCallback() {
run_all(this.$$.on_disconnect);
}
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1)
callbacks.splice(index, 1);
};
}
$set($$props) {
if (this.$$set && !is_empty($$props)) {
this.$$.skip_bound = true;
this.$$set($$props);
this.$$.skip_bound = false;
}
}
};
}
function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[7] = list[i];
return child_ctx;
}
function create_if_block(ctx) {
let div;
let t;
let if_block = ctx[0] && create_if_block_2(ctx);
let each_value = ctx[2];
let each_blocks = [];
for (let i = 0; i < each_value.length; i += 1) {
each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i));
}
return {
c() {
div = element("div");
if (if_block)
if_block.c();
t = space();
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].c();
}
attr(div, "id", "displayFeed");
},
m(target, anchor) {
insert(target, div, anchor);
if (if_block)
if_block.m(div, null);
append(div, t);
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].m(div, null);
}
},
p(ctx2, dirty) {
if (ctx2[0]) {
if (if_block) {
if_block.p(ctx2, dirty);
} else {
if_block = create_if_block_2(ctx2);
if_block.c();
if_block.m(div, t);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
}
if (dirty & 6) {
each_value = ctx2[2];
let i;
for (i = 0; i < each_value.length; i += 1) {
const child_ctx = get_each_context(ctx2, each_value, i);
if (each_blocks[i]) {
each_blocks[i].p(child_ctx, dirty);
} else {
each_blocks[i] = create_each_block(child_ctx);
each_blocks[i].c();
each_blocks[i].m(div, null);
}
}
for (; i < each_blocks.length; i += 1) {
each_blocks[i].d(1);
}
each_blocks.length = each_value.length;
}
},
d(detaching) {
if (detaching)
detach(div);
if (if_block)
if_block.d();
destroy_each(each_blocks, detaching);
}
};
}
function create_if_block_2(ctx) {
let div;
let span;
let t;
return {
c() {
div = element("div");
span = element("span");
t = text(ctx[0]);
attr(span, "id", "headerTitle");
attr(div, "class", "content");
},
m(target, anchor) {
insert(target, div, anchor);
append(div, span);
append(span, t);
},
p(ctx2, dirty) {
if (dirty & 1)
set_data(t, ctx2[0]);
},
d(detaching) {
if (detaching)
detach(div);
}
};
}
function create_if_block_1(ctx) {
let div;
let raw_value = ctx[7].summary + "";
return {
c() {
div = element("div");
attr(div, "class", "summary");
},
m(target, anchor) {
insert(target, div, anchor);
div.innerHTML = raw_value;
},
p(ctx2, dirty) {
if (dirty & 4 && raw_value !== (raw_value = ctx2[7].summary + ""))
div.innerHTML = raw_value;
},
d(detaching) {
if (detaching)
detach(div);
}
};
}
function create_each_block(ctx) {
let a;
let div0;
let img;
let img_src_value;
let img_alt_value;
let t0;
let div4;
let div1;
let t1_value = ctx[7].source.name + "";
let t1;
let t2;
let div2;
let t3_value = ctx[7].title + "";
let t3;
let t4;
let div3;
let t5_value = when(ctx[7].date) + "";
let t5;
let t6;
let t7;
let a_href_value;
let a_title_value;
let if_block = ctx[1] === "true" && create_if_block_1(ctx);
return {
c() {
a = element("a");
div0 = element("div");
img = element("img");
t0 = space();
div4 = element("div");
div1 = element("div");
t1 = text(t1_value);
t2 = space();
div2 = element("div");
t3 = text(t3_value);
t4 = space();
div3 = element("div");
t5 = text(t5_value);
t6 = space();
if (if_block)
if_block.c();
t7 = space();
if (!src_url_equal(img.src, img_src_value = ctx[7].image))
attr(img, "src", img_src_value);
attr(img, "alt", img_alt_value = ctx[7].title);
attr(div0, "class", "media");
attr(div1, "class", "subtitle");
attr(div2, "class", "title");
attr(div3, "class", "subtitle");
attr(div4, "class", "content");
attr(a, "href", a_href_value = ctx[7].URL);
attr(a, "title", a_title_value = ctx[7].title);
attr(a, "class", "post");
attr(a, "target", "_blank");
},
m(target, anchor) {
insert(target, a, anchor);
append(a, div0);
append(div0, img);
append(a, t0);
append(a, div4);
append(div4, div1);
append(div1, t1);
append(div4, t2);
append(div4, div2);
append(div2, t3);
append(div4, t4);
append(div4, div3);
append(div3, t5);
append(div4, t6);
if (if_block)
if_block.m(div4, null);
append(a, t7);
},
p(ctx2, dirty) {
if (dirty & 4 && !src_url_equal(img.src, img_src_value = ctx2[7].image)) {
attr(img, "src", img_src_value);
}
if (dirty & 4 && img_alt_value !== (img_alt_value = ctx2[7].title)) {
attr(img, "alt", img_alt_value);
}
if (dirty & 4 && t1_value !== (t1_value = ctx2[7].source.name + ""))
set_data(t1, t1_value);
if (dirty & 4 && t3_value !== (t3_value = ctx2[7].title + ""))
set_data(t3, t3_value);
if (dirty & 4 && t5_value !== (t5_value = when(ctx2[7].date) + ""))
set_data(t5, t5_value);
if (ctx2[1] === "true") {
if (if_block) {
if_block.p(ctx2, dirty);
} else {
if_block = create_if_block_1(ctx2);
if_block.c();
if_block.m(div4, null);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
}
if (dirty & 4 && a_href_value !== (a_href_value = ctx2[7].URL)) {
attr(a, "href", a_href_value);
}
if (dirty & 4 && a_title_value !== (a_title_value = ctx2[7].title)) {
attr(a, "title", a_title_value);
}
},
d(detaching) {
if (detaching)
detach(a);
if (if_block)
if_block.d();
}
};
}
function create_fragment(ctx) {
let if_block_anchor;
let if_block = ctx[2].length && create_if_block(ctx);
return {
c() {
if (if_block)
if_block.c();
if_block_anchor = empty();
this.c = noop;
},
m(target, anchor) {
if (if_block)
if_block.m(target, anchor);
insert(target, if_block_anchor, anchor);
},
p(ctx2, [dirty]) {
if (ctx2[2].length) {
if (if_block) {
if_block.p(ctx2, dirty);
} else {
if_block = create_if_block(ctx2);
if_block.c();
if_block.m(if_block_anchor.parentNode, if_block_anchor);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
}
},
i: noop,
o: noop,
d(detaching) {
if (if_block)
if_block.d(detaching);
if (detaching)
detach(if_block_anchor);
}
};
}
function when(timestamp) {
return new Date(timestamp).toLocaleDateString(void 0, {
weekday: "long",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
}
function instance($$self, $$props, $$invalidate) {
let { feed = "" } = $$props;
let { title = "Feed" } = $$props;
let { max = false } = $$props;
let { summary = "true" } = $$props;
let mounted = false;
let posts = [];
function update2(v) {
if (!mounted)
return;
fetch(feed).then((res) => res.json()).then((e) => {
$$invalidate(2, posts = e.posts);
}).catch((e) => {
console.error(e);
});
}
onMount(() => {
mounted = true;
update2();
});
$$self.$$set = ($$props2) => {
if ("feed" in $$props2)
$$invalidate(3, feed = $$props2.feed);
if ("title" in $$props2)
$$invalidate(0, title = $$props2.title);
if ("max" in $$props2)
$$invalidate(4, max = $$props2.max);
if ("summary" in $$props2)
$$invalidate(1, summary = $$props2.summary);
};
$$self.$$.update = () => {
if ($$self.$$.dirty & 25) {
update2();
}
};
return [title, summary, posts, feed, max];
}
class DisplayFeed extends SvelteElement {
constructor(options) {
super();
this.shadowRoot.innerHTML = `<style>#displayFeed{font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;overflow-x:hidden;font-size:1.2rem;width:100%;box-sizing:content-box}#headerTitle{line-height:45px;font-size:1.3rem;font-weight:600}.post{display:flex;flex-direction:row;flex-wrap:wrap;width:100%;padding:0px;color:black;text-decoration:none;transition:background .3s ease-in-out;border-radius:5px;;}.post:hover{background-color:#ececec}.post .media,.post .content{padding:10px;display:flex;flex-direction:column;flex-basis:100%;flex:1}.post .content{flex:3}.post .content .title{font-size:1.5rem;font-weight:700;color:black}.post .content .subtitle{font-weight:100;color:#666;font-size:.9rem}.post .content .summary{margin-top:1rem;color:#333;font-size:1rem}.post .media img{border-radius:5px;flex:1;max-height:250px;width:400px;object-fit:cover}</style>`;
init(this, {
target: this.shadowRoot,
props: attribute_to_object(this.attributes),
customElement: true
}, instance, create_fragment, safe_not_equal, { feed: 3, title: 0, max: 4, summary: 1 }, null);
if (options) {
if (options.target) {
insert(options.target, this, options.anchor);
}
if (options.props) {
this.$set(options.props);
flush();
}
}
}
static get observedAttributes() {
return ["feed", "title", "max", "summary"];
}
get feed() {
return this.$$.ctx[3];
}
set feed(feed) {
this.$$set({ feed });
flush();
}
get title() {
return this.$$.ctx[0];
}
set title(title) {
this.$$set({ title });
flush();
}
get max() {
return this.$$.ctx[4];
}
set max(max) {
this.$$set({ max });
flush();
}
get summary() {
return this.$$.ctx[1];
}
set summary(summary) {
this.$$set({ summary });
flush();
}
}
customElements.define("display-feed", DisplayFeed);

File diff suppressed because one or more lines are too long

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
static/v.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

1
static/vuetify-logo.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

After

Width:  |  Height:  |  Size: 539 B

10
store/README.md Normal file
View file

@ -0,0 +1,10 @@
# STORE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your Vuex Store files.
Vuex Store option is implemented in the Nuxt.js framework.
Creating a file in this directory automatically activates the option in the framework.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).

4
webcomponents/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/node_modules/
/dist/
/.vscode/
.DS_Store

48
webcomponents/README.md Normal file
View file

@ -0,0 +1,48 @@
# Svelte + Vite
This template should help get you started developing with Svelte in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `checkJs` in the JS template?**
It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```js
// store.js
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

14
webcomponents/index.html Normal file
View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Display Feed</title>
</head>
<body>
<display-feed
feed='http://localhost:3000/api/source/2' title='Feed'></display-feed>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -0,0 +1,34 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "esnext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

View file

@ -0,0 +1,19 @@
{
"name": "display-feed",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:lib": "vite build -c=vite.lib.config.js",
"serve": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.11",
"svelte": "^3.37.0",
"vite": "^2.6.4"
},
"dependencies": {
"twind": "^0.16.16"
}
}

View file

@ -0,0 +1,154 @@
<script>
import { onMount } from 'svelte'
export let feed = ''
export let title = 'Feed'
export let max = false
export let summary = 'true'
// export let tags = ''
let mounted = false
let posts = []
function update (v) {
if (!mounted) return
const params = []
if (max) {
params.push(`max=${max}`)
}
fetch(feed)
.then(res => res.json())
.then(e => {
posts = e.posts
})
.catch(e => {
console.error(e)
})
}
function when (timestamp) {
return new Date(timestamp)
.toLocaleDateString(undefined,
{
weekday: 'long',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
onMount(() => {
mounted = true
update()
})
$: update(feed && max && title)
</script>
<svelte:options tag="display-feed"/>
{#if posts.length}
<div id='displayFeed'>
{#if title}
<div class='content'>
<span id='headerTitle'>{title}</span>
</div>
{/if}
{#each posts as post}
<a href='{post.URL}' title='{post.title}' class='post' target='_blank'>
<div class='media'>
<img src='{post.image}' alt={post.title}/>
</div>
<div class='content'>
<div class='subtitle'>{post.source.name}</div>
<div class='title'>{post.title}</div>
<div class='subtitle'>{when(post.date)}</div>
{#if summary === 'true'}
<div class='summary'>{@html post.summary}</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
<style>
#displayFeed {
font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
overflow-x: hidden;
font-size: 1.2rem;
width: 100%;
box-sizing: content-box;
}
#headerTitle {
line-height: 45px;
font-size: 1.3rem;
font-weight: 600;
}
.post {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
padding: 0px;
color: black;
text-decoration: none;
transition: background .3s ease-in-out;
border-radius: 5px;;
}
.post:hover {
background-color: #ececec;
}
.post .media,
.post .content {
padding: 10px;
display: flex;
flex-direction: column;
flex-basis: 100%;
flex: 1;
}
.post .content {
flex: 3;
}
.post .content .title {
font-size: 1.5rem;
font-weight: 700;
color: black;
}
.post .content .subtitle {
font-weight: 100;
color: #666;
font-size: .9rem;
}
.post .content .summary {
margin-top: 1rem;
color: #333;
font-size: 1rem;
}
.post .content .summary a {
color: orangered;
text-decoration: none;
}
.post .media img {
border-radius: 5px;
flex: 1;
max-height: 250px;
width: 400px;
object-fit: cover;
}
</style>

View file

@ -0,0 +1 @@
export * from './DisplayFeed.svelte'

View file

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
svelte({
compilerOptions: { customElement: true }
})
]
})

View file

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: './src/main.js',
name: 'GancioEvents'
}
},
plugins: [svelte({compilerOptions: { customElement: true }})]
})

329
webcomponents/yarn.lock Normal file
View file

@ -0,0 +1,329 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@rollup/pluginutils@^4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec"
integrity sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@sveltejs/vite-plugin-svelte@^1.0.0-next.11":
version "1.0.0-next.31"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.0-next.31.tgz#5d0d5445ed85a1af613224eacff78c69f14c7fad"
integrity sha512-8K3DcGP1V+XBv389u32S6wt8xiun6hHd5wn28AKLSoNTIhOmJOA2RJUJzp0seTRI86Shme4lzHI2Fgq4qz1wXQ==
dependencies:
"@rollup/pluginutils" "^4.1.1"
debug "^4.3.3"
kleur "^4.1.4"
magic-string "^0.25.7"
require-relative "^0.8.7"
svelte-hmr "^0.14.7"
csstype@^3.0.5:
version "3.0.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
debug@^4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
dom-serializer@^1.0.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.2.0"
entities "^2.0.0"
domelementtype@^2.0.1, domelementtype@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
domhandler@^4.0.0, domhandler@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626"
integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==
dependencies:
domelementtype "^2.2.0"
domutils@^2.5.2:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
dependencies:
dom-serializer "^1.0.1"
domelementtype "^2.2.0"
domhandler "^4.2.0"
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
esbuild-android-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44"
integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==
esbuild-darwin-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72"
integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==
esbuild-darwin-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a"
integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==
esbuild-freebsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85"
integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==
esbuild-freebsd-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52"
integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==
esbuild-linux-32@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69"
integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==
esbuild-linux-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3"
integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==
esbuild-linux-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1"
integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==
esbuild-linux-arm@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe"
integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==
esbuild-linux-mips64le@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7"
integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==
esbuild-linux-ppc64le@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2"
integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==
esbuild-netbsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038"
integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==
esbuild-openbsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7"
integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==
esbuild-sunos-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4"
integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==
esbuild-windows-32@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7"
integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==
esbuild-windows-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294"
integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==
esbuild-windows-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3"
integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==
esbuild@^0.13.2:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf"
integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==
optionalDependencies:
esbuild-android-arm64 "0.13.15"
esbuild-darwin-64 "0.13.15"
esbuild-darwin-arm64 "0.13.15"
esbuild-freebsd-64 "0.13.15"
esbuild-freebsd-arm64 "0.13.15"
esbuild-linux-32 "0.13.15"
esbuild-linux-64 "0.13.15"
esbuild-linux-arm "0.13.15"
esbuild-linux-arm64 "0.13.15"
esbuild-linux-mips64le "0.13.15"
esbuild-linux-ppc64le "0.13.15"
esbuild-netbsd-64 "0.13.15"
esbuild-openbsd-64 "0.13.15"
esbuild-sunos-64 "0.13.15"
esbuild-windows-32 "0.13.15"
esbuild-windows-64 "0.13.15"
esbuild-windows-arm64 "0.13.15"
estree-walker@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
htmlparser2@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.0.0"
domutils "^2.5.2"
entities "^2.0.0"
is-core-module@^2.2.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
dependencies:
has "^1.0.3"
kleur@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
dependencies:
sourcemap-codec "^1.4.4"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nanoid@^3.1.30:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
path-parse@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.2.2:
version "2.3.0"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
postcss@^8.3.8:
version "8.4.4"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.4.tgz#d53d4ec6a75fd62557a66bb41978bf47ff0c2869"
integrity sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==
dependencies:
nanoid "^3.1.30"
picocolors "^1.0.0"
source-map-js "^1.0.1"
require-relative@^0.8.7:
version "0.8.7"
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=
resolve@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
dependencies:
is-core-module "^2.2.0"
path-parse "^1.0.6"
rollup@^2.57.0:
version "2.60.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.2.tgz#3f45ace36a9b10b4297181831ea0719922513463"
integrity sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw==
optionalDependencies:
fsevents "~2.3.2"
source-map-js@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
sourcemap-codec@^1.4.4:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
style-vendorizer@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/style-vendorizer/-/style-vendorizer-2.1.1.tgz#5f06601c1724cfb314fe1153e7e442c58dde771c"
integrity sha512-gVO6Cwxtg8iX0X1W4xMhSc5WbQpiIBQDkhq3JkwebMRRgyhCfuvMrnPlTAGTRjfQPGRmzgjCOZ4drehTnLahHA==
svelte-hmr@^0.14.7:
version "0.14.7"
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.7.tgz#7fa8261c7b225d9409f0a86f3b9ea5c3ca6f6607"
integrity sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==
svelte@^3.37.0:
version "3.44.2"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.44.2.tgz#3e69be2598308dfc8354ba584cec54e648a50f7f"
integrity sha512-jrZhZtmH3ZMweXg1Q15onb8QlWD+a5T5Oca4C1jYvSURp2oD35h4A5TV6t6MEa93K4LlX6BkafZPdQoFjw/ylA==
twind@^0.16.16:
version "0.16.16"
resolved "https://registry.yarnpkg.com/twind/-/twind-0.16.16.tgz#76959cd21897528f9a2631a293e3381b668332ad"
integrity sha512-UlAYjkGCdgjg4xU1SIwRW0PpG0anZrY32+kS2jYsi32yb4RuW0ttzaI1OglgwAUk/rZwzoINilnIFORzOSFZag==
dependencies:
csstype "^3.0.5"
htmlparser2 "^6.0.0"
style-vendorizer "^2.0.0"
vite@^2.6.4:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.6.14.tgz#35c09a15e4df823410819a2a239ab11efb186271"
integrity sha512-2HA9xGyi+EhY2MXo0+A2dRsqsAG3eFNEVIo12olkWhOmc8LfiM+eMdrXf+Ruje9gdXgvSqjLI9freec1RUM5EA==
dependencies:
esbuild "^0.13.2"
postcss "^8.3.8"
resolve "^1.20.0"
rollup "^2.57.0"
optionalDependencies:
fsevents "~2.3.2"

8914
yarn.lock Normal file

File diff suppressed because it is too large Load diff