commit 7de52e202dad2d5677fb533df6e804a21514d445 Author: Matthias Kretschmann Date: Sun Oct 20 01:40:55 2019 +0200 initial commit diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..167cddc --- /dev/null +++ b/.eslintrc @@ -0,0 +1,36 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + }, + "project": "./tsconfig.json", + "tsconfigRootDir": "./" + }, + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:jsx-a11y/recommended", + "prettier/@typescript-eslint", + "prettier/react", + "plugin:prettier/recommended", + "plugin:react/recommended" + ], + "plugins": ["@typescript-eslint", "react"], + "rules": { + "react/prop-types": "off", + "@typescript-eslint/explicit-function-return-type": "off" + }, + "env": { + "es6": true, + "browser": true, + "jest": true + }, + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd28e47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +.DS_Store +/.next/ +/out/ +/build +package-lock.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..121d245 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "none", + "tabWidth": 2 +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d33b9b1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +dist: xenial +sudo: required +language: node_js +node_js: node + +cache: npm + +before_install: + # Fixes an issue where the max file watch count is exceeded, triggering ENOSPC + # https://stackoverflow.com/questions/22475849/node-js-error-enospc#32600959 + - echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + +# will run `npm install` automatically here + +script: + - npm test + - npm run build + +notifications: + email: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fabfb66 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2019 Matthias Kretschmann + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf5abe2 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# ipfs + +> A public IPFS node & gateway. +> [ipfs.kretschmann.io](https://ipfs.kretschmann.io) + +[![Build Status](https://flat.badgen.net/travis/kremalicious/ipfs?icon=travis)](https://travis-ci.com/kremalicious/ipfs) + +This repo holds a simple React app built with [Next.js](https://nextjs.org) serving as the frontpage of [ipfs.kretschmann.io](https://ipfs.kretschmann.io) from where you can add files to IPFS via drag and drop. + +--- + +- [Development](#development) +- [Production](#production) +- [Deployment](#deployment) + +--- + +## Development + +```bash +npm i +npm start +``` + +Will start a live-reloading local server, reachable under [localhost:3000](http://localhost:3000). + +## Production + +To create a production build, run from the root of the project: + +```bash +npm run build +``` + +Outputs to `./public`. + +## Deployment + +Every branch is automatically deployed to [Now](https://zeit.co/now) with their GitHub integration. A link to a deployment will appear under each Pull Request. + +The latest deployment of the `master` branch is automatically aliased to `ipfs.kremalicious.now.sh`. diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..7b7aa2c --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..d4a4e38 --- /dev/null +++ b/next.config.js @@ -0,0 +1,35 @@ +const withCSS = require('@zeit/next-css') + +const withSvgr = (nextConfig = {}, nextComposePlugins = {}) => { + return Object.assign({}, nextConfig, { + webpack(config, options) { + config.module.rules.push({ + test: /\.svg$/, + use: [ + { + loader: '@svgr/webpack', + options: { + icon: true + } + } + ] + }) + + if (typeof nextConfig.webpack === 'function') { + return nextConfig.webpack(config, options) + } + + return config + } + }) +} + +module.exports = withSvgr( + withCSS({ + cssModules: true, + cssLoaderOptions: { + importLoaders: 1, + localIdentName: '[local]___[hash:base64:5]' + } + }) +) diff --git a/package.json b/package.json new file mode 100644 index 0000000..0dc7f9a --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "@kremalicious/ipfs", + "version": "1.0.0", + "description": "Public IPFS Node.", + "scripts": { + "start": "next dev", + "build": "next build", + "serve": "next start", + "test": "eslint --ignore-path .gitignore 'src/**/*.{ts,tsx}'", + "format": "prettier ./src/**/*.{css,yml,js,jsx,ts,tsx,json} --write" + }, + "author": "Matthias Kretschmann ", + "license": "MIT", + "dependencies": { + "@zeit/next-css": "^1.0.1", + "axios": "^0.19.0", + "ipfs-http-client": "^39.0.0", + "next": "9.1.1", + "next-seo": "^2.1.2", + "next-svgr": "0.0.2", + "react": "^16.10.2", + "react-dom": "^16.10.2", + "react-dropzone": "^10.1.10", + "use-dark-mode": "^2.3.1" + }, + "devDependencies": { + "@types/next-seo": "^1.10.0", + "@types/node": "^12.11.1", + "@types/react": "^16.9.9", + "@typescript-eslint/eslint-plugin": "^2.4.0", + "@typescript-eslint/parser": "^2.4.0", + "eslint": "^6.5.1", + "eslint-config-prettier": "^6.4.0", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.16.0", + "prettier": "^1.18.2", + "typescript": "^3.6.4" + }, + "engines": { + "node": "10.x" + } +} diff --git a/site.config.js b/site.config.js new file mode 100644 index 0000000..24b8121 --- /dev/null +++ b/site.config.js @@ -0,0 +1,7 @@ +module.exports = { + title: 'ipfs.kretschmann.io', + description: 'A public IPFS Gateway', + url: 'https://ipfs.kretschmann.io', + ipfsGateway: 'https://ipfs.kretschmann.io', + ipfsNodeUri: 'https://ipfs.kretschmann.io:443' +} diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts new file mode 100644 index 0000000..d867952 --- /dev/null +++ b/src/@types/global.d.ts @@ -0,0 +1,15 @@ +declare module '*.module.css' { + const classes: { readonly [key: string]: string } + export default classes +} + +declare module '*.svg' { + import * as React from 'react' + export const ReactComponent: React.FunctionComponent< + React.SVGProps + > + const src: string + export default src +} + +declare module 'ipfs-http-client' diff --git a/src/Layout.module.css b/src/Layout.module.css new file mode 100644 index 0000000..46d805e --- /dev/null +++ b/src/Layout.module.css @@ -0,0 +1,15 @@ +@import './styles/_variables.css'; + +.app { + padding: var(--spacer); + display: flex; + flex-wrap: wrap; + text-align: center; + min-height: 100vh; + margin: auto; + max-width: var(--screen-sm); +} + +.main { + width: 100%; +} diff --git a/src/Layout.tsx b/src/Layout.tsx new file mode 100644 index 0000000..77cb1d5 --- /dev/null +++ b/src/Layout.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode } from 'react' +import Head from 'next/head' +import { NextSeo } from 'next-seo' +import Footer from './components/Footer' +import styles from './Layout.module.css' +import { title, description, url } from '../site.config' + +export default function Layout({ + children, + pageTitle = title +}: { + children: ReactNode + pageTitle?: string +}) { + return ( +
+ + + + + + +
{children}
+ +
+
+ ) +} diff --git a/src/components/Add.module.css b/src/components/Add.module.css new file mode 100644 index 0000000..1a297fb --- /dev/null +++ b/src/components/Add.module.css @@ -0,0 +1,8 @@ +@import '../styles/_variables.css'; + +.add { + width: 100%; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; +} diff --git a/src/components/Add.tsx b/src/components/Add.tsx new file mode 100644 index 0000000..4b9a51a --- /dev/null +++ b/src/components/Add.tsx @@ -0,0 +1,101 @@ +import React, { useState, useEffect } from 'react' +import { ipfsNodeUri, ipfsGateway } from '../../site.config' +import Dropzone from './Dropzone' +import styles from './Add.module.css' +import Spinner from './Spinner' +import useIpfsApi, { IpfsConfig } from '../hooks/use-ipfs-api' + +export function formatBytes(a: number, b: number) { + if (a === 0) return '0 Bytes' + const c = 1024 + const d = b || 2 + const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const f = Math.floor(Math.log(a) / Math.log(c)) + return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f] +} + +const { hostname, port, protocol } = new URL(ipfsNodeUri) + +const ipfsConfig: IpfsConfig = { + protocol: protocol.replace(':', ''), + host: hostname, + port: port || '443' +} + +async function addToIpfs( + files: File[], + setFileSizeReceived: (size: string) => void, + ipfs: any +) { + const file = [...files][0] + const fileDetails = { path: file.name, content: file } + + const response = await ipfs.add(fileDetails, { + wrapWithDirectory: true, + progress: (length: number) => setFileSizeReceived(formatBytes(length, 0)) + }) + + // CID of wrapping directory is returned last + const cid = `${response[response.length - 1].hash}/${file.name}` + return cid +} + +export default function Add() { + const { ipfs, isIpfsReady, ipfsError } = useIpfsApi(ipfsConfig) + const [fileHash, setFileHash] = useState() + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState() + const [error, setError] = useState() + const [fileSize, setFileSize] = useState() + const [fileSizeReceived, setFileSizeReceived] = useState('') + + useEffect(() => { + setMessage( + `Adding to IPFS
+ ${fileSizeReceived || 0}/${fileSize}
` + ) + }, [fileSize, fileSizeReceived]) + + async function handleOnDrop(acceptedFiles: File[]) { + if (!acceptedFiles[0]) return + + setLoading(true) + setError(null) + + const totalSize = formatBytes(acceptedFiles[0].size, 0) + setFileSize(totalSize) + + try { + const cid = await addToIpfs(acceptedFiles, setFileSizeReceived, ipfs) + if (!cid) return + setFileHash(cid) + setLoading(false) + } catch (error) { + setError(`Adding to IPFS failed: ${error.message}`) + return null + } + } + + return ( +
+ {loading ? ( + + ) : fileHash ? ( + + ipfs://{fileHash} + + ) : ( + + )} +
+ ) +} diff --git a/src/components/Dropzone.module.css b/src/components/Dropzone.module.css new file mode 100644 index 0000000..489a143 --- /dev/null +++ b/src/components/Dropzone.module.css @@ -0,0 +1,47 @@ +@import '../styles/_variables.css'; + +:root { + --border-color: var(--brand-grey-light); +} + +:global(.dark) { + --border-color: var(--brand-grey); +} + +.dropzone { + border: 0.1rem dashed var(--border-color); + border-radius: calc(var(--border-radius) * 2); + padding: calc(var(--spacer) * 2) var(--spacer); + transition: 0.2s ease-out; + cursor: pointer; + text-align: center; + opacity: 0.75; +} + +.dropzone:hover, +.dropzone:focus, +.dropzone:active { + outline: 0; +} + +.dragover { + composes: dropzone; + border-color: var(--brand-cyan); +} + +.disabled { + composes: dropzone; + border-color: var(--brand-grey-light); + pointer-events: none; +} + +.error { + font-size: var(--font-size-small); + color: red; +} + +.dropzone p { + text-align: center; + margin-bottom: 0; + color: var(--brand-grey); +} diff --git a/src/components/Dropzone.tsx b/src/components/Dropzone.tsx new file mode 100644 index 0000000..83ac16e --- /dev/null +++ b/src/components/Dropzone.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from 'react' +import { useDropzone } from 'react-dropzone' +import styles from './Dropzone.module.css' + +export default function Dropzone({ + handleOnDrop, + disabled, + multiple, + error +}: { + handleOnDrop(files: File[]): void + disabled?: boolean + multiple?: boolean + error?: string +}) { + const onDrop = useCallback(acceptedFiles => handleOnDrop(acceptedFiles), [ + handleOnDrop + ]) + + const { + getRootProps, + getInputProps, + isDragActive, + isDragReject + } = useDropzone({ onDrop }) + + return ( +
+ + {isDragActive && !isDragReject ? ( + `Drop it like it's hot!` + ) : multiple ? ( + `Drag 'n' drop some files here, or click to select files` + ) : error ? ( +
{error}
+ ) : ( + `Drag 'n' drop a file here, or click to select a file` + )} +
+ ) +} diff --git a/src/components/Footer.module.css b/src/components/Footer.module.css new file mode 100644 index 0000000..7eb9482 --- /dev/null +++ b/src/components/Footer.module.css @@ -0,0 +1,15 @@ +@import '../styles/_variables.css'; + +.footer { + width: 100%; + font-size: var(--font-size-mini); + padding: var(--spacer) 0; + padding-bottom: 0; + align-self: flex-end; + margin-top: var(--spacer); +} + +.footer, +.footer a { + color: var(--color-text); +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..fa122a8 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import ThemeSwitch from './ThemeSwitch' +import styles from './Footer.module.css' + +export default function Footer() { + const year = new Date().getFullYear() + + return ( + + ) +} diff --git a/src/components/Info.module.css b/src/components/Info.module.css new file mode 100644 index 0000000..a23c6d7 --- /dev/null +++ b/src/components/Info.module.css @@ -0,0 +1,18 @@ +@import '../styles/_variables.css'; + +.info { + font-size: var(--font-size-small); + opacity: 0.8; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.info h2 { + font-size: var(--font-size-base); + color: var(--color-text); + margin-top: calc(var(--spacer) * 2); + margin-left: var(--spacer); + margin-right: var(--spacer); + display: inline-block; +} diff --git a/src/components/Info.tsx b/src/components/Info.tsx new file mode 100644 index 0000000..2099ef9 --- /dev/null +++ b/src/components/Info.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import Status from './Status' +import styles from './Info.module.css' + +export default function Info() { + return ( + + ) +} diff --git a/src/components/Spinner.module.css b/src/components/Spinner.module.css new file mode 100644 index 0000000..2693ef2 --- /dev/null +++ b/src/components/Spinner.module.css @@ -0,0 +1,36 @@ +@import '../styles/_variables.css'; + +.spinner { + position: relative; + text-align: center; + margin-top: calc(var(--spacer) * 4); + margin-bottom: calc(var(--spacer) / 2); + line-height: 1.3; +} + +.spinner:before { + content: ''; + box-sizing: border-box; + position: absolute; + top: 0; + left: 50%; + width: 20px; + height: 20px; + margin-top: -20px; + margin-left: -10px; + border-radius: 50%; + border: 2px solid var(--brand-main); + border-top-color: var(--brand-cyan); + animation: spinner 0.6s linear infinite; +} + +.spinnerMessage { + color: var(--brand-grey); + padding-top: calc(var(--spacer) / 4); +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 0000000..8ec4949 --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import styles from './Spinner.module.css' + +const Spinner = ({ message }: { message?: string }) => { + return ( +
+ {message && ( +
+ )} +
+ ) +} + +export default Spinner diff --git a/src/components/Status.module.css b/src/components/Status.module.css new file mode 100644 index 0000000..1b0c79f --- /dev/null +++ b/src/components/Status.module.css @@ -0,0 +1,28 @@ +@import '../styles/_variables.css'; + +/* default: red square */ +.status { + width: var(--font-size-mini); + height: var(--font-size-mini); + display: inline-block; + background: #ff4136; + margin-right: calc(var(--spacer) / 8); +} + +/* yellow triangle */ +.loading { + composes: status; + background: none; + width: 0; + height: 0; + border-left: calc(var(--font-size-mini) / 1.7) solid transparent; + border-right: calc(var(--font-size-mini) / 1.7) solid transparent; + border-bottom: var(--font-size-mini) solid #ffdc00; +} + +/* green circle */ +.online { + composes: status; + border-radius: 50%; + background: #3d9970; +} diff --git a/src/components/Status.tsx b/src/components/Status.tsx new file mode 100644 index 0000000..1fd5fc6 --- /dev/null +++ b/src/components/Status.tsx @@ -0,0 +1,47 @@ +import React, { useState, useEffect } from 'react' +import { pingUrl } from '../utils' +import { ipfsGateway, ipfsNodeUri } from '../../site.config' +import styles from './Status.module.css' + +export default function Status({ type }: { type: string }) { + const [isOnline, setIsOnline] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + async function ping() { + const url = + type === 'gateway' + ? `${ipfsGateway}/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme` + : `${ipfsNodeUri}/api/v0/id` + + const ping = await pingUrl(url) + setIsLoading(false) + isOnline !== ping && setIsOnline(ping) + } + + useEffect(() => { + ping() + + const timer = setInterval(() => { + ping() + }, 10000) // run every 10 sec. + + return () => { + clearInterval(timer) + setIsOnline(false) + setIsLoading(false) + } + }, []) + + const classes = isLoading + ? styles.loading + : isOnline + ? styles.online + : styles.status + + return ( + + ) +} diff --git a/src/components/ThemeSwitch.module.css b/src/components/ThemeSwitch.module.css new file mode 100644 index 0000000..8bdfa6a --- /dev/null +++ b/src/components/ThemeSwitch.module.css @@ -0,0 +1,76 @@ +@import '../styles/_variables.css'; + +:root { + --knob-size: 8px; + --knob-space: 1px; +} + +.themeSwitch { + position: relative; + display: inline-block; + margin-bottom: calc(var(--spacer) * var(--line-height)); + margin-left: auto; +} + +.themeSwitch svg { + width: 0.8rem; + height: 0.8rem; + margin-top: -0.05rem; + fill: var(--brand-grey-light); +} + +.themeSwitch svg:last-child { + margin-top: -0.1rem; + width: 0.7rem; + height: 0.7rem; +} + +.checkboxContainer { + display: flex; + align-items: center; +} + +.checkboxFake { + display: block; + position: relative; + width: calc(var(--knob-size) * 2.5); + height: calc(var(--knob-size) + calc(var(--knob-space) * 4)); + border: 1px solid var(--brand-grey-light); + border-radius: 15rem; + margin-left: calc(var(--spacer) / 3); + margin-right: calc(var(--spacer) / 3); +} + +.checkboxFake::after { + content: ''; + position: absolute; + top: var(--knob-space); + left: var(--knob-space); + width: var(--knob-size); + height: var(--knob-size); + background-color: var(--brand-grey-light); + border-radius: 15rem; + transition: transform 0.2s var(--easing); + transform: translate3d(0, 0, 0); +} + +.checkbox { + position: relative; + cursor: pointer; +} + +[type='checkbox']:checked + .checkboxContainer .checkboxFake::after { + transform: translate3d(100%, 0, 0); +} + +[type='checkbox'], +.label { + width: 1px; + height: 1px; + border: 0; + clip: rect(0 0 0 0); + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} diff --git a/src/components/ThemeSwitch.tsx b/src/components/ThemeSwitch.tsx new file mode 100644 index 0000000..77ec496 --- /dev/null +++ b/src/components/ThemeSwitch.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import useDarkMode from 'use-dark-mode' + +import Day from '../images/day.svg' +import Night from '../images/night.svg' +import styles from './ThemeSwitch.module.css' + +const ThemeToggle = () => ( + + + + + +) + +const ThemeToggleInput = ({ + isDark, + toggleDark +}: { + isDark: boolean + toggleDark(): void +}) => ( + +) + +export default function ThemeSwitch() { + const darkMode = useDarkMode(false, { + classNameDark: 'dark', + classNameLight: 'light' + }) + + return ( + + ) +} diff --git a/src/hooks/use-ipfs-api.tsx b/src/hooks/use-ipfs-api.tsx new file mode 100644 index 0000000..1fa91f7 --- /dev/null +++ b/src/hooks/use-ipfs-api.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react' +import ipfsClient from 'ipfs-http-client' + +let ipfs: any = null +let ipfsVersion = '' + +export interface IpfsConfig { + protocol: string + host: string + port: string +} + +function parseHTML(str: string) { + const tmp = document.implementation.createHTMLDocument() + tmp.body.innerHTML = str + return tmp.body.children +} + +export default function useIpfsApi(config: IpfsConfig) { + const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs)) + const [ipfsError, setIpfsError] = useState('') + + useEffect(() => { + async function initIpfs() { + if (ipfs !== null) return + // eslint-disable-next-line + ipfs = await ipfsClient(config) + + try { + const version = await ipfs.version() + ipfsVersion = version.version + } catch (error) { + let { message } = error + + if (!error.status) { + const htmlData = parseHTML(error) + message = htmlData.item(0) + message = message.textContent + } + + setIpfsError(`IPFS connection error: ${message}`) + setIpfsReady(false) + return + } + setIpfsReady(Boolean(await ipfs.id())) + } + + initIpfs() + }, [config]) + + useEffect(() => { + // just like componentWillUnmount() + return function cleanup() { + if (ipfs) { + setIpfsReady(false) + ipfs = null + ipfsVersion = '' + setIpfsError('') + } + } + }, []) + + return { ipfs, ipfsVersion, isIpfsReady, ipfsError } +} diff --git a/src/images/day.svg b/src/images/day.svg new file mode 100644 index 0000000..b48ffbb --- /dev/null +++ b/src/images/day.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/night.svg b/src/images/night.svg new file mode 100644 index 0000000..c2ee7c9 --- /dev/null +++ b/src/images/night.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/index.module.css b/src/pages/index.module.css new file mode 100644 index 0000000..4a40f59 --- /dev/null +++ b/src/pages/index.module.css @@ -0,0 +1,24 @@ +@import '../styles/_variables.css'; + +.header { + margin-top: 10vh; + margin-bottom: calc(var(--spacer) * 2); + text-align: center; +} + +.title { + margin-top: 0; + margin-bottom: calc(var(--spacer) / 2); + font-size: var(--font-size-h2); +} + +@media screen and (min-width: 640px) { + .title { + font-size: var(--font-size-h1); + } +} + +.header a, +.description { + font-size: var(--font-size-large); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..e332146 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +import '../styles/global.css' + +import Add from '../components/Add' +import { title, description } from '../../site.config' +import styles from './index.module.css' + +import Layout from '../Layout' +import Info from '../components/Info' + +const Home = () => ( + +
+

{title}

+

{description}

+
+ + + +
+) + +export default Home diff --git a/src/styles/_variables.css b/src/styles/_variables.css new file mode 100644 index 0000000..8094540 --- /dev/null +++ b/src/styles/_variables.css @@ -0,0 +1,58 @@ +:root { + --brand-main: #015565; + --brand-cyan: #43a699; + --brand-main-light: #88bec8; + --brand-light: #e7eef4; + + --brand-grey: #4e5d63; + --brand-grey-light: #70858e; + /* --brand-grey-dimmed: lighten($brand-grey, 50%); */ + + --font-family-base: 'ff-tisa-sans-web-pro', 'Trebuchet MS', 'Helvetica Neue', + Helvetica, Arial, sans-serif; + --font-family-headings: 'brandon-grotesque', 'Avenir Next', 'Helvetica Neue', + Helvetica, Arial, sans-serif; + --font-family-monospace: 'Fira Code', 'Fira Mono', Menlo, Monaco, Consolas, + 'Courier New', monospace; + + --font-size-root: 18px; + --font-size-base: 1rem; + --font-size-large: 1.15rem; + --font-size-small: 0.85rem; + --font-size-mini: 0.7rem; + + --font-size-h1: 2.5rem; + --font-size-h2: 2rem; + --font-size-h3: 1.5rem; + --font-size-h4: 1.2rem; + --font-size-h5: 1.1rem; + + --font-weight-base: 400; + --font-weight-bold: 700; + --line-height: 1.6; + + --font-weight-headings: 500; + --line-height-headings: 1.1; + + --spacer: calc(var(--font-size-base) * var(--line-height)); + --border-radius: 0.3rem; + + --screen-xs: 30em; + --screen-sm: 40.625em; + --screen-md: 60em; + --screen-lg: 87.5em; + + --easing: cubic-bezier(0.75, 0, 0.08, 1); + + --body-background-color: var(--brand-light); + --color-text: var(--brand-grey); + --color-headings: var(--brand-main); + --selection-background-color: var(--brand-main); +} + +:global(.dark) { + --body-background-color: #1d2224; + --color-text: var(--brand-grey-light); + --color-headings: var(--brand-main-light); + --selection-background: var(--brand-main-light); +} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..ce4c8d9 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,132 @@ +@import '_variables.css'; +@import url('https://use.typekit.net/msu4qap.css'); + +*, +*:before, +*:after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +html { + font-size: var(--font-size-root); +} + +body { + color: var(--color-text); + font-size: var(--font-size-base); + font-family: var(--font-family-base); + font-weight: var(--font-weight-base); + line-height: var(--line-height); + text-rendering: optimizeLegibility; + letter-spacing: -0.01em; + font-feature-settings: 'liga', 'kern'; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + min-height: 100vh; + transition: background 0.4s var(--easing); + background: var(--body-background-color); + + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +a { + text-decoration: none; + color: var(--brand-cyan); + transition: 0.2s ease-out var(--easing); +} + +p { + margin: 0; + margin-bottom: var(--spacer); +} + +h1, +h2, +h3, +h4, +h5 { + color: var(--color-headings); + font-family: var(--font-family-headings); + line-height: var(--line-height-headings); + font-weight: var(--font-weight-headings); +} + +h1 { + font-size: var(--font-size-h1); +} + +h2 { + font-size: var(--font-size-h2); +} + +h3 { + font-size: var(--font-size-h3); +} + +h4 { + font-size: var(--font-size-h4); +} + +h5 { + font-size: var(--font-size-h5); +} + +figure, +img, +svg, +video, +audio, +embed, +canvas, +picture { + max-width: 100%; + height: auto; + margin: 0 auto; + display: block; +} + +ul { + margin-top: 0; + margin-bottom: calc(var(--spacer) / var(--line-height)); + padding-left: 0; + list-style: none; + padding-left: calc(var(--spacer) / var(--line-height)); +} + +ul li { + position: relative; + display: block; + margin-bottom: 0; +} + +ul li + li { + margin-top: calc(var(--spacer) / 4); +} + +ul li:before { + content: ' \2015'; + top: -1px; + position: absolute; + left: -1rem; + color: var(--brand-grey); + user-select: none; +} + +::-moz-selection { + background: var(--selection-background-color); + color: #fff; +} + +::selection { + background: var(--selection-background-color); + color: #fff; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..ced6785 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,12 @@ +import axios from 'axios' + +export async function pingUrl(url: string) { + try { + const response = await axios(url, { timeout: 5000 }) + if (!response || response.status !== 200) return false + return true + } catch (error) { + console.error(error.message) + return false + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5bdd590 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "jsx": "preserve", + "lib": ["dom", "es2017"], + "module": "esnext", + "moduleResolution": "node", + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "preserveConstEnums": true, + "removeComments": false, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "esnext", + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "exclude": ["node_modules"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.mdx"] +}