Skip to content

Commit 1707715

Browse files
authored
Add media gallery plugin server
1 parent 80a7a51 commit 1707715

File tree

7 files changed

+1444
-0
lines changed

7 files changed

+1444
-0
lines changed

media-gallery-plugin/.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PORT="3001"
2+
URL="http://localhost:3001"

media-gallery-plugin/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
.DS_Store
3+
.env

media-gallery-plugin/README.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Kit Media Gallery Plugin Demo
2+
3+
This directory serves as an example of how you can structure your application to respond
4+
to Kit's media gallery plugin framework.
5+
6+
It contains a *list media* endpoint, which returns a list of paginated media items to
7+
display in the gallery. You can view the comments in `index.js` to learn more about the
8+
implementation.
9+
10+
If you want to run this app locally, you can do so by running:
11+
12+
```
13+
cp .env.example .env
14+
npm install
15+
npm run start
16+
```
17+
18+
This endpoint assume that your plugin would be structured as follows on your [plugin
19+
settings](https://app.kit.com/account_settings/developer_settings).
20+
21+
- **Plugin name:**
22+
My Media
23+
24+
- **Description:**
25+
A media gallery plugin demo.
26+
27+
- **HTML URL:**
28+
https://YOUR_URL/media
29+
30+
> Replace `YOUR_URL` with your server's URL.
31+
32+
- **Settings JSON:**
33+
34+
```json
35+
[
36+
{
37+
"type": "text",
38+
"name": "query",
39+
"help": "Search your media",
40+
"required": false
41+
}
42+
]
43+
```

media-gallery-plugin/index.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
require('dotenv').config()
2+
const express = require('express')
3+
const {
4+
search,
5+
filter,
6+
sort,
7+
paginate,
8+
find,
9+
MEDIA_ITEMS,
10+
} = require('./media-generator')
11+
const app = express()
12+
const port = process.env.PORT || 3001
13+
14+
app.use(express.json())
15+
16+
/**
17+
* Responds to request for listing media items.
18+
* - Expects pagination query params: `after`, `before`, and `per_page`.
19+
* - Expects search query param: `search.<setting name>` (configured in plugin settings)
20+
* - Expects filter query params: `filters.<setting name>` (configured in plugin settings)
21+
* - Expects sort query param: `sort.<setting name>` (configured in plugin settings)
22+
*/
23+
app.get('/media', (request, response) => {
24+
console.log(request.query)
25+
26+
const searchedMedia = search(MEDIA_ITEMS, request.query.settings?.query)
27+
const filteredMedia = filter(searchedMedia, request.query.settings || {})
28+
const sortedMedia = sort(filteredMedia, request.query.settings || {})
29+
const {
30+
data: paginatedMedia,
31+
perPage,
32+
startCursor,
33+
endCursor,
34+
hasPreviousPage,
35+
hasNextPage,
36+
} = paginate(sortedMedia, {
37+
after: request.query.after,
38+
before: request.query.before,
39+
perPage: Math.min(Number.parseInt(request.query.per_page || '100'), 1000),
40+
})
41+
const data = paginatedMedia.map(mediaItem => ({
42+
id: mediaItem.id,
43+
type: mediaItem.type,
44+
alt: mediaItem.alt,
45+
caption: mediaItem.caption,
46+
title: mediaItem.title,
47+
url: mediaItem.url,
48+
thumbnail_url: mediaItem.thumbnail_url,
49+
hotlink: mediaItem.hotlink,
50+
attribution: mediaItem.attribution,
51+
notify_download_url: mediaItem.notify_download_url,
52+
}))
53+
54+
response.json({
55+
data,
56+
pagination: {
57+
per_page: perPage,
58+
start_cursor: startCursor,
59+
end_cursor: endCursor,
60+
has_previous_page: hasPreviousPage,
61+
has_next_page: hasNextPage,
62+
},
63+
})
64+
})
65+
66+
/**
67+
* Responds to request that a media item was downloaded.
68+
*/
69+
app.post('/media/:id/downloaded', (request, response) => {
70+
const media = find(request.url)
71+
console.log(
72+
`${media.title} ${media.url} ${request.params.id} was downloaded!`
73+
)
74+
75+
response.sendStatus(204)
76+
})
77+
78+
/**
79+
* Responds to request for listing folder options for dynamic select setting
80+
*/
81+
app.get('/folders', (request, response) => {
82+
response.json({
83+
options: [
84+
{ label: 'Home', value: 'home' },
85+
{ label: 'Favorites', value: 'favorites' },
86+
{ label: 'Shared', value: 'shared' },
87+
],
88+
})
89+
})
90+
91+
app.listen(port, () => {
92+
console.log(`Server running at :${port}`)
93+
})
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const { faker } = require('@faker-js/faker')
2+
const Fuse = require('fuse.js')
3+
4+
/**
5+
* Fake media items. For a real plugin, this would come from a database or API.
6+
*/
7+
const MEDIA_ITEMS = faker.helpers.multiple(
8+
() => {
9+
const id = faker.string.nanoid()
10+
return {
11+
id,
12+
type: 'image',
13+
alt: faker.lorem.sentence(),
14+
caption: faker.lorem.words(),
15+
title: faker.system.commonFileName('jpg'),
16+
url: faker.image.urlPicsumPhotos({
17+
width: 600,
18+
height: 900,
19+
blur: 0,
20+
grayscale: false,
21+
}),
22+
thumbnail_url: faker.image.urlPicsumPhotos({
23+
width: 600,
24+
height: 900,
25+
blur: 0,
26+
grayscale: false,
27+
}),
28+
hotlink: faker.datatype.boolean(),
29+
...(faker.datatype.boolean() && {
30+
attribution: {
31+
label: faker.person.fullName(),
32+
href: `https://${faker.internet.domainName()}/abc?utm_source=your_app_name&utm_medium=referral`,
33+
},
34+
}),
35+
...(faker.datatype.boolean() && {
36+
notify_download_url: `${process.env.URL}/media/${id}/downloaded`,
37+
}),
38+
39+
labels: faker.helpers.arrayElement([
40+
[],
41+
['my_content'],
42+
['my_content', 'favorite'],
43+
['shared'],
44+
]),
45+
created_at: faker.date.anytime(),
46+
}
47+
},
48+
{ count: 2000 }
49+
)
50+
51+
/**
52+
* Searches for media items using query.
53+
*/
54+
function search(items, query) {
55+
const fuse = new Fuse(items, {
56+
threshold: 0.8,
57+
keys: ['title', 'caption', 'attribution.label'],
58+
})
59+
if (query) {
60+
return fuse.search(query).map(search => search.item)
61+
}
62+
return items
63+
}
64+
65+
/**
66+
* Filters for media items
67+
*/
68+
function filter(items, params) {
69+
if (params.label) {
70+
return items.filter(item => item.labels.includes(params.label))
71+
}
72+
return items
73+
}
74+
75+
/**
76+
* Sort for media items
77+
*/
78+
function sort(items, params) {
79+
if (params.sort === 'alphabetical_asc') {
80+
return items.slice().sort((a, b) => a.caption.localeCompare(b.caption))
81+
} else if (params.sort === 'alphabetical_desc') {
82+
return items.slice().sort((a, b) => a.caption.localeCompare(b.caption) * -1)
83+
} else if (params.sort === 'created_asc') {
84+
return items.slice().sort((a, b) => b.created_at - a.created_at)
85+
} else if (params.sort === 'created_desc') {
86+
return items.slice().sort((a, b) => (b.created_at - a.created_at) * -1)
87+
}
88+
return items
89+
}
90+
91+
/**
92+
* Paginates through media items. Uses limit/offset approach.
93+
*/
94+
function paginate(data, { before, after, perPage }) {
95+
const decodedBefore =
96+
before && Buffer.from(before, 'base64').toString('ascii')
97+
const decodedAfter = after && Buffer.from(after, 'base64').toString('ascii')
98+
99+
let startCursor = 0
100+
101+
if (after) {
102+
startCursor = Number.parseInt(decodedAfter)
103+
} else if (before) {
104+
startCursor = Number.parseInt(decodedBefore) - perPage
105+
}
106+
107+
const pagedData = data.slice(startCursor, startCursor + perPage)
108+
109+
return {
110+
data: pagedData,
111+
perPage,
112+
startCursor: Buffer.from(startCursor.toString()).toString('base64'),
113+
endCursor: Buffer.from((startCursor + perPage).toString()).toString(
114+
'base64'
115+
),
116+
hasPreviousPage: startCursor > 0,
117+
hasNextPage: startCursor + perPage <= data.length,
118+
}
119+
}
120+
121+
function find({ url }) {
122+
return MEDIA_ITEMS.find(media => media.notify_download_url === url)
123+
}
124+
125+
module.exports = {
126+
MEDIA_ITEMS,
127+
search,
128+
filter,
129+
sort,
130+
paginate,
131+
find,
132+
}

0 commit comments

Comments
 (0)