├── .DS_Store ├── .eslintrc ├── .gitignore ├── .node-version ├── README.md ├── etc └── dev-server.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── .DS_Store ├── api │ ├── .DS_Store │ ├── collection │ │ ├── get.js │ │ └── index.js │ ├── collections │ │ ├── get.js │ │ └── index.js │ ├── errors.js │ ├── filters │ │ ├── get.js │ │ └── index.js │ ├── index.js │ ├── raindrops │ │ ├── get.js │ │ └── index.js │ └── user │ │ ├── get.js │ │ └── index.js ├── assets │ ├── .DS_Store │ ├── brand │ │ └── icon_48.svg │ └── remixicon.symbol.svg ├── co │ ├── .DS_Store │ ├── badge │ │ ├── index.jsx │ │ └── index.module.css │ ├── button │ │ ├── index.jsx │ │ ├── index.module.css │ │ ├── select.jsx │ │ ├── select.module.css │ │ └── share.jsx │ ├── collections │ │ ├── compact │ │ │ ├── index.jsx │ │ │ └── single.jsx │ │ ├── cover │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── hooks │ │ │ ├── index.jsx │ │ │ ├── useChildrens.jsx │ │ │ ├── useParents.jsx │ │ │ └── useRoot.jsx │ │ └── listing │ │ │ ├── index.jsx │ │ │ ├── index.module.css │ │ │ └── single │ │ │ ├── index.jsx │ │ │ └── index.module.css │ ├── form │ │ ├── checkbox │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── fields │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── form │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── index.jsx │ │ ├── input │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── label │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── select │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ └── textarea │ │ │ ├── index.jsx │ │ │ └── index.module.css │ ├── icon │ │ ├── .DS_Store │ │ ├── index.jsx │ │ └── index.module.css │ ├── layout │ │ ├── info │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ └── toolbar │ │ │ ├── index.jsx │ │ │ └── index.module.css │ ├── page │ │ ├── content │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── footer │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── header │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── index.jsx │ │ ├── index.module.css │ │ ├── pagination │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── path │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── subheader │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ └── theme.module.scss │ ├── picture │ │ ├── index.jsx │ │ └── index.module.css │ ├── popover │ │ ├── context.jsx │ │ ├── index.jsx │ │ └── index.module.css │ ├── raindrops │ │ ├── listing │ │ │ ├── index.jsx │ │ │ ├── index.module.css │ │ │ └── useInfiniteScroll.js │ │ ├── path │ │ │ └── index.jsx │ │ ├── single │ │ │ ├── add │ │ │ │ ├── index.jsx │ │ │ │ └── index.module.css │ │ │ ├── cover │ │ │ │ ├── index.jsx │ │ │ │ └── index.module.css │ │ │ ├── creator │ │ │ │ └── index.jsx │ │ │ ├── highlights │ │ │ │ ├── highlight │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── index.module.css │ │ │ │ ├── index.jsx │ │ │ │ └── index.module.css │ │ │ ├── important │ │ │ │ └── index.jsx │ │ │ ├── index.jsx │ │ │ ├── index.module.css │ │ │ ├── path │ │ │ │ ├── index.jsx │ │ │ │ └── index.module.css │ │ │ ├── tags │ │ │ │ ├── index.jsx │ │ │ │ └── tag │ │ │ │ │ └── index.jsx │ │ │ └── type │ │ │ │ ├── index.jsx │ │ │ │ └── index.module.css │ │ └── sort │ │ │ └── index.jsx │ └── search │ │ ├── field │ │ ├── index.jsx │ │ └── index.module.css │ │ ├── hooks │ │ ├── index.jsx │ │ └── useFilterHref.jsx │ │ └── tags │ │ ├── index.jsx │ │ ├── index.module.css │ │ └── tag │ │ └── index.jsx ├── config │ ├── api.js │ ├── links.js │ ├── raindrops.js │ └── vendors.js ├── modules │ ├── .DS_Store │ ├── async │ │ ├── index.js │ │ ├── pause.js │ │ └── timeout.js │ ├── browser │ │ ├── copyText.js │ │ └── index.js │ ├── format │ │ ├── callback │ │ │ └── debounce.js │ │ ├── date │ │ │ ├── index.js │ │ │ ├── parse.js │ │ │ └── short.jsx │ │ └── url │ │ │ ├── domain.js │ │ │ ├── index.js │ │ │ └── query.js │ └── router │ │ ├── index.js │ │ └── linkFactory.js ├── pages │ ├── _app │ │ ├── app.css │ │ └── index.jsx │ ├── _error │ │ └── _error.page.jsx │ ├── api │ │ ├── oembed │ │ │ ├── collection.js │ │ │ ├── index.page.js │ │ │ └── user.js │ │ └── ogimage │ │ │ └── index.page.js │ ├── collection │ │ ├── embed │ │ │ ├── index.page.jsx │ │ │ ├── index.page.route.js │ │ │ ├── options.page.jsx │ │ │ └── options.page.route.js │ │ ├── empty-slug.page.js │ │ ├── empty-slug.page.route.js │ │ ├── search │ │ │ ├── index.page.jsx │ │ │ ├── index.page.route.js │ │ │ ├── options.page.jsx │ │ │ └── options.page.route.js │ │ ├── share │ │ │ ├── index.page.jsx │ │ │ ├── index.page.route.js │ │ │ ├── options.page.jsx │ │ │ └── options.page.route.js │ │ └── view │ │ │ ├── index.page.jsx │ │ │ ├── index.page.route.js │ │ │ ├── options.page.jsx │ │ │ └── options.page.route.js │ ├── legacy │ │ ├── byId │ │ │ ├── collection.page.js │ │ │ ├── collection.page.route.js │ │ │ ├── user.page.js │ │ │ └── user.page.route.js │ │ └── obsolete │ │ │ ├── collection-id-section.page.js │ │ │ ├── collection-id-section.page.route.js │ │ │ ├── collection-view-options.page.js │ │ │ ├── collection-view-options.page.route.js │ │ │ ├── collection-view.page.js │ │ │ └── collection-view.page.route.js │ └── user │ │ ├── embed │ │ ├── index.page.jsx │ │ ├── index.page.route.js │ │ ├── options.page.jsx │ │ └── options.page.route.js │ │ ├── home │ │ ├── index.page.jsx │ │ └── index.page.route.js │ │ └── share │ │ ├── index.page.jsx │ │ ├── index.page.route.js │ │ ├── options.page.jsx │ │ └── options.page.route.js ├── public │ ├── favicon.ico │ └── icon_128.png └── renderer │ ├── _default.page.client.jsx │ └── _default.page.server.jsx ├── vite.config.js ├── worker ├── index.js ├── ssr.js ├── static-assets.js └── webpack.config.js └── wrangler.toml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raindropio/pages/d012250750984f7559ff742dd5bcc6e297e8a9e1/.DS_Store -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "plugins": [ 9 | "react", 10 | "react-hooks" 11 | ], 12 | "rules": { 13 | "no-inner-declarations": "off", 14 | "no-unused-vars": 1, 15 | "no-debugger": 0, 16 | "no-console": 0, 17 | "new-cap": 0, 18 | "strict": 0, 19 | "no-underscore-dangle": 0, 20 | "no-use-before-define": 0, 21 | "eol-last": 0, 22 | "quotes": [1, "single"], 23 | "jsx-quotes": [1, "prefer-single"], 24 | "react/jsx-no-undef": 1, 25 | "react/jsx-uses-react": 1, 26 | "react/jsx-uses-vars": 1, 27 | "no-empty": ["error", { 28 | "allowEmptyCatch": true 29 | }] 30 | }, 31 | "globals": { 32 | "Promise": true, 33 | "__TARGET__": true, 34 | "RAINDROP_ENVIRONMENT": true, 35 | "Set": true, 36 | "Map": true, 37 | "browser": true 38 | } 39 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node 2 | node_modules 3 | 4 | # outputs 5 | /worker/worker/ 6 | /dist/ 7 | .DS_Store -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.0.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raindrop.io Public Pages for Collection / User 2 | 3 | USE NODE 16 TO DEPLOY 4 | 5 | ## Implementation 6 | Build with `vite` & `vite-ssr-plugin`. Can be run on any serverless environment. 7 | But optimized for Cloudflare Worker for now. 8 | 9 | **Folder structure** 10 | - etc/dev-server Used only for local development, mimics Cloudflare Worker environment 11 | - src 12 | - pages 13 | - public Files that will be copied to production root folder 14 | - _app Like next.js 15 | - _error Like next.js 16 | - renderer Different root index files for client/server env 17 | - worker Source of Cloudflare Worker 18 | - wrangler.toml Cloudflare Worker specific 19 | 20 | ## Todo 21 | [] BaseURL 22 | [] Sentry 23 | [] Loading indicator -------------------------------------------------------------------------------- /etc/dev-server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const { createPageRenderer } = require('vite-plugin-ssr') 3 | const vite = require('vite') 4 | 5 | const isProduction = process.env.NODE_ENV === 'production' 6 | const root = `${__dirname}/..` 7 | 8 | global.fetch = require('node-fetch') 9 | 10 | startServer() 11 | 12 | async function startServer() { 13 | const app = express() 14 | 15 | let viteDevServer 16 | if (isProduction) 17 | app.use(express.static(`${root}/dist/client`)) 18 | else { 19 | viteDevServer = await vite.createServer({ 20 | root, 21 | server: { middlewareMode: true }, 22 | }) 23 | 24 | app.use(viteDevServer.middlewares) 25 | } 26 | 27 | const renderPage = createPageRenderer({ viteDevServer, isProduction, root }) 28 | app.get('*', async (req, res, next) => { 29 | const { httpResponse, statusCode, headers={}, redirect, json, proxy } = await renderPage({ 30 | url: req.originalUrl 31 | }) 32 | 33 | //remove caching headers for dev 34 | if (!isProduction) 35 | for(const i of Object.keys(headers)) 36 | if (i.toLowerCase() == 'cache-control') 37 | delete headers[i] 38 | 39 | if (redirect) { 40 | return res.redirect(statusCode||302, redirect) 41 | } else if (proxy) { 42 | const r = await fetch(proxy) 43 | r.body.pipe(res) 44 | return res 45 | .status(r.status) 46 | .type(r.headers.get('content-type')) 47 | } else if (json) 48 | return res 49 | .status(statusCode || 200) 50 | .set(headers) 51 | .json(json) 52 | else if (!httpResponse) 53 | return next() 54 | 55 | res 56 | .status(statusCode || httpResponse.statusCode) 57 | .type(httpResponse.contentType) 58 | .set(headers) 59 | .send(httpResponse.body) 60 | }) 61 | 62 | const port = 80 63 | app.listen(port) 64 | console.log(`Server running at http://localhost:${port}`) 65 | } 66 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~*": ["src/*"] 6 | } 7 | }, 8 | "include": ["src/**/*"] 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pages", 3 | "version": "0.0.0", 4 | "imports": { 5 | "~*": "./src/*.js" 6 | }, 7 | "scripts": { 8 | "local": "node ./etc/dev-server", 9 | "preview": "npm run build:vite && sudo wrangler dev --port 80", 10 | "deploy:prod": "npm run build:vite && DEBUG=wrangler:* wrangler publish --env production --verbose", 11 | "build:vite": "vite build && vite build --ssr" 12 | }, 13 | "dependencies": { 14 | "@cloudflare/kv-asset-handler": "0.2.x", 15 | "@cloudflare/wrangler": "^1.19.5", 16 | "@vitejs/plugin-react": "1.1.x", 17 | "color-convert": "^2.0.1", 18 | "date-fns": "2.x", 19 | "express": "4.x", 20 | "lodash-es": "4.x", 21 | "markdown-to-jsx": "^7.2.0", 22 | "modern-normalize": "1.1.x", 23 | "react": "17.x", 24 | "react-dom": "17.x", 25 | "react-helmet": "6.x", 26 | "react-portal": "^4.2.1", 27 | "vite": "2.6.x", 28 | "vite-plugin-ssr": "0.3.31", 29 | "vite-plugin-svgr": "0.6.x" 30 | }, 31 | "devDependencies": { 32 | "eslint": "7.x", 33 | "eslint-plugin-react": "7.x", 34 | "eslint-plugin-react-hooks": "4.x", 35 | "node-fetch": "2.x", 36 | "sass": "1.43.x" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raindropio/pages/d012250750984f7559ff742dd5bcc6e297e8a9e1/src/.DS_Store -------------------------------------------------------------------------------- /src/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raindropio/pages/d012250750984f7559ff742dd5bcc6e297e8a9e1/src/api/.DS_Store -------------------------------------------------------------------------------- /src/api/collection/get.js: -------------------------------------------------------------------------------- 1 | import { API_ENDPOINT } from '~config/api' 2 | import { FetchError } from '../errors' 3 | 4 | export async function get(id) { 5 | if (typeof id == 'undefined') 6 | throw new FetchError(404) 7 | 8 | const res = await fetch(`${API_ENDPOINT}/collection/${id}`) 9 | if (!res.ok) 10 | throw new FetchError(res.status, res.statusText) 11 | 12 | const { result, item } = await res.json() 13 | 14 | if (!result) 15 | return null 16 | 17 | item.slug = item.slug || 'a' 18 | 19 | return item 20 | } -------------------------------------------------------------------------------- /src/api/collection/index.js: -------------------------------------------------------------------------------- 1 | export * from './get' 2 | -------------------------------------------------------------------------------- /src/api/collections/get.js: -------------------------------------------------------------------------------- 1 | import { API_ENDPOINT } from '~config/api' 2 | import { FetchError } from '../errors' 3 | 4 | export async function getByUserId(userId, options={}) { 5 | const params = new URLSearchParams(options) 6 | 7 | const res = await fetch(`${API_ENDPOINT}/collections/${String(userId)}?${params.toString()}`) 8 | if (!res.ok) 9 | throw new FetchError(res.status, res.statusText) 10 | 11 | const { result, items } = await res.json() 12 | 13 | if (!result) 14 | return [] 15 | 16 | return items 17 | .map(item=>({ 18 | ...item, 19 | slug: item.slug || 'a' 20 | })) 21 | } 22 | 23 | export async function getByUserName(user_name, options={}) { 24 | const params = new URLSearchParams(options) 25 | 26 | const res = await fetch(`${API_ENDPOINT}/collections/username/${String(user_name)}?${params.toString()}`) 27 | if (!res.ok) 28 | throw new FetchError(res.status, res.statusText) 29 | 30 | const { result, items } = await res.json() 31 | 32 | if (!result) 33 | return [] 34 | 35 | return items 36 | .map(item=>({ 37 | ...item, 38 | slug: item.slug || 'a' 39 | })) 40 | } -------------------------------------------------------------------------------- /src/api/collections/index.js: -------------------------------------------------------------------------------- 1 | export * from './get' -------------------------------------------------------------------------------- /src/api/errors.js: -------------------------------------------------------------------------------- 1 | export class FetchError extends Error { 2 | constructor(status, message, ...params) { 3 | super(...params) 4 | 5 | if (Error.captureStackTrace) 6 | Error.captureStackTrace(this, FetchError) 7 | 8 | this.name = 'FetchError' 9 | this.status = status 10 | this.message = message || status 11 | this.date = new Date() 12 | } 13 | } -------------------------------------------------------------------------------- /src/api/filters/get.js: -------------------------------------------------------------------------------- 1 | import { API_ENDPOINT } from '~config/api' 2 | import { FetchError } from '../errors' 3 | import { optionsToQueryString } from '~api/raindrops/get' 4 | 5 | export async function get(id, _options) { 6 | const { sort, perpage, ...options } = _options 7 | const res = await fetch(`${API_ENDPOINT}/filters/${id}?${optionsToQueryString(options)}`) 8 | if (!res.ok) 9 | throw new FetchError(res.status, res.statusText) 10 | 11 | const { result, ...items } = await res.json() 12 | 13 | if (!result) 14 | return [] 15 | 16 | return items 17 | } -------------------------------------------------------------------------------- /src/api/filters/index.js: -------------------------------------------------------------------------------- 1 | export * from './get' -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import * as collection from './collection' 2 | import * as collections from './collections' 3 | import * as filters from './filters' 4 | import * as raindrops from './raindrops' 5 | import * as user from './user' 6 | 7 | export default { 8 | collection, 9 | collections, 10 | filters, 11 | raindrops, 12 | user 13 | } -------------------------------------------------------------------------------- /src/api/raindrops/get.js: -------------------------------------------------------------------------------- 1 | import { API_ENDPOINT } from '~config/api' 2 | import { FetchError } from '../errors' 3 | 4 | export function optionsToQueryString(options={}) { 5 | const params = new URLSearchParams(options) 6 | params.set('version', 2) 7 | 8 | //nested 9 | if (params.get('nested') === 'false') 10 | params.delete('nested') 11 | else 12 | params.set('nested', true) 13 | 14 | return params.toString() 15 | } 16 | 17 | export async function get(id, options={}) { 18 | const res = await fetch(`${API_ENDPOINT}/raindrops/${id}?${optionsToQueryString(options)}`) 19 | if (!res.ok) 20 | throw new FetchError(res.status, res.statusText) 21 | 22 | const { result, items, count=0 } = await res.json() 23 | 24 | if (!result) 25 | return { 26 | items: [], 27 | count: 0 28 | } 29 | 30 | return { 31 | items, 32 | count 33 | } 34 | } -------------------------------------------------------------------------------- /src/api/raindrops/index.js: -------------------------------------------------------------------------------- 1 | export * from './get' -------------------------------------------------------------------------------- /src/api/user/get.js: -------------------------------------------------------------------------------- 1 | import { API_ENDPOINT } from '~config/api' 2 | import { FetchError } from '../errors' 3 | 4 | //id or name 5 | export async function getById(id) { 6 | const res = await fetch(`${API_ENDPOINT}/user/${String(id)}`) 7 | if (!res.ok) 8 | throw new FetchError(res.status, res.statusText) 9 | 10 | const { result, user } = await res.json() 11 | 12 | if (!result) 13 | return null 14 | 15 | return user 16 | } 17 | 18 | export async function getByName(name) { 19 | const res = await fetch(`${API_ENDPOINT}/user/name/${String(name)}`) 20 | if (!res.ok) 21 | throw new FetchError(res.status, res.statusText) 22 | 23 | const { result, user } = await res.json() 24 | 25 | if (!result) 26 | return null 27 | 28 | return user 29 | } -------------------------------------------------------------------------------- /src/api/user/index.js: -------------------------------------------------------------------------------- 1 | export * from './get' -------------------------------------------------------------------------------- /src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raindropio/pages/d012250750984f7559ff742dd5bcc6e297e8a9e1/src/assets/.DS_Store -------------------------------------------------------------------------------- /src/assets/brand/icon_48.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/co/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raindropio/pages/d012250750984f7559ff742dd5bcc6e297e8a9e1/src/co/.DS_Store -------------------------------------------------------------------------------- /src/co/badge/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | 3 | export default function Badge({ className='', variant, ...etc }) { 4 | return ( 5 |
9 | ) 10 | } -------------------------------------------------------------------------------- /src/co/badge/index.module.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | display: inline-block; 3 | font-size: var(--font-size-micro); 4 | font-weight: 600; 5 | color: var(--background-color-regular); 6 | 7 | height: 1.3em; 8 | line-height: 1.3em; 9 | padding: 0 var(--padding-small); 10 | border-radius: var(--border-radius-regular); 11 | } 12 | 13 | .badge[data-variant='disabled'] { 14 | background: var(--text-color-secondary); 15 | opacity: .5; 16 | } -------------------------------------------------------------------------------- /src/co/button/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import { forwardRef } from 'react' 3 | import Icon, { Logo, Avatar } from '~co/icon' 4 | 5 | export * from './select' 6 | export * from './share' 7 | 8 | export function Base({ as='a', className='', variant, color, size, bold=false, disabled=false, inline=true, forwardedRef, ...props }) { 9 | const Component = as 10 | 11 | return ( 12 | 22 | ) 23 | } 24 | 25 | export default forwardRef((props, ref) => { 26 | return 27 | }) 28 | 29 | export function Buttons({ className='', tight=false, ...etc }) { 30 | return ( 31 |
32 | ) 33 | } -------------------------------------------------------------------------------- /src/co/button/index.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | user-select: none; 3 | appearance: none !important; 4 | background: transparent; 5 | border: 0; 6 | cursor: pointer; 7 | white-space: nowrap; 8 | flex-shrink: 0; 9 | max-width: 100%; 10 | vertical-align: middle; 11 | 12 | height: var(--this-button-size); 13 | line-height: var(--this-button-size); 14 | border-radius: var(--border-radius-large); 15 | 16 | display: inline-grid; 17 | grid-auto-flow: column; 18 | grid-gap: var(--padding-regular); 19 | align-items: center; 20 | justify-content: start; 21 | 22 | padding: 0 var(--padding-regular); 23 | transition: background .1s linear, box-shadow .1s linear, filter .1s linear; 24 | } 25 | .button[data-inline="false"] { 26 | display: grid; 27 | justify-content: center; 28 | } 29 | 30 | .button:active { 31 | filter: brightness(90%); 32 | } 33 | 34 | /* size */ 35 | .button[data-size='regular'] { 36 | --this-button-size: var(--button-size-regular) 37 | } 38 | 39 | .button[data-size='small'] { 40 | --this-button-size: var(--button-size-small); 41 | font-size: var(--font-size-small); 42 | } 43 | 44 | .button[data-size='large'] { 45 | --this-button-size: var(--button-size-large); 46 | } 47 | 48 | .button[data-single-icon='true'] { 49 | contain: strict; 50 | padding: 0; 51 | justify-content: center; 52 | width: var(--this-button-size); 53 | } 54 | 55 | /* colors */ 56 | .button[data-color='secondary'] { 57 | color: var(--text-color-secondary); 58 | } 59 | 60 | .button[data-color='regular'] { 61 | color: var(--text-color-regular); 62 | } 63 | 64 | .button[data-color='accent'] { 65 | color: var(--accent-color); 66 | } 67 | 68 | /* variants */ 69 | .button[data-variant='regular'] { 70 | background: var(--background-color-regular); 71 | box-shadow: inset 0 0 0 var(--line-size) var(--line-color); 72 | } 73 | .button[data-variant='regular']:hover, 74 | .button[data-variant='ghost']:hover { 75 | box-shadow: inset 0 0 0 var(--line-size) var(--line-color), 0 var(--line-size) 2px rgba(0,0,0,.3); 76 | } 77 | 78 | .button[data-variant='ghost'] { 79 | background: var(--background-color-secondary); 80 | } 81 | 82 | .button[data-variant='regular']:active, 83 | .button[data-variant='ghost']:active, 84 | .button[data-variant='flat']:hover { 85 | background: var(--background-color-secondary); 86 | box-shadow: 0 0 0 2px var(--background-color-secondary); 87 | } 88 | 89 | .button[data-size='small'][data-variant='flat'] { 90 | padding-left: 0; 91 | padding-right: 0; 92 | border-radius: var(--border-radius-regular); 93 | } 94 | 95 | .button[data-variant='active'] { 96 | background: var(--accent-color); 97 | color: var(--background-color-regular); 98 | } 99 | 100 | .button[data-variant='disabled'] { 101 | pointer-events: none; 102 | background: var(--background-color-disabled); 103 | color: var(--text-color-disabled); 104 | } 105 | 106 | /* modifiers */ 107 | .button[data-bold='true'] { 108 | font-weight: 600; 109 | } 110 | 111 | /* group */ 112 | .buttons { 113 | --buttons-gap: calc(var(--padding-regular) + var(--padding-small)); 114 | 115 | display: flex; 116 | flex-wrap: wrap; 117 | align-content: flex-start; 118 | 119 | margin-bottom: calc(var(--buttons-gap) * -1); 120 | margin-right: calc(var(--buttons-gap) * -1); 121 | } 122 | 123 | .buttons > * { 124 | margin: var(--buttons-gap); 125 | margin-top: 0; 126 | margin-left: 0; 127 | } 128 | 129 | .buttons[data-tight='true'] { 130 | --buttons-gap: var(--padding-regular) 131 | } -------------------------------------------------------------------------------- /src/co/button/select.jsx: -------------------------------------------------------------------------------- 1 | import s from './select.module.css' 2 | import React, { useCallback } from 'react' 3 | import { Base } from './index' 4 | import Icon from '~co/icon' 5 | 6 | /* 7 | options = [ { value, label, ..anything } ] 8 | selected = value 9 | children = function({ value, label, ...}=>render) optional 10 | onChange = functioon(value) 11 | */ 12 | export function Select({ className='', options=[], selected, children, onChange, ...etc }) { 13 | const active = options.find(({value})=>(value||undefined) == (selected||undefined)) 14 | 15 | const onNativeChange = useCallback(e=>{ 16 | e.preventDefault() 17 | onChange && onChange(e.target.value) 18 | }, [onChange]) 19 | 20 | return ( 21 | 25 | {!!active && (children ? 26 | children(active) : 27 | ( 28 | <> 29 | {active.label} 30 | 33 | 34 | ) 35 | )} 36 | 37 | 53 | 54 | ) 55 | } -------------------------------------------------------------------------------- /src/co/button/select.module.css: -------------------------------------------------------------------------------- 1 | .select { 2 | position: relative; 3 | overflow: initial; /* fix firefox bug */ 4 | border-radius: var(--border-radius-regular); 5 | } 6 | .select:focus-within { 7 | outline-style: auto; 8 | } 9 | 10 | .select select { 11 | opacity: 0; 12 | position: absolute; 13 | top: 0; 14 | right: 0; 15 | left: 0; 16 | bottom: 0; 17 | width: 100%; 18 | height: 100%; 19 | appearance: none; 20 | } 21 | 22 | .dropDownIcon { 23 | color: var(--text-color-secondary); 24 | } -------------------------------------------------------------------------------- /src/co/button/share.jsx: -------------------------------------------------------------------------------- 1 | import { TWITTER_ID, FACEBOOK_APP_ID } from '~config/vendors' 2 | import { copyText } from '~modules/browser' 3 | 4 | import Button from './index' 5 | import Icon from '~co/icon' 6 | 7 | export function Share({ url, title }) { 8 | return [ 9 | , 19 | 20 | , 30 | 31 | , 41 | 42 | , 49 | 50 | , 57 | ] 58 | } -------------------------------------------------------------------------------- /src/co/collections/compact/index.jsx: -------------------------------------------------------------------------------- 1 | import { Buttons } from '~co/button' 2 | import Single from './single' 3 | 4 | export default function CollectionsCompact({ items, user }) { 5 | if (!items.length) 6 | return null 7 | 8 | return ( 9 | 10 | {items.map(item=>( 11 | 15 | ))} 16 | 17 | ) 18 | } -------------------------------------------------------------------------------- /src/co/collections/compact/single.jsx: -------------------------------------------------------------------------------- 1 | import Button from '~co/button' 2 | import Cover from '../cover' 3 | 4 | export default function CollectionsCompactSingle({ item, user, ...etc }) { 5 | return ( 6 | 17 | ) 18 | } -------------------------------------------------------------------------------- /src/co/collections/cover/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import Icon, { Image } from '~co/icon' 3 | 4 | export default function CollectionCover({ className='', cover, title, size, fallback=true }) { 5 | if (!Array.isArray(cover) || 6 | !cover.length) 7 | return fallback ? ( 8 | 12 | ) : null 13 | 14 | return ( 15 | {title} 20 | ) 21 | } -------------------------------------------------------------------------------- /src/co/collections/cover/index.module.css: -------------------------------------------------------------------------------- 1 | .fallback { 2 | color: var(--text-color-secondary); 3 | text-decoration: none; 4 | } -------------------------------------------------------------------------------- /src/co/collections/hooks/index.jsx: -------------------------------------------------------------------------------- 1 | export * from './useChildrens' 2 | export * from './useRoot' 3 | export * from './useParents' -------------------------------------------------------------------------------- /src/co/collections/hooks/useChildrens.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import sortBy from 'lodash-es/sortBy' 3 | 4 | export function useChildrens(items, collection) { 5 | return useMemo(()=> 6 | sortBy( 7 | items.filter(c=>c.parent?.$id == collection._id), 8 | ['sort'] 9 | ), 10 | items 11 | ) 12 | } -------------------------------------------------------------------------------- /src/co/collections/hooks/useParents.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | export function useParents(collections=[], collection, self) { 4 | return useMemo(()=>{ 5 | const parents = [] 6 | 7 | if (!collection) 8 | return parents 9 | 10 | const find = (findId)=>{ 11 | const parent = collections.find(({_id})=>_id == findId) 12 | 13 | if (parent){ 14 | parents.unshift(parent) 15 | 16 | if (parent.parent?.$id) 17 | find(parent.parent.$id) 18 | } 19 | } 20 | find(collection.parent?.$id) 21 | 22 | if (self) 23 | parents.push(collection) 24 | 25 | return parents 26 | }, [collections, collection]) 27 | } -------------------------------------------------------------------------------- /src/co/collections/hooks/useRoot.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import sortBy from 'lodash-es/sortBy' 3 | 4 | export function useRoot(items) { 5 | return useMemo(()=> 6 | sortBy( 7 | items.filter(({parent})=>{ 8 | if (parent) 9 | return !items.find(({_id})=>_id==parent.$id) 10 | 11 | return true 12 | }), 13 | ['title'] 14 | ), 15 | items 16 | ) 17 | } -------------------------------------------------------------------------------- /src/co/collections/listing/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import Single from './single' 3 | 4 | export default function CollectionsListing({ items, user, target }) { 5 | if (!items.length) 6 | return null 7 | 8 | return ( 9 | 19 | ) 20 | } -------------------------------------------------------------------------------- /src/co/collections/listing/index.module.css: -------------------------------------------------------------------------------- 1 | .listing { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fill, minmax(min(100%, 250px), 1fr)); 4 | grid-template-rows: 1fr; 5 | margin: 0 calc(var(--padding-large) * -1); 6 | } 7 | 8 | @media screen and (max-width: 500px) { 9 | .listing { 10 | grid-template-columns: 50vw 50vw; 11 | } 12 | } -------------------------------------------------------------------------------- /src/co/collections/listing/single/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import { useMemo } from 'react' 3 | import colorConvert from 'color-convert' 4 | 5 | import Cover from '../../cover' 6 | 7 | export default function CollectionsSingle({ item, user, target }) { 8 | const folderStyle = useMemo(()=> 9 | item.color ? { 10 | '--bg-rgb': colorConvert.hex.rgb(item.color.replace('#','')) 11 | } : undefined, 12 | [item.color] 13 | ) 14 | 15 | return ( 16 | 21 | 25 | 29 | 30 | 31 | 32 | {item.title} 33 | 34 | 35 | ) 36 | } -------------------------------------------------------------------------------- /src/co/collections/listing/single/index.module.css: -------------------------------------------------------------------------------- 1 | a.single { 2 | display: inline-grid; 3 | grid-gap: var(--padding-regular); 4 | align-content: flex-start; 5 | 6 | color: var(--text-color-regular); 7 | padding: var(--padding-large); 8 | } 9 | 10 | .folder { 11 | border-radius: var(--border-radius-regular); 12 | position: relative; 13 | 14 | background: var(--background-color-secondary); 15 | transition: background .1s linear; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | aspect-ratio: 5 / 3; 20 | } 21 | 22 | @supports not (aspect-ratio: 1/1) { 23 | .folder { 24 | height: 180px; 25 | } 26 | } 27 | 28 | .folder[data-custom-bg='true'] { 29 | background: rgba(var(--bg-rgb), .08); 30 | } 31 | 32 | .single:hover .folder[data-custom-bg='true'] { 33 | background: rgba(var(--bg-rgb), .15); 34 | } 35 | 36 | .cover { 37 | display: block; 38 | } 39 | 40 | .title { 41 | font-weight: 600; 42 | } -------------------------------------------------------------------------------- /src/co/form/checkbox/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import { useCallback, useContext } from 'react' 3 | import { Context } from '../form' 4 | import Icon from '~co/icon' 5 | 6 | export function Checkbox({ className='', children, ...etc }) { 7 | const { values, onChange } = useContext(Context) 8 | const checked = values[etc.name] 9 | 10 | return ( 11 | 31 | ) 32 | } -------------------------------------------------------------------------------- /src/co/form/checkbox/index.module.css: -------------------------------------------------------------------------------- 1 | .label { 2 | display: inline-grid; 3 | grid-template-areas: 'checkbox title'; 4 | grid-template-columns: min-content max-content; 5 | grid-gap: var(--padding-regular); 6 | align-items: center; 7 | height: var(--button-size-regular); 8 | cursor: pointer; 9 | } 10 | 11 | .checkbox { 12 | grid-area: checkbox; 13 | position: relative; 14 | color: var(--text-color-secondary); 15 | } 16 | 17 | .checkbox > * { 18 | pointer-events: none; 19 | } 20 | 21 | .checkbox input { 22 | cursor: pointer; 23 | pointer-events: auto; 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | right: 0; 28 | bottom: 0; 29 | width: 100%; 30 | height: 100%; 31 | appearance: none; 32 | } 33 | 34 | .checkbox .icon { 35 | display: block; 36 | } 37 | 38 | .label[data-checked='true'] .checkbox { 39 | color: var(--accent-color) 40 | } 41 | 42 | .title { 43 | grid-area: title; 44 | } -------------------------------------------------------------------------------- /src/co/form/fields/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | 3 | export function Fields({ className='', inset=false, ...etc }) { 4 | return ( 5 |
9 | ) 10 | } -------------------------------------------------------------------------------- /src/co/form/fields/index.module.css: -------------------------------------------------------------------------------- 1 | .fields { 2 | display: grid; 3 | grid-auto-flow: row; 4 | grid-gap: var(--padding-regular); 5 | } 6 | 7 | .fields[data-inset='true'] { 8 | padding: var(--padding-regular); 9 | background-color: var(--background-color-secondary); 10 | border-radius: var(--border-radius-regular); 11 | } -------------------------------------------------------------------------------- /src/co/form/form/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import { createContext, useMemo, useCallback } from 'react' 3 | 4 | export const Context = createContext({ 5 | values: {} 6 | }) 7 | 8 | export default function Form({ className='', value, onChange, onSubmit, ...etc }) { 9 | const onFieldChange = useCallback(e=>{ 10 | const elem = e.currentTarget 11 | const fieldName = elem.name 12 | const fieldValue = elem.type == 'checkbox' ? (elem.checked||false) : elem.value 13 | 14 | onChange({ 15 | ...value, 16 | [fieldName]: fieldValue 17 | }) 18 | }, [value, onChange]) 19 | 20 | const onFormSubmit = useCallback(e=>{ 21 | e.preventDefault() 22 | if (onSubmit) onSubmit(e) 23 | }, [onSubmit]) 24 | 25 | const context = useMemo(()=>{ 26 | let values = {...value} 27 | for(const i in values) 28 | if (values[i]=='true') 29 | values[i] = true 30 | else if (values[i]=='false') 31 | values[i] = false 32 | 33 | return { 34 | values, 35 | onChange: onFieldChange 36 | } 37 | }, [value]) 38 | 39 | return ( 40 | 41 |
45 | 46 | ) 47 | } -------------------------------------------------------------------------------- /src/co/form/form/index.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | display: grid; 3 | grid-template-columns: max-content 1fr; 4 | grid-gap: var(--padding-large); 5 | padding: var(--padding-regular) 0; 6 | } 7 | 8 | .form > * { 9 | grid-column: 2 / -1; 10 | } -------------------------------------------------------------------------------- /src/co/form/index.jsx: -------------------------------------------------------------------------------- 1 | import Form from './form' 2 | export * from './label' 3 | export * from './fields' 4 | export * from './checkbox' 5 | export * from './input' 6 | export * from './select' 7 | export * from './textarea' 8 | 9 | export default Form -------------------------------------------------------------------------------- /src/co/form/input/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import { useContext } from 'react' 3 | import { Context } from '../form' 4 | 5 | export function Input({ className='', ...etc }) { 6 | const { values, onChange } = useContext(Context) 7 | 8 | return ( 9 | 15 | ) 16 | } -------------------------------------------------------------------------------- /src/co/form/input/index.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | padding: var(--padding-regular); 3 | height: var(--button-size-regular); 4 | background: var(--background-color-secondary); 5 | border: 0; 6 | border-radius: var(--border-radius-regular); 7 | color: var(--text-color-regular); 8 | } -------------------------------------------------------------------------------- /src/co/form/label/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | 3 | export function Label({ className='', ...etc }) { 4 | return ( 5 |
8 | ) 9 | } -------------------------------------------------------------------------------- /src/co/form/label/index.module.css: -------------------------------------------------------------------------------- 1 | .label { 2 | grid-column: 1; 3 | text-align: right; 4 | font-size: var(--font-size-small); 5 | color: var(--text-color-secondary); 6 | padding-top: var(--padding-small); 7 | padding-left: var(--padding-large); 8 | 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | gap: var(--padding-small); 13 | } -------------------------------------------------------------------------------- /src/co/form/select/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import { useCallback, useContext } from 'react' 3 | import { Context } from '../form' 4 | import { Select as ButtonSelect } from '~co/button' 5 | 6 | export function Select({ className='', ...etc }) { 7 | const { values, onChange } = useContext(Context) 8 | const selected = values[etc.name] 9 | const onSelectChange = useCallback(value=>{ 10 | onChange({ 11 | currentTarget: { 12 | name: etc.name, 13 | value 14 | } 15 | }) 16 | }, [onChange]) 17 | 18 | return ( 19 | 25 | ) 26 | } -------------------------------------------------------------------------------- /src/co/form/select/index.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raindropio/pages/d012250750984f7559ff742dd5bcc6e297e8a9e1/src/co/form/select/index.module.css -------------------------------------------------------------------------------- /src/co/form/textarea/index.jsx: -------------------------------------------------------------------------------- 1 | import s from './index.module.css' 2 | import { useCallback, useContext } from 'react' 3 | import { Context } from '../form' 4 | 5 | export function Textarea({ className='', ...etc }) { 6 | const { values, onChange } = useContext(Context) 7 | const onFocus = useCallback(e=>{ 8 | if (etc.readOnly) 9 | e.target.select() 10 | 11 | if (etc.onFocus) 12 | etc.onFocus(e) 13 | }) 14 | 15 | return ( 16 |