├── .prettierignore ├── components ├── tags │ ├── index.ts │ └── Tags.tsx ├── button │ ├── index.ts │ └── Button.tsx ├── social │ ├── index.ts │ └── Social.tsx ├── talks │ ├── index.ts │ └── Talks.tsx ├── profile │ ├── index.ts │ └── Profile.tsx ├── layout │ ├── Header.tsx │ ├── Footer.tsx │ ├── Layout.tsx │ └── assets │ │ └── algolia.svg ├── social-meta.tsx └── card │ ├── Cards.tsx │ └── Card.tsx ├── styles └── index.css ├── .babelrc ├── next-env.d.ts ├── postcss.config.js ├── og-image ├── public │ ├── favicon.ico │ └── index.html ├── fonts │ ├── Inter-UI-Bold.woff2 │ └── Inter-UI-Regular.woff2 ├── src │ ├── types.ts │ ├── sanitizer.ts │ ├── file.ts │ ├── chromium.ts │ ├── card.ts │ ├── parser.ts │ └── template.ts ├── tsconfig.json ├── package.json ├── LICENSE └── yarn.lock ├── utils ├── helpers.ts ├── algolia.ts └── analytics.ts ├── .prettierrc ├── tailwind.config.js ├── libs └── fetch.ts ├── apiHooks ├── useTwitter.ts └── useAlgolia.ts ├── .gitignore ├── pages ├── _app.tsx ├── api │ ├── speakers.ts │ ├── updateSpeakers.ts │ └── twitter.ts ├── index.tsx └── speaker │ └── [username].tsx ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md ├── config.ts ├── CODE_OF_CONDUCT.md └── data └── countries.ts /.prettierignore: -------------------------------------------------------------------------------- 1 | .build 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /components/tags/index.ts: -------------------------------------------------------------------------------- 1 | import Tags from './Tags'; 2 | 3 | export default Tags; 4 | -------------------------------------------------------------------------------- /styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["inline-react-svg"] 4 | } 5 | -------------------------------------------------------------------------------- /components/button/index.ts: -------------------------------------------------------------------------------- 1 | import Button from './Button'; 2 | 3 | export default Button; 4 | -------------------------------------------------------------------------------- /components/social/index.ts: -------------------------------------------------------------------------------- 1 | import Social from './Social'; 2 | 3 | export default Social; 4 | -------------------------------------------------------------------------------- /components/talks/index.ts: -------------------------------------------------------------------------------- 1 | import Talks from './Talks'; 2 | 3 | export default Talks; 4 | -------------------------------------------------------------------------------- /components/profile/index.ts: -------------------------------------------------------------------------------- 1 | import Profile from './Profile'; 2 | 3 | export default Profile; 4 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['tailwindcss', 'postcss-preset-env'], 3 | }; 4 | -------------------------------------------------------------------------------- /og-image/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nezdemkovski/confcitizens/HEAD/og-image/public/favicon.ico -------------------------------------------------------------------------------- /og-image/fonts/Inter-UI-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nezdemkovski/confcitizens/HEAD/og-image/fonts/Inter-UI-Bold.woff2 -------------------------------------------------------------------------------- /og-image/fonts/Inter-UI-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nezdemkovski/confcitizens/HEAD/og-image/fonts/Inter-UI-Regular.woff2 -------------------------------------------------------------------------------- /utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export const generateLocation = (city: string | null, country: string | null) => 2 | `${city ? city + ', ' : ''}${country}`; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./components/**/*.tsx', './pages/**/*.tsx'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | variants: {}, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /libs/fetch.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch'; 2 | 3 | export default async function (...args: any[]): Promise { 4 | // @ts-ignore 5 | const res = await fetch(...args); 6 | return res.json(); 7 | } 8 | -------------------------------------------------------------------------------- /apiHooks/useTwitter.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetch from '../libs/fetch'; 4 | 5 | const useTwitterImage = (username: string) => { 6 | return useSWR(`/api/twitter?username=${username}`, fetch); 7 | }; 8 | 9 | export default useTwitterImage; 10 | -------------------------------------------------------------------------------- /og-image/src/types.ts: -------------------------------------------------------------------------------- 1 | export type FileType = 'png' | 'jpeg'; 2 | export type Theme = 'light' | 'dark'; 3 | 4 | export interface ParsedRequest { 5 | fileType: FileType; 6 | text: string; 7 | theme: Theme; 8 | md: boolean; 9 | fontSize: string; 10 | images: string[]; 11 | tags: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editors 2 | .vscode 3 | .idea 4 | 5 | # build output 6 | dist 7 | .next 8 | 9 | # dependencies 10 | **/node_modules/** 11 | package-lock.json 12 | 13 | # testing 14 | /coverage 15 | 16 | # misc 17 | .DS_Store 18 | .env* 19 | !.env-sample 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .vercel -------------------------------------------------------------------------------- /og-image/src/sanitizer.ts: -------------------------------------------------------------------------------- 1 | const entityMap: { [key: string]: string } = { 2 | '&': '&', 3 | '<': '<', 4 | '>': '>', 5 | '"': '"', 6 | "'": ''', 7 | '/': '/', 8 | }; 9 | 10 | export function sanitizeHtml(html: string) { 11 | return String(html).replace(/[&<>"'\/]/g, key => entityMap[key]); 12 | } 13 | -------------------------------------------------------------------------------- /apiHooks/useAlgolia.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetch from '../libs/fetch'; 4 | 5 | const useAlgolia = (phrase: string, initialData: any = {}) => { 6 | return useSWR( 7 | `/api/speakers${phrase ? `?phrase=${phrase}` : ''}`, 8 | fetch, 9 | initialData, 10 | ); 11 | }; 12 | 13 | export default useAlgolia; 14 | -------------------------------------------------------------------------------- /utils/algolia.ts: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch'; 2 | 3 | import { 4 | ALGOLIA_APPLICATION_ID, 5 | ALGOLIA_INDEX_NAME, 6 | ALGOLIA_SEARCH_ONLY_API_KEY, 7 | } from '@config'; 8 | 9 | const client = algoliasearch( 10 | ALGOLIA_APPLICATION_ID, 11 | ALGOLIA_SEARCH_ONLY_API_KEY, 12 | ); 13 | const algolia = client.initIndex(ALGOLIA_INDEX_NAME); 14 | 15 | export default algolia; 16 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | 3 | import Layout from '@components/layout/Layout'; 4 | 5 | import '@styles'; 6 | 7 | const MyApp = ({ Component, pageProps }: AppProps) => ( 8 | <> 9 |

The website is under construction!

10 | 11 | 12 | 13 | 14 | ); 15 | 16 | export default MyApp; 17 | -------------------------------------------------------------------------------- /og-image/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "esnext", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "strict": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": true, 11 | "noEmitOnError": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "removeComments": true, 15 | "preserveConstEnums": true 16 | }, 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /components/tags/Tags.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Speaker } from '@speakers'; 4 | 5 | interface Props { 6 | tagList: Speaker['tags']; 7 | } 8 | 9 | const Tags: FC = ({ tagList }) => ( 10 |
11 | {tagList.map((tag, id) => ( 12 |
16 | {tag} 17 |
18 | ))} 19 |
20 | ); 21 | 22 | export default Tags; 23 | -------------------------------------------------------------------------------- /og-image/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Open Graph Image as a Service 11 | 12 | 13 | 14 |
15 |

Open Graph Image as a Service

16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /og-image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "og-image", 3 | "version": "1.0.0", 4 | "description": "Generate an open graph image for twitter/facebook/etc", 5 | "license": "MIT", 6 | "main": "src/card.ts", 7 | "dependencies": { 8 | "chrome-aws-lambda": "2.1.1", 9 | "marked": "^0.8.0", 10 | "puppeteer-core": "2.1.1" 11 | }, 12 | "devDependencies": { 13 | "@types/marked": "^0.7.2", 14 | "@types/puppeteer-core": "^2.0.0", 15 | "typescript": "^3.3.4000" 16 | }, 17 | "scripts": { 18 | "start": "node dist/card.js", 19 | "build": "tsc", 20 | "now-build": "tsc", 21 | "watch": "tsc --watch", 22 | "dev": "nodemon -e js,ts,tsx -x 'ts-node src/card.ts' -w src" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /og-image/src/file.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { writeFile } from 'fs'; 3 | import { tmpdir } from 'os'; 4 | import { join } from 'path'; 5 | import { promisify } from 'util'; 6 | const writeFileAsync = promisify(writeFile); 7 | 8 | export async function writeTempFile(name: string, contents: string) { 9 | const randomFile = randomBytes(32).toString('hex') + '.html'; 10 | const randomPath = join(tmpdir(), randomFile); 11 | console.log(`Writing file ${name} to ${randomPath}`); 12 | await writeFileAsync(randomPath, contents); 13 | return randomPath; 14 | } 15 | 16 | export function pathToFileURL(path: string) { 17 | const fileUrl = 'file://' + path; 18 | console.log('File url is ' + fileUrl); 19 | return fileUrl; 20 | } 21 | -------------------------------------------------------------------------------- /og-image/src/chromium.ts: -------------------------------------------------------------------------------- 1 | import * as chromeAws from 'chrome-aws-lambda'; 2 | import { launch, Page } from 'puppeteer-core'; 3 | 4 | import { FileType } from './types'; 5 | let _page: Page | null; 6 | 7 | async function getPage() { 8 | if (_page) { 9 | return _page; 10 | } 11 | const chrome = chromeAws as any; 12 | const browser = await launch({ 13 | args: chrome.args, 14 | executablePath: await chrome.executablePath, 15 | headless: chrome.headless, 16 | }); 17 | _page = await browser.newPage(); 18 | return _page; 19 | } 20 | 21 | export async function getScreenshot(url: string, type: FileType) { 22 | const page = await getPage(); 23 | await page.setViewport({ width: 2048, height: 1170 }); 24 | await page.goto(url); 25 | const file = await page.screenshot({ type }); 26 | return file; 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@components/*": ["components/*"], 19 | "@utils/*": ["utils/*"], 20 | "@lib/*": ["lib/*"], 21 | "@styles": ["styles/index.css"], 22 | "@apiHooks/*": ["apiHooks/*"], 23 | "@speakers": ["data/speakers.ts"], 24 | "@config": ["config.ts"] 25 | } 26 | }, 27 | "exclude": ["node_modules", "og-image"], 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 29 | } 30 | -------------------------------------------------------------------------------- /components/social/Social.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Speaker } from '@speakers'; 4 | import Button from '@components/Button'; 5 | 6 | interface Props { 7 | data: Speaker; 8 | } 9 | 10 | const Social: FC = ({ data }) => ( 11 |
12 | {data.social.blog &&
31 | ); 32 | 33 | export default Social; 34 | -------------------------------------------------------------------------------- /components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import GithubCorner from 'react-github-corner'; 3 | 4 | // const HeaderWrapper = styled.div` 5 | // text-align: center; 6 | // padding: 50px 0 25px; 7 | // background: #ffffff; 8 | // `; 9 | 10 | // const Logo = styled.h1` 11 | // font-weight: 700; 12 | // font-family: 'Chinese Quote', -apple-system, BlinkMacSystemFont, 'Segoe UI', 13 | // 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', 14 | // Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 15 | // 'Segoe UI Symbol'; 16 | // cursor: pointer; 17 | // user-select: none; 18 | // `; 19 | 20 | const Header = () => ( 21 |
22 | 23 |

ConfCitizens

24 | 25 |

Open-source and crowd-sourced conference speakers website

26 | 27 |
28 | ); 29 | 30 | export default Header; 31 | -------------------------------------------------------------------------------- /components/social-meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | interface Props { 4 | title?: string; 5 | description?: string; 6 | image?: string; 7 | url?: string; 8 | keywords?: string; 9 | } 10 | 11 | const SocialMeta = ({ title, description, image, url, keywords }: Props) => ( 12 | 13 | 14 | 18 | {title && } 19 | {url && } 20 | {description && } 21 | {description && } 22 | {image && ( 23 | 24 | )} 25 | {keywords && } 26 | 27 | ); 28 | 29 | export default SocialMeta; 30 | -------------------------------------------------------------------------------- /components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | const AlgoliaLogo = require('./assets/algolia.svg') as string; 2 | 3 | const Footer = () => ( 4 | 37 | ); 38 | 39 | export default Footer; 40 | -------------------------------------------------------------------------------- /utils/analytics.ts: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | 3 | import { GA_TRACKING_CODE } from '@config'; 4 | 5 | export const initGA = () => { 6 | ReactGA.initialize(GA_TRACKING_CODE); 7 | }; 8 | 9 | export const logPageView = () => { 10 | console.log(`Logging pageview for ${window.location.pathname}`); // tslint:disable-line:no-console 11 | 12 | ReactGA.set({ page: window.location.pathname }); 13 | ReactGA.pageview(window.location.pathname); 14 | }; 15 | 16 | export const logEvent = ( 17 | category: string = '', 18 | action: string = '', 19 | label: string = '', 20 | ) => { 21 | if (window.GA_INITIALIZED && category && action) { 22 | console.log( 23 | `Logging event with category "${category}", action "${action}" and label "${label}"`, 24 | ); 25 | ReactGA.event({ category, action, label }); 26 | } 27 | }; 28 | 29 | export const logException = ( 30 | description: string = '', 31 | fatal: boolean = false, 32 | ) => { 33 | if (window.GA_INITIALIZED && description) { 34 | ReactGA.exception({ description, fatal }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, FC } from 'react'; 2 | 3 | import { initGA, logPageView } from '../../utils/analytics'; 4 | import Footer from './Footer'; 5 | import Header from './Header'; 6 | 7 | declare global { 8 | interface Window { 9 | GA_INITIALIZED: any; 10 | } 11 | } 12 | 13 | // const Content = styled.div` 14 | // padding: 0 15px; 15 | // background: #ffffff; 16 | 17 | // @media (min-width: 992px) { 18 | // padding: 0 100px; 19 | // } 20 | 21 | // @media (min-width: 1200px) { 22 | // padding: 0 300px; 23 | // } 24 | // `; 25 | 26 | const Layout: FC = ({ children }) => { 27 | useEffect(() => { 28 | if (process.env.NODE_ENV === 'production') { 29 | if (!window.GA_INITIALIZED) { 30 | initGA(); 31 | window.GA_INITIALIZED = true; 32 | } 33 | logPageView(); 34 | } 35 | }, []); 36 | 37 | return ( 38 | <> 39 |
40 |
{children}
41 |