@ -13,14 +13,14 @@
|
||||
},
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postStartCommand": "yarn install --frozen-lockfile && git update-index --assume-unchanged data/manifest.json && git config pull.rebase true",
|
||||
"postStartCommand": "yarn install --frozen-lockfile && ./node_modules/.bin/next telemetry disable",
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [1337],
|
||||
"forwardPorts": [3000],
|
||||
|
||||
"portsAttributes": {
|
||||
"1337": {
|
||||
"label": "Webpack",
|
||||
"3000": {
|
||||
"label": "next dev",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
@ -32,12 +32,11 @@
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"budparr.language-hugo-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"divlo.vscode-styled-jsx-syntax",
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"stylelint.vscode-stylelint",
|
||||
"ms-vscode.wordcount"
|
||||
"stylelint.vscode-stylelint"
|
||||
]
|
||||
}
|
||||
|
16
.env.example
@ -1,16 +0,0 @@
|
||||
AIRTABLE_API_KEY=
|
||||
AIRTABLE_BASE=
|
||||
FAUNADB_SERVER_SECRET=
|
||||
GH_PUBLIC_TOKEN=
|
||||
HCAPTCHA_SECRET_KEY=
|
||||
HCAPTCHA_SITE_KEY=
|
||||
LHCI_ADMIN_TOKEN=
|
||||
LHCI_GITHUB_APP_TOKEN=
|
||||
LHCI_TOKEN=
|
||||
PERCY_TOKEN=
|
||||
SENTRY_AUTH_TOKEN=
|
||||
SENTRY_DSN=
|
||||
SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_CLIENT_SECRET=
|
||||
SPOTIFY_REFRESH_TOKEN=
|
||||
WEBMENTIONS_TOKEN=
|
@ -1,67 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"@jakejarvis/eslint-config",
|
||||
"plugin:compat/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
plugins: ["jsx-a11y", "prettier"],
|
||||
parser: "@babel/eslint-parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
impliedStrict: true,
|
||||
jsx: true,
|
||||
},
|
||||
jsxPragma: null,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
settings: {
|
||||
polyfills: ["fetch", "Promise"],
|
||||
react: {
|
||||
pragma: "h",
|
||||
version: "17.0",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Mostly cherry-picked from eslint-config-preact:
|
||||
// https://github.com/preactjs/eslint-config-preact/blob/v1.3.0/index.js
|
||||
"react/display-name": ["warn", { ignoreTranspilerName: false }],
|
||||
"react/jsx-key": ["error", { checkFragmentShorthand: true }],
|
||||
"react/jsx-no-bind": [
|
||||
"warn",
|
||||
{
|
||||
ignoreRefs: true,
|
||||
allowFunctions: true,
|
||||
allowArrowFunctions: true,
|
||||
},
|
||||
],
|
||||
"react/jsx-no-comment-textnodes": "error",
|
||||
"react/jsx-no-duplicate-props": "error",
|
||||
"react/jsx-no-target-blank": "error",
|
||||
"react/jsx-no-undef": "error",
|
||||
"react/jsx-tag-spacing": ["error", { beforeSelfClosing: "always" }],
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"react/no-danger": "warn",
|
||||
"react/no-deprecated": "error",
|
||||
"react/no-unknown-property": ["error", { ignore: ["class"] }], // className in react == class in preact
|
||||
"react/prefer-es6-class": "error",
|
||||
"react/prefer-stateless-function": "warn",
|
||||
"react/prop-types": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-render-return": "error",
|
||||
"react/self-closing-comp": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
},
|
||||
ignorePatterns: ["public/**", "static/assets/**"],
|
||||
};
|
8
.eslintrc.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@jakejarvis/eslint-config", "next/core-web-vitals", "plugin:prettier/recommended"],
|
||||
plugins: ["prettier"],
|
||||
rules: {
|
||||
"react/no-unescaped-entities": "off",
|
||||
},
|
||||
};
|
4
.gitattributes
vendored
@ -1,6 +1,2 @@
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Mark cached Hugo resources/assets as generated to suppress in Git diffs and repo stats.
|
||||
# https://github.com/github/linguist/blob/master/README.md#generated-code
|
||||
resources/_gen/** linguist-generated=true
|
||||
|
29
.github/workflows/ci.yml
vendored
@ -1,29 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: yarn
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
env:
|
||||
SKIP_OPTIMIZE_IMAGES: true
|
||||
- run: yarn lint
|
||||
- run: npx -p @percy/cli percy snapshot ./public
|
||||
if: github.actor == 'jakejarvis'
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
61
.github/workflows/post-deploy.yml
vendored
@ -1,61 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on: deployment_status
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.deployment_status.state == 'success'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: |
|
||||
LOWERENV=$(echo "${{ github.event.deployment_status.environment }}" | tr "[:upper:]" "[:lower:]")
|
||||
echo "NORMALIZED_ENV=${LOWERENV}" >> $GITHUB_ENV
|
||||
- uses: getsentry/action-release@v1
|
||||
if: ${{ env.NORMALIZED_ENV == 'production' }}
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: jakejarvis
|
||||
SENTRY_PROJECT: jarvis
|
||||
with:
|
||||
environment: ${{ env.NORMALIZED_ENV }}
|
||||
- uses: browser-actions/setup-chrome@latest
|
||||
with:
|
||||
chrome-version: stable
|
||||
- run: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install -g @lhci/cli
|
||||
- run: |
|
||||
chrome --version
|
||||
lhci --version
|
||||
curl https://lhci.jrvs.io/version || true
|
||||
echo "ref: ${{ github.event.deployment.ref }}"
|
||||
echo "env: ${{ env.NORMALIZED_ENV }}"
|
||||
echo "url: ${{ github.event.deployment_status.target_url }}"
|
||||
- if: ${{ env.NORMALIZED_ENV == 'production' }}
|
||||
run: |
|
||||
echo "BASE_DEPLOY_URL=https://jarv.is" >> $GITHUB_ENV
|
||||
echo "LHCI_EXTRA_FLAGS=" >> $GITHUB_ENV
|
||||
echo "LHCI_BUILD_CONTEXT__CURRENT_BRANCH=main" >> $GITHUB_ENV
|
||||
- if: ${{ env.NORMALIZED_ENV == 'preview' }}
|
||||
run: |
|
||||
echo "BASE_DEPLOY_URL=${{ github.event.deployment_status.target_url }}" >> $GITHUB_ENV
|
||||
echo "LHCI_EXTRA_FLAGS=--assert.assertions.is-crawlable=off --assert.assertions.canonical=off" >> $GITHUB_ENV
|
||||
echo "LHCI_BUILD_CONTEXT__CURRENT_BRANCH=${{ github.event.deployment.ref }}" >> $GITHUB_ENV
|
||||
- continue-on-error: true
|
||||
env:
|
||||
LHCI_TOKEN: ${{ secrets.LHCI_TOKEN }}
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
run: |
|
||||
lhci autorun ${{ env.LHCI_EXTRA_FLAGS }} \
|
||||
--collect.url=${{ env.BASE_DEPLOY_URL }}/ \
|
||||
--collect.url=${{ env.BASE_DEPLOY_URL }}/notes/how-to-pull-request-fork-github/ \
|
||||
--collect.url=${{ env.BASE_DEPLOY_URL }}/projects/ \
|
||||
--collect.url=${{ env.BASE_DEPLOY_URL }}/contact/
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lhci-results
|
||||
path: ./.lighthouseci
|
15
.github/workflows/purge-artifacts.yml
vendored
@ -1,15 +0,0 @@
|
||||
name: Purge old artifacts
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * 3" # every Wednesday at 9 AM UTC == 5 AM EDT
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
purge-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: kolpav/purge-artifacts-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
expire-in: 0
|
29
.gitignore
vendored
@ -1,15 +1,10 @@
|
||||
# Hugo artifacts
|
||||
public/
|
||||
builds/
|
||||
_vendor/
|
||||
assets/jsconfig.json
|
||||
hugo_stats.json
|
||||
.hugo_build.lock
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
|
||||
# webpack output
|
||||
static/assets/
|
||||
data/manifest.json
|
||||
webpack_stats.json
|
||||
# generated at build-time by next-sitemap
|
||||
public/robots.txt
|
||||
public/sitemap.xml
|
||||
|
||||
# node/npm/yarn
|
||||
node_modules/
|
||||
@ -18,12 +13,10 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Lighthouse CI
|
||||
.lighthouseci/
|
||||
|
||||
# here be secrets
|
||||
*.env*
|
||||
!.env.example
|
||||
*.pem
|
||||
|
||||
# macOS/iCloud junk
|
||||
.DS_Store
|
||||
@ -34,5 +27,11 @@ yarn-error.log*
|
||||
*.icloud
|
||||
.nosync
|
||||
|
||||
# local Vercel config
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
@ -1,38 +0,0 @@
|
||||
{
|
||||
"ci": {
|
||||
"collect": {
|
||||
"numberOfRuns": 3,
|
||||
"settings": {
|
||||
"preset": "desktop",
|
||||
"chromeFlags": "--no-sandbox"
|
||||
}
|
||||
},
|
||||
"assert": {
|
||||
"preset": "lighthouse:no-pwa",
|
||||
"assertions": {
|
||||
"color-contrast": "warn",
|
||||
"csp-xss": "warn",
|
||||
"link-text": "warn",
|
||||
"long-tasks": "warn",
|
||||
"total-byte-weight": ["warn", { "minScore": 0.9 }],
|
||||
"unminified-css": "warn",
|
||||
"unminified-javascript": "warn",
|
||||
"unsized-images": "warn",
|
||||
"unused-css-rules": "warn",
|
||||
"unused-javascript": "warn",
|
||||
"uses-optimized-images": "warn",
|
||||
"uses-rel-preconnect": "warn",
|
||||
"valid-source-maps": "warn",
|
||||
"modern-image-formats": "off",
|
||||
"offscreen-images": "off",
|
||||
"uses-long-cache-ttl": "off",
|
||||
"uses-responsive-images": "off"
|
||||
},
|
||||
"includePassedAssertions": true
|
||||
},
|
||||
"upload": {
|
||||
"target": "lhci",
|
||||
"serverBaseUrl": "https://lhci.jrvs.io"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"default": true,
|
||||
"code-block-style": { "style": "fenced" },
|
||||
"code-fence-style": { "style": "backtick" },
|
||||
"fenced-code-language": true,
|
||||
"heading-style": { "style": "atx" },
|
||||
"hr-style": { "style": "---" },
|
||||
"line-length": false,
|
||||
"no-empty-links": false,
|
||||
"no-inline-html": {
|
||||
"allowed_elements": ["a", "span", "img", "svg", "path", "polygon", "circle", "g", "sup"]
|
||||
},
|
||||
"no-trailing-punctuation": false,
|
||||
"single-h1": false,
|
||||
"whitespace": false
|
||||
}
|
34
.percy.yml
@ -1,34 +0,0 @@
|
||||
version: 2
|
||||
snapshot:
|
||||
widths:
|
||||
- 450
|
||||
- 1050
|
||||
enable-javascript: true
|
||||
percy-css: >
|
||||
.embed,
|
||||
iframe,
|
||||
video,
|
||||
img[src$=".gif"],
|
||||
.loading,
|
||||
#meta-hits,
|
||||
#contact-form-captcha {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
animation: none !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
static:
|
||||
include:
|
||||
- "index.html"
|
||||
- "uses/index.html"
|
||||
- "notes/how-to-pull-request-fork-github/index.html"
|
||||
- "notes/shodan-search-queries/index.html"
|
||||
- "contact/index.html"
|
||||
discovery:
|
||||
network-idle-timeout: 750
|
||||
disable-cache: true
|
@ -1,20 +1,3 @@
|
||||
# Hugo/Webpack stuff that doesn't play well with Prettier
|
||||
assets/**/vendor/
|
||||
assets/jsconfig.json
|
||||
layouts/
|
||||
static/
|
||||
hugo_stats.json
|
||||
webpack_stats.json
|
||||
data/manifest.json
|
||||
|
||||
# output from Hugo
|
||||
public/
|
||||
builds/
|
||||
resources/
|
||||
_vendor/
|
||||
|
||||
# miscellaneous
|
||||
.vercel/
|
||||
.lighthouseci/
|
||||
.vscode/
|
||||
/content/notes/dark-mode/example/
|
||||
/.next
|
||||
/out
|
||||
/public
|
||||
|
@ -1,17 +1,11 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard-scss", "stylelint-config-sass-guidelines", "stylelint-prettier/recommended"],
|
||||
"plugins": ["stylelint-no-unsupported-browser-features", "stylelint-prettier"],
|
||||
"extends": ["stylelint-config-standard-scss", "stylelint-prettier/recommended"],
|
||||
"plugins": ["stylelint-scss", "stylelint-prettier"],
|
||||
"rules": {
|
||||
"color-hex-length": "long",
|
||||
"max-nesting-depth": 6,
|
||||
"no-descending-specificity": null,
|
||||
"order/order": null,
|
||||
"order/properties-alphabetical-order": null,
|
||||
"plugin/no-unsupported-browser-features": [true, { "severity": "warning", "ignore": ["flexbox"] }],
|
||||
"selector-max-compound-selectors": null,
|
||||
"selector-max-id": null,
|
||||
"selector-no-qualifying-type": null,
|
||||
"shorthand-property-no-redundant-values": null
|
||||
"rule-empty-line-before": null,
|
||||
"selector-class-pattern": null
|
||||
},
|
||||
"ignoreFiles": ["public/**", "static/assets/**", "node_modules/**"]
|
||||
"ignoreFiles": ["public/**"]
|
||||
}
|
||||
|
11
.vscode/extensions.json
vendored
@ -1,13 +1,10 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"budparr.language-hugo-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"divlo.vscode-styled-jsx-syntax",
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"stylelint.vscode-stylelint",
|
||||
"ms-vscode.wordcount"
|
||||
"stylelint.vscode-stylelint"
|
||||
]
|
||||
}
|
||||
|
21
.vscode/settings.json
vendored
@ -5,29 +5,18 @@
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"search.exclude": {
|
||||
"**/.next": true,
|
||||
"**/node_modules": true
|
||||
},
|
||||
"css.validate": false,
|
||||
"scss.validate": false,
|
||||
"html.format.templating": true,
|
||||
"go-template.languages": [
|
||||
"html",
|
||||
"json",
|
||||
"xml",
|
||||
"atom",
|
||||
"webmanifest"
|
||||
],
|
||||
"go-template.patterns": [
|
||||
"layouts/**/*"
|
||||
],
|
||||
"prettier.requireConfig": true,
|
||||
"prettier.configPath": ".prettierrc.json",
|
||||
"markdown.preview.lineHeight": 1.75,
|
||||
"stylelint.packageManager": "yarn",
|
||||
"stylelint.reportNeedlessDisables": true,
|
||||
"stylelint.reportInvalidScopeDisables": true,
|
||||
"stylelint.validate": [
|
||||
"css",
|
||||
"scss"
|
||||
],
|
||||
"stylelint.validate": ["css", "scss"],
|
||||
"npm.packageManager": "yarn",
|
||||
"eslint.packageManager": "yarn"
|
||||
}
|
||||
|
24
README.md
@ -1,34 +1,22 @@
|
||||
# 🏡 [jarv.is](https://jarv.is/)
|
||||
|
||||
[](https://github.com/jakejarvis/jarv.is/actions?query=workflow%3ACI+branch%3Amain)
|
||||
[](https://vercel.com/deployments/jarv.is)
|
||||
[](https://github.com/gohugoio/hugo)
|
||||
[](https://creativecommons.org/licenses/by/4.0/)
|
||||
[](https://github.com/jakejarvis/jarv.is)
|
||||
[](http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion/)
|
||||
|
||||
Personal website of [@jakejarvis](https://github.com/jakejarvis), created and deployed using [Hugo](https://gohugo.io/), [Preact](https://preactjs.com/), [Vercel](https://vercel.com/), [and more](https://jarv.is/humans.txt).
|
||||
Personal website of [@jakejarvis](https://github.com/jakejarvis), created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [and more](https://jarv.is/humans.txt).
|
||||
|
||||
I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/11) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too!
|
||||
|
||||
## 💾 Starting a local development server
|
||||
## 🧶 Getting Started
|
||||
|
||||
### 🧶 Using Yarn:
|
||||
Run `yarn install` and `yarn dev`, then open [http://localhost:3000/](http://localhost:3000/). ([Yarn must be installed](https://yarnpkg.com/en/docs/install) first; NPM _should_ also work at your own risk.) Pages will live-refresh when source files are changed.
|
||||
|
||||
Run `yarn install` and `yarn start`, then open [http://localhost:1337/](http://localhost:1337/). ([Yarn must be installed](https://yarnpkg.com/en/docs/install) first; NPM _should_ also work at your own risk.) Hugo, [Webpack](https://webpack.js.org/), and [Gulp](https://gulpjs.com/) will automatically work together to build the site, and pages will live-refresh via [Browsersync](https://browsersync.io/) when source files are changed.
|
||||
|
||||
### ▲ Using [`vercel dev`](https://vercel.com/docs/cli#commands/dev):
|
||||
|
||||
The [Vercel CLI](https://vercel.com/docs/cli) is not included as a project dependency here, but [installing it globally](https://vercel.com/cli) (`npm i -g vercel`) and running `vercel dev` in this repository will build and serve the static site automatically via Yarn **as well as the [serverless functions](/api)** used in production. Pretty nifty! (Note: the CLI will usually start the server at [http://localhost:3000/](http://localhost:3000/) instead of port 1337.)
|
||||
|
||||
### 🤯 Why does this look _way_ more complex than it needs to be?!
|
||||
|
||||
[Because it is.](https://www.jvt.me/talks/overengineering-your-personal-website/)
|
||||
|
||||
## 📜 Licenses
|
||||
## 📜 License
|
||||
|
||||

|
||||
|
||||
Site content (everything in [`content/notes`](content/notes/)) is published under the [**Creative Commons Attribution 4.0 International License**](LICENSE) (CC-BY-4.0), which means that you can copy, redistribute, remix, transform, and build upon the content for any purpose as long as you give appropriate credit.
|
||||
Site content (everything in [`/notes`](notes/)) is published under the [**Creative Commons Attribution 4.0 International License**](LICENSE) (CC-BY-4.0), which means that you can copy, redistribute, remix, transform, and build upon the content for any purpose as long as you give appropriate credit.
|
||||
|
||||
All original code in this repository is published under the [**MIT License**](https://opensource.org/licenses/MIT). [See licenses for third-party libraries here.](https://jarv.is/assets/third_party.txt)
|
||||
All original code in this repository is published under the [**MIT License**](https://opensource.org/licenses/MIT).
|
||||
|
100
api/report.js
@ -1,100 +0,0 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import fetch from "node-fetch";
|
||||
import getStream from "get-stream";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN || "",
|
||||
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || "",
|
||||
});
|
||||
|
||||
// this "proxy" to report-uri.com is temporary until I'm bored enough to make my own reporting API from scratch
|
||||
// https://report-uri.com/account/setup/
|
||||
const REPORT_URI_SUBDOMAIN = "jarvis";
|
||||
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
// permissive access control headers
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
// disable caching on both ends
|
||||
res.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Expires", 0);
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).send(); // 405 Method Not Allowed
|
||||
}
|
||||
|
||||
// start parsing body manually, since the serverless helper functions don't recognize `application/csp-report` and
|
||||
// `application/reports` as JSON to be parsed:
|
||||
// https://vercel.com/docs/runtimes#official-runtimes/node-js/node-js-request-and-response-objects/request-body
|
||||
const body = JSON.parse(await getStream(req));
|
||||
|
||||
// default to returning 400 Bad Request for an invalid POST request
|
||||
let statusCode = 400;
|
||||
|
||||
// TODO: add Expect-CT reporting endpoint
|
||||
if (typeof req.query.csp !== "undefined" && req.headers["content-type"].startsWith("application/csp-report")) {
|
||||
// send a CSP violation:
|
||||
// https://docs.report-uri.com/setup/csp/
|
||||
statusCode = await sendCsp(body, req.headers);
|
||||
} else if (req.headers["content-type"].startsWith("application/reports")) {
|
||||
// send a report:
|
||||
// https://docs.report-uri.com/setup/reporting-api/
|
||||
statusCode = await sendReport(body, req.headers);
|
||||
}
|
||||
|
||||
return res.status(statusCode).send();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
// log error to sentry, give it 2 seconds to finish sending
|
||||
Sentry.captureException(error);
|
||||
await Sentry.flush(2000);
|
||||
|
||||
return res.status(500).send(); // 500 Internal Server Error
|
||||
}
|
||||
};
|
||||
|
||||
const sendCsp = async (body, headers) => {
|
||||
// filter out any last invalid reports (JSON must have at least one csp-report object)
|
||||
if (Object.hasOwnProperty.call(body, "csp-report")) {
|
||||
const response = await fetch(`https://${REPORT_URI_SUBDOMAIN}.report-uri.com/r/d/csp/enforce`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/csp-report",
|
||||
"User-Agent": headers["user-agent"],
|
||||
Origin: headers["origin"],
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// API returns 201 Created if successful
|
||||
if (response.status !== 201) {
|
||||
console.error(`[CSP] ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return response.status;
|
||||
}
|
||||
|
||||
return 400; // 400 Bad Request
|
||||
};
|
||||
|
||||
const sendReport = async (body, headers) => {
|
||||
const response = await fetch(`https://${REPORT_URI_SUBDOMAIN}.report-uri.com/a/d/g`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/reports+json",
|
||||
"User-Agent": headers["user-agent"],
|
||||
Origin: headers["origin"],
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// API returns 201 Created if successful
|
||||
if (response.status !== 201) {
|
||||
console.error(`[REPORT] ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return response.status;
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ dateFormat "2006-01-02 15:04:05-0700" .Date }}
|
||||
description: ""
|
||||
image: ""
|
||||
draft: true
|
||||
---
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ dateFormat "2006-01-02 15:04:05-0700" .Date }}
|
||||
description: ""
|
||||
image: ""
|
||||
layout: etc
|
||||
draft: true
|
||||
---
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ dateFormat "2006-01-02 15:04:05-0700" .Date }}
|
||||
description: ""
|
||||
tags:
|
||||
-
|
||||
image: ""
|
||||
css: |
|
||||
div#content {
|
||||
|
||||
}
|
||||
aliases:
|
||||
-
|
||||
draft: true
|
||||
comments: true
|
||||
---
|
@ -1,13 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ dateFormat "2006-01-02 15:04:05-0700" .Date }}
|
||||
description: ""
|
||||
image: "thumb.png"
|
||||
layout: video
|
||||
resources:
|
||||
- src: "{{ .Name }}.mp4"
|
||||
- src: "{{ .Name }}.webm"
|
||||
- src: "subs.en.vtt"
|
||||
- src: "thumb.png"
|
||||
draft: true
|
||||
---
|
Before Width: | Height: | Size: 10 KiB |
@ -1,9 +0,0 @@
|
||||
import "./src/theme.js";
|
||||
import "./src/emoji.js";
|
||||
import "./src/hits.js";
|
||||
import "./src/clipboard.js";
|
||||
import "./src/anchor.js";
|
||||
import "./src/contact.js";
|
||||
import "./src/projects.js";
|
||||
|
||||
export default () => {};
|
@ -1,13 +0,0 @@
|
||||
// A super tiny script to restore dark mode off the bat (to hopefully avoid blinding flashes of white).
|
||||
// NOTE: This is inlined by Hugo/esbuild (see layouts/partials/head/restore-theme.html) instead of Webpack.
|
||||
// ANOTHER NOTE: Whenever this code is changed, its (minified) CSP hash *MUST* be updated manually in vercel.json.
|
||||
|
||||
import { getDarkPref, updateDOM } from "./src/utils/theme.js";
|
||||
|
||||
try {
|
||||
// Set root class and color-scheme property to dark if either...
|
||||
// - the user has explicitly toggled it previously.
|
||||
// - the user's OS is in dark mode.
|
||||
const pref = getDarkPref();
|
||||
updateDOM(pref === "true" || (!pref && window.matchMedia("(prefers-color-scheme: dark)").matches));
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
@ -1,23 +0,0 @@
|
||||
import { h, render } from "preact";
|
||||
|
||||
// react components:
|
||||
import Anchor from "./components/Anchor.js";
|
||||
|
||||
// loop through each h2, h3, h4 in this page's content area
|
||||
// prettier-ignore
|
||||
document.querySelectorAll([
|
||||
"div#content h2",
|
||||
"div#content h3",
|
||||
"div#content h4",
|
||||
]).forEach((heading) => {
|
||||
// don't add to elements without a pre-existing ID (e.g. `<h2 id="...">`)
|
||||
if (!heading.hasAttribute("id")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: little hacky hack to make the anchor appear AFTER the existing h tag
|
||||
const linkTarget = document.createElement("a");
|
||||
heading.appendChild(linkTarget);
|
||||
|
||||
render(<Anchor id={heading.getAttribute("id")} title={heading.textContent.trim()} />, heading, linkTarget);
|
||||
});
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 35"><path d="M22 11.06c0 6.44-5 7.44-5 13.44 0 3.1-3.12 3.36-5.5 3.36-2.05 0-6.59-.78-6.59-3.36 0-6-4.91-7-4.91-13.44C0 5.03 5.29.14 11.08.14 16.88.14 22 5.03 22 11.06z" fill="#CCCBCB"/><path d="M15.17 32.5c0 .83-2.24 2.5-4.17 2.5-1.93 0-4.17-1.67-4.17-2.5 0-.83 2.24-.5 4.17-.5 1.93 0 4.17-.33 4.17.5z" fill="#CCD6DD"/><path d="M15.7 10.3a1 1 0 0 0-1.4 0L11 13.58l-3.3-3.3a1 1 0 1 0-1.4 1.42l3.7 3.7V26a1 1 0 1 0 2 0V15.41l3.7-3.7a1 1 0 0 0 0-1.42z" fill="#7D7A72"/><path d="M17 31a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6h12v6z" fill="#99AAB5"/><path d="M5 32a1 1 0 0 1-.16-1.99l12-2a1 1 0 1 1 .33 1.97l-12 2A.93.93 0 0 1 5 32zm0-4a1 1 0 0 1-.16-1.99l12-2a1 1 0 1 1 .33 1.97l-12 2A.93.93 0 0 1 5 28z" fill="#CCD6DD"/></svg>
|
Before Width: | Height: | Size: 774 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 35"><path d="M22 11.06c0 6.44-5 7.44-5 13.44 0 3.1-3.12 3.36-5.5 3.36-2.05 0-6.59-.78-6.59-3.36 0-6-4.91-7-4.91-13.44C0 5.03 5.29.14 11.08.14 16.88.14 22 5.03 22 11.06z" fill="#FFD983"/><path d="M15.17 32.5c0 .83-2.24 2.5-4.17 2.5-1.93 0-4.17-1.67-4.17-2.5 0-.83 2.24-.5 4.17-.5 1.93 0 4.17-.33 4.17.5z" fill="#B9C9D9"/><path d="M15.7 10.3a1 1 0 0 0-1.4 0L11 13.58l-3.3-3.3a1 1 0 1 0-1.4 1.42l3.7 3.7V26a1 1 0 1 0 2 0V15.41l3.7-3.7a1 1 0 0 0 0-1.42z" fill="#FFCC4D"/><path d="M17 31a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6h12v6z" fill="#99AAB5"/><path d="M5 32a1 1 0 0 1-.16-1.99l12-2a1 1 0 1 1 .33 1.97l-12 2A.93.93 0 0 1 5 32zm0-4a1 1 0 0 1-.16-1.99l12-2a1 1 0 1 1 .33 1.97l-12 2A.93.93 0 0 1 5 28z" fill="#CCD6DD"/></svg>
|
Before Width: | Height: | Size: 774 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#D99E82" d="M36 32a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4v-9c0-2.209.791-3 3-3h30c2.209 0 3 .791 3 3v9z"></path><path fill="#662113" d="M25 20a7 7 0 1 1-14 0h14z"></path><path fill="#C1694F" d="M4 36h28a4 4 0 0 0 4-4H0a4 4 0 0 0 4 4z"></path><path fill="#DD2E44" d="M27.435 8.511 19.572.648a2.23 2.23 0 0 0-3.145 0L8.564 8.511C7.7 9.377 7.993 10 9.216 10H14v6a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-6h4.784c1.223 0 1.516-.623.651-1.489z"></path></svg>
|
Before Width: | Height: | Size: 507 B |
@ -1,15 +0,0 @@
|
||||
import { h, render } from "preact";
|
||||
|
||||
// react components:
|
||||
import CopyButton from "./components/CopyButton.js";
|
||||
|
||||
// loop through each code fence on page (if any)
|
||||
document.querySelectorAll("div.highlight").forEach((highlightDiv) => {
|
||||
// actual code element will have class "language-*" (even if plaintext)
|
||||
const codeElement = highlightDiv.querySelector('code[class^="language-"]');
|
||||
|
||||
if (codeElement) {
|
||||
// add the button as a sibling to the original Hugo block whose contents we're copying
|
||||
render(<CopyButton content={codeElement.textContent} timeout={2000} />, highlightDiv);
|
||||
}
|
||||
});
|
@ -1,21 +0,0 @@
|
||||
import { h } from "preact";
|
||||
import isTouchDevice from "is-touch-device";
|
||||
|
||||
const Anchor = (props) => {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
class="anchorjs-link"
|
||||
href={`#${props.id}`}
|
||||
title={`Jump to "${props.title}"`}
|
||||
aria-label={`Jump to "${props.title}"`}
|
||||
style={{
|
||||
// if this is a touchscreen, always show the "#" icon instead waiting for hover
|
||||
// NOTE: this is notoriously unreliable; see https://github.com/Modernizr/Modernizr/pull/2432
|
||||
opacity: isTouchDevice() ? 1 : null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Anchor;
|
@ -1,31 +0,0 @@
|
||||
import { h } from "preact";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import fetch from "unfetch";
|
||||
|
||||
// react components:
|
||||
import Loading from "./Loading.js";
|
||||
|
||||
const Counter = (props) => {
|
||||
const [hits, setHits] = useState();
|
||||
|
||||
// start fetching hits from API once slug is set
|
||||
useEffect(() => {
|
||||
fetch(`/api/hits/?slug=${encodeURIComponent(props.slug)}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => setHits(data.hits || 0));
|
||||
}, [props.slug]);
|
||||
|
||||
// show spinning loading indicator if data isn't fetched yet
|
||||
if (!hits) {
|
||||
return <Loading boxes={3} width={20} />;
|
||||
}
|
||||
|
||||
// we have data!
|
||||
return (
|
||||
<span title={`${hits.toLocaleString("en-US")} ${hits === 1 ? "view" : "views"}`}>
|
||||
{hits.toLocaleString("en-US")}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Counter;
|
@ -1,43 +0,0 @@
|
||||
import { h } from "preact";
|
||||
|
||||
const Loading = (props) => {
|
||||
// allow a custom number of pulsing boxes (defaults to 3)
|
||||
const boxes = props.boxes || 3;
|
||||
// each individual box's animation has a staggered start in corresponding order
|
||||
const animationTiming = props.timing || 0.1; // seconds
|
||||
// each box is just an empty div
|
||||
const divs = [];
|
||||
|
||||
for (let i = 0; i < boxes; i++) {
|
||||
divs.push(
|
||||
<div
|
||||
style={{
|
||||
// width of each box correlates with number of boxes (with a little padding)
|
||||
width: `${props.width / (boxes + 1)}px`,
|
||||
height: "100%",
|
||||
display: "inline-block",
|
||||
// see assets/sass/components/_animation.scss:
|
||||
animation: "loading 1.5s infinite ease-in-out both",
|
||||
"animation-delay": `${i * animationTiming}s`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="loading"
|
||||
style={{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.width / 2}px`,
|
||||
display: "inline-block",
|
||||
"text-align": "center",
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{divs}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
@ -1,81 +0,0 @@
|
||||
import { h } from "preact";
|
||||
import { intlFormat, formatDistanceToNowStrict } from "date-fns";
|
||||
import parseEmoji from "../utils/parseEmoji.js";
|
||||
|
||||
// react components:
|
||||
import { StarIcon, RepoForkedIcon } from "@primer/octicons-react";
|
||||
|
||||
const RepositoryCard = (props) => (
|
||||
<div class="github-card">
|
||||
<a class="repo-name" href={props.url} target="_blank" rel="noopener noreferrer">
|
||||
{props.name}
|
||||
</a>
|
||||
|
||||
{props.description && (
|
||||
<p
|
||||
class="repo-description"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: parseEmoji(props.description) }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class="repo-meta">
|
||||
{props.language && (
|
||||
<div class="repo-meta-item">
|
||||
<span class="repo-language-color" style={{ "background-color": props.language.color }} />
|
||||
<span>{props.language.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.stars > 0 && (
|
||||
<div class="repo-meta-item">
|
||||
<a
|
||||
href={`${props.url}/stargazers`}
|
||||
title={`${props.stars.toLocaleString("en-US")} ${props.stars === 1 ? "star" : "stars"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<StarIcon size={16} />
|
||||
<span>{props.stars.toLocaleString("en-US")}</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.forks > 0 && (
|
||||
<div class="repo-meta-item">
|
||||
<a
|
||||
href={`${props.url}/network/members`}
|
||||
title={`${props.forks.toLocaleString("en-US")} ${props.forks === 1 ? "fork" : "forks"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RepoForkedIcon size={16} />
|
||||
<span>{props.forks.toLocaleString("en-US")}</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
class="repo-meta-item"
|
||||
title={intlFormat(
|
||||
new Date(props.updatedAt),
|
||||
{
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
timeZoneName: "short",
|
||||
},
|
||||
{
|
||||
locale: "en-US",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>Updated {formatDistanceToNowStrict(new Date(props.updatedAt), { addSuffix: true })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RepositoryCard;
|
@ -1,36 +0,0 @@
|
||||
import { h, Fragment } from "preact";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import fetch from "unfetch";
|
||||
|
||||
// react components:
|
||||
import Loading from "./Loading.js";
|
||||
import RepositoryCard from "./RepositoryCard.js";
|
||||
|
||||
const RepositoryGrid = () => {
|
||||
const [repos, setRepos] = useState([]);
|
||||
|
||||
// start fetching repos from API immediately
|
||||
useEffect(() => {
|
||||
// API endpoint (sort by stars, limit to 12)
|
||||
fetch("/api/projects/?top&limit=12")
|
||||
.then((response) => response.json())
|
||||
.then((data) => setRepos(data || []));
|
||||
}, []);
|
||||
|
||||
// show spinning loading indicator if data isn't fetched yet
|
||||
if (repos.length === 0) {
|
||||
return <Loading boxes={3} width={40} style={{ margin: "0.7em auto" }} />;
|
||||
}
|
||||
|
||||
// we have data!
|
||||
return (
|
||||
<>
|
||||
{repos.map((repo) => (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<RepositoryCard {...repo} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryGrid;
|
@ -1,9 +0,0 @@
|
||||
import { h, render } from "preact";
|
||||
|
||||
// react components:
|
||||
import ContactForm from "./components/ContactForm.js";
|
||||
|
||||
// don't continue if there isn't a contact form on this page
|
||||
if (typeof window !== "undefined" && document.querySelector(".layout-contact #contact-form-wrapper")) {
|
||||
render(<ContactForm />, document.querySelector(".layout-contact #contact-form-wrapper"));
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import parseEmoji from "./utils/parseEmoji.js";
|
||||
|
||||
// apply to the entire body automatically on load...
|
||||
parseEmoji(document.body);
|
@ -1,23 +0,0 @@
|
||||
import { h, render } from "preact";
|
||||
import canonicalUrl from "get-canonical-url";
|
||||
|
||||
// react components:
|
||||
import Counter from "./components/Counter.js";
|
||||
|
||||
// page must have a div#meta-hits-counter element to continue
|
||||
if (typeof window !== "undefined" && document.querySelector(".layout-single #meta-hits-counter")) {
|
||||
// use <link rel="canonical"> to deduce a consistent identifier for this page
|
||||
const canonical = canonicalUrl({
|
||||
normalize: true,
|
||||
normalizeOptions: {
|
||||
removeTrailingSlash: true,
|
||||
removeQueryParameters: true,
|
||||
stripHash: true,
|
||||
},
|
||||
});
|
||||
|
||||
// get path and strip beginning and ending forward slash
|
||||
const slug = new URL(canonical).pathname.replace(/^\/|\/$/g, "") || "/";
|
||||
|
||||
render(<Counter slug={slug} />, document.querySelector(".layout-single #meta-hits-counter"));
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { h, render } from "preact";
|
||||
|
||||
// react components:
|
||||
import RepositoryGrid from "./components/RepositoryGrid.js";
|
||||
|
||||
// detect if these cards are wanted on this page (only /projects)
|
||||
if (typeof window !== "undefined" && document.querySelector(".layout-projects #github-cards")) {
|
||||
render(<RepositoryGrid />, document.querySelector(".layout-projects #github-cards"));
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { h, render } from "preact";
|
||||
|
||||
// react components:
|
||||
import ThemeToggle from "./components/ThemeToggle.js";
|
||||
|
||||
// finally render the nifty lightbulb in the header
|
||||
if (typeof window !== "undefined" && document.querySelector(".theme-toggle")) {
|
||||
render(<ThemeToggle />, document.querySelector(".theme-toggle"));
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import fetch from "unfetch";
|
||||
|
||||
const getData = (url) =>
|
||||
fetch(url)
|
||||
.then((response) => response.json())
|
||||
.then((data) => data || []);
|
||||
|
||||
export default getData;
|
@ -1,8 +0,0 @@
|
||||
import * as imagemoji from "imagemoji";
|
||||
|
||||
const parseEmoji = (what) =>
|
||||
// we're hosting twemojis locally instead of from Twitter's CDN
|
||||
imagemoji.parse(what, (icon) => `/assets/emoji/${icon}.svg`);
|
||||
|
||||
// reuse this so the URL above doesn't need to be changed in multiple places
|
||||
export default parseEmoji;
|
@ -1,25 +0,0 @@
|
||||
// class names (`<html class="...">`) for each theme
|
||||
const lightClass = "light";
|
||||
const darkClass = "dark";
|
||||
|
||||
// store preference in local storage
|
||||
const storageKey = "dark_mode";
|
||||
export const getDarkPref = () => localStorage.getItem(storageKey);
|
||||
export const setDarkPref = (pref) => localStorage.setItem(storageKey, pref);
|
||||
|
||||
// use the `<html class="...">` as a hint to what the theme was set to outside of the button component
|
||||
// there's probably (definitely) a cleaner way to do this..?
|
||||
export const isDark = () => document.documentElement.classList.contains(darkClass);
|
||||
|
||||
// sets appropriate `<html class="...">` and `color-scheme` CSS property
|
||||
export const updateDOM = (dark) => {
|
||||
const root = document.documentElement;
|
||||
|
||||
// set `<html class="...">`
|
||||
root.classList.remove(darkClass, lightClass);
|
||||
root.classList.add(dark ? darkClass : lightClass);
|
||||
|
||||
// ...and the CSS `color-scheme` property:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme
|
||||
root.style.colorScheme = dark ? "dark" : "light";
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
@use "sass:color";
|
||||
|
||||
@use "settings";
|
||||
|
||||
// Figure out the color of the "transparent" link underlines:
|
||||
@function underline-hack($color, $background: #ffffff) {
|
||||
// Calculate underline color by mix()'ing it with a given background to give the impression of opacity but with much
|
||||
// better efficiency and compatibility.
|
||||
$color-transparentized: color.mix($color, $background, settings.$link-underline-opacity);
|
||||
|
||||
// Return a "gradient" as a hack to get the fancy underline to wrap:
|
||||
// https://www.dannyguo.com/blog/animated-multiline-link-underlines-with-css/
|
||||
@return linear-gradient($color-transparentized, $color-transparentized);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
@use "sass:list";
|
||||
|
||||
// The maximum width of the content area:
|
||||
$max-width: 865px;
|
||||
|
||||
// Width at which to switch to mobile styles:
|
||||
$responsive-width: 800px;
|
||||
|
||||
// Web fonts:
|
||||
$webfont-sans: "Inter";
|
||||
$webfont-sans-variable: "Inter var";
|
||||
$webfont-mono: "Roboto Mono";
|
||||
$webfont-mono-variable: "Roboto Mono var";
|
||||
|
||||
// System fonts:
|
||||
// https://primer.style/design/foundations/typography#font-stack
|
||||
// stylelint-disable-next-line value-keyword-case
|
||||
$system-fonts-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
$system-fonts-mono: ui-monospace, "SFMono-Regular", "Consolas", "Liberation Mono", "Menlo", "Courier", monospace;
|
||||
|
||||
// Prefer web fonts with system fonts as backup:
|
||||
$font-stack-sans: list.join($webfont-sans, $system-fonts-sans);
|
||||
$font-stack-sans-variable: list.join($webfont-sans-variable, $system-fonts-sans);
|
||||
$font-stack-mono: list.join($webfont-mono, $system-fonts-mono);
|
||||
$font-stack-mono-variable: list.join($webfont-mono-variable, $system-fonts-mono);
|
||||
|
||||
// Fancy link underline settings:
|
||||
$link-underline-opacity: 40%;
|
||||
$link-underline-size: 2px;
|
@ -1,55 +0,0 @@
|
||||
@use "sass:map";
|
||||
|
||||
// Takes a map of CSS properties and theme keys (see below) and set both html.light and html.dark selectors.
|
||||
// ex. @include themes.themed((color: "text", background-color: "background-inner"));
|
||||
@mixin themed($properties) {
|
||||
// keep track of the original selector(s) calling this mixin for below
|
||||
$selectors: #{&};
|
||||
|
||||
// add corresponding html.light and html.dark root selectors
|
||||
@each $theme, $map in $themes {
|
||||
@at-root html.#{$theme} {
|
||||
#{$selectors} {
|
||||
@each $property, $color in $properties {
|
||||
#{$property}: map.get($map, $color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------
|
||||
|
||||
// Dark & Light Themes
|
||||
$themes: (
|
||||
light: (
|
||||
background-inner: #ffffff,
|
||||
background-outer: #fcfcfc,
|
||||
text: #202020,
|
||||
medium-dark: #515151,
|
||||
medium: #5e5e5e,
|
||||
medium-light: #757575,
|
||||
light: #d2d2d2,
|
||||
kinda-light: #e3e3e3,
|
||||
super-light: #f4f4f4,
|
||||
super-duper-light: #fbfbfb,
|
||||
links: #0e6dc2,
|
||||
success: #44a248,
|
||||
error: #ff1b1b,
|
||||
),
|
||||
dark: (
|
||||
background-inner: #1e1e1e,
|
||||
background-outer: #252525,
|
||||
text: #f1f1f1,
|
||||
medium-dark: #d7d7d7,
|
||||
medium: #b1b1b1,
|
||||
medium-light: #959595,
|
||||
light: #646464,
|
||||
kinda-light: #535353,
|
||||
super-light: #272727,
|
||||
super-duper-light: #1f1f1f,
|
||||
links: #88c7ff,
|
||||
success: #78df55,
|
||||
error: #ff5151,
|
||||
),
|
||||
);
|
@ -1,100 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
.wave,
|
||||
.beat {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wave {
|
||||
animation: wave 5s infinite;
|
||||
animation-delay: 1s;
|
||||
transform-origin: 65% 80%;
|
||||
}
|
||||
|
||||
.beat {
|
||||
animation: beat 10s infinite; // 6 bpm, call 911 if you see this please.
|
||||
animation-delay: 7.5s; // offset from wave animation
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
// stylelint-disable rule-empty-line-before
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
5% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
// stylelint-enable rule-empty-line-before
|
||||
|
||||
// pause for 3.5 out of 5 seconds
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes beat {
|
||||
// stylelint-disable rule-empty-line-before
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
2% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
4% {
|
||||
transform: scale(1);
|
||||
}
|
||||
6% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
8% {
|
||||
transform: scale(1);
|
||||
}
|
||||
// stylelint-enable rule-empty-line-before
|
||||
|
||||
// pause for ~9 out of 10 seconds
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// modified from https://tobiasahlin.com/spinkit/
|
||||
@keyframes loading {
|
||||
// stylelint-disable rule-empty-line-before
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
transform: scale(0.6);
|
||||
}
|
||||
// stylelint-enable rule-empty-line-before
|
||||
}
|
||||
|
||||
// https://web.dev/prefers-reduced-motion/#(bonus)-forcing-reduced-motion-on-all-websites
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
animation-delay: -1ms !important;
|
||||
animation-duration: 1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Main content
|
||||
div#content {
|
||||
font-size: 0.925em;
|
||||
letter-spacing: -0.004em;
|
||||
line-height: 1.7;
|
||||
|
||||
b,
|
||||
strong {
|
||||
letter-spacing: 0.008em; // not sure why the discrepancy between weights
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1.5em;
|
||||
border-left: 3px solid;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-dark",
|
||||
border-color: "links",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
letter-spacing: 0.001em;
|
||||
line-height: 1.5;
|
||||
|
||||
&:hover > .anchorjs-link {
|
||||
opacity: 1; // '#' link appears on hover over entire sub-heading line
|
||||
}
|
||||
}
|
||||
|
||||
// special bottom border for H2s
|
||||
h2 {
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
border-color: "kinda-light",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// AnchorJS styles
|
||||
.anchorjs-link {
|
||||
margin: 0 0.25em;
|
||||
padding: 0 0.25em;
|
||||
background: none;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
opacity: 0; // overridden by JS on mobile devices
|
||||
user-select: none;
|
||||
|
||||
&::before {
|
||||
content: "\0023"; // pound sign
|
||||
}
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-light",
|
||||
)
|
||||
);
|
||||
|
||||
&:hover {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "links",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 1em auto;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
margin-top: 0.5em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 1.5em;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1.5em auto;
|
||||
height: 2px;
|
||||
border: 0;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
background-color: "light",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
// stylelint-disable-next-line block-no-empty
|
||||
@mixin responsive() {
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
// External social embeds
|
||||
.embed {
|
||||
&.codepen {
|
||||
iframe {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.tweet {
|
||||
margin: 0.5em 0;
|
||||
|
||||
> .twitter-tweet-rendered {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.youtube {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
margin: 1em auto;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
iframe.youtube-player {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.video {
|
||||
video {
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.vimeo {
|
||||
padding: 75% 0 0 0;
|
||||
position: relative;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.gh-buttons {
|
||||
text-align: center;
|
||||
|
||||
// GitHub's script replaces <a> with <span>, so cover both
|
||||
> a,
|
||||
> span {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
// stylelint-disable-next-line block-no-empty
|
||||
@mixin responsive() {
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
@use "../abstracts/settings";
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Global Footer Styles
|
||||
footer {
|
||||
width: 100%;
|
||||
letter-spacing: -0.005em;
|
||||
padding: 1.25em 1.5em;
|
||||
border-top: 1px solid;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-dark",
|
||||
border-color: "kinda-light",
|
||||
)
|
||||
);
|
||||
|
||||
a {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-dark",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
div.footer-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: settings.$max-width;
|
||||
margin: 0 auto;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85em;
|
||||
line-height: 2.3;
|
||||
}
|
||||
|
||||
// underline View Source link
|
||||
a#footer-view-source {
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-dark",
|
||||
border-color: "light",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
footer {
|
||||
padding: 1em 1.25em 0 1.25em;
|
||||
|
||||
// stack columns on left instead of flexboxing across
|
||||
div.footer-row {
|
||||
display: block;
|
||||
line-height: 2;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
@use "sass:map";
|
||||
|
||||
@use "../abstracts/settings";
|
||||
@use "../abstracts/themes";
|
||||
@use "../abstracts/functions";
|
||||
|
||||
// Global Styles
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
scroll-behavior: smooth;
|
||||
font-family: settings.$font-stack-sans;
|
||||
font-kerning: normal;
|
||||
font-variant-ligatures: normal;
|
||||
font-feature-settings: "kern", "liga", "calt", "clig", "ss01";
|
||||
|
||||
// global base font size:
|
||||
font-size: 0.975em;
|
||||
line-height: 1.5;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
background-color: "background-outer",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre,
|
||||
.monospace {
|
||||
font-family: settings.$font-stack-mono;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
// override above font-family if browser supports variable fonts
|
||||
// https://caniuse.com/#feat=variable-fonts
|
||||
@supports (font-variation-settings: normal) {
|
||||
body {
|
||||
font-family: settings.$font-stack-sans-variable;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre,
|
||||
.monospace {
|
||||
font-family: settings.$font-stack-mono-variable;
|
||||
}
|
||||
|
||||
// Chrome doesn't automatically slant multi-axis Inter var, for some reason.
|
||||
// Adding "slnt" -10 fixes Chrome but then over-italicizes in Firefox. AHHHHHHHHHH.
|
||||
em {
|
||||
font-style: normal !important;
|
||||
font-variation-settings: "ital" 1, "slnt" -10;
|
||||
|
||||
// Roboto Mono doesn't have this problem, but the above fix breaks it, of course.
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre,
|
||||
.monospace {
|
||||
font-style: italic !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// this is what's extended by different layouts (in ../pages)
|
||||
div.layout {
|
||||
max-width: settings.$max-width;
|
||||
margin: 0 auto;
|
||||
display: block; // https://stackoverflow.com/questions/28794718/max-width-not-working-for-ie-11
|
||||
}
|
||||
|
||||
// stretch background for entire width of content area
|
||||
main {
|
||||
width: 100%;
|
||||
padding: 0 1.5em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "text",
|
||||
background-color: "background-inner",
|
||||
)
|
||||
);
|
||||
|
||||
a {
|
||||
background-position: 0% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0% settings.$link-underline-size;
|
||||
padding-bottom: settings.$link-underline-size;
|
||||
transition: background-size 0.25s ease-in-out;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "links",
|
||||
)
|
||||
);
|
||||
|
||||
// cool link underlines via messy SCSS hacking (see ../abstracts/_functions)
|
||||
@each $theme, $map in themes.$themes {
|
||||
@at-root html.#{$theme} #{&} {
|
||||
background-image: functions.underline-hack(map.get($map, "links"), map.get($map, "background-inner"));
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-size: 100% settings.$link-underline-size;
|
||||
}
|
||||
|
||||
// set an anchor's class to `no-underline` to disable all of this
|
||||
&.no-underline {
|
||||
background: none !important;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// page titles
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
letter-spacing: -0.005em;
|
||||
|
||||
a {
|
||||
// disable fancy underline without `.no-underline`
|
||||
color: inherit !important;
|
||||
background: none !important;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make SVG twemojis relative to surrounding text
|
||||
// https://github.com/twitter/twemoji#inline-styles
|
||||
.emoji {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
margin: 0 0.05em;
|
||||
vertical-align: -0.22em;
|
||||
border: 0;
|
||||
display: inline-block;
|
||||
|
||||
// have cursor act like it's hovering a regular unicode emoji, especially since twemojis can still be copied/pasted
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
// need to manually unset text cursor above when emoji's within a link
|
||||
a .emoji {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
// pulsating loading spinner
|
||||
div.loading > div {
|
||||
@include themes.themed(
|
||||
(
|
||||
background-color: "medium-light",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
body {
|
||||
// Safari iOS menu bar reappears below 44px:
|
||||
// https://www.eventbrite.com/engineering/mobile-safari-why/
|
||||
padding-bottom: 45px !important;
|
||||
|
||||
// Allows you to scroll below the viewport; default value is visible
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-left: 1.25em;
|
||||
padding-right: 1.25em;
|
||||
}
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
@use "../abstracts/settings";
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Global Header Styles
|
||||
header {
|
||||
width: 100%;
|
||||
padding: 0.7em 1.5em;
|
||||
border-bottom: 1px solid;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
border-color: "kinda-light",
|
||||
)
|
||||
);
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
max-width: settings.$max-width;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
a#header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-dark",
|
||||
)
|
||||
);
|
||||
|
||||
img#header-selfie {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
user-select: none;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
border-color: "light",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
span#header-name {
|
||||
margin: 0 0.6em;
|
||||
font-size: 1.25em;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "links",
|
||||
)
|
||||
);
|
||||
|
||||
img#header-selfie {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal emoji links
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin-left: 1.75em;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-dark",
|
||||
)
|
||||
);
|
||||
|
||||
&:hover {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "links",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
span {
|
||||
&.header-menu-icon {
|
||||
width: 1.3em;
|
||||
font-size: 1.3em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.header-menu-text {
|
||||
font-size: 0.95em;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.002em;
|
||||
margin-left: 0.65em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode toggle
|
||||
&.theme-toggle {
|
||||
width: 1.3em;
|
||||
margin-left: 1.4em;
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
margin: 0.3em -0.3em 0 0;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 1.56em; // 24.33px, don't ask
|
||||
height: 1.56em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no margin on the first child to make more room on narrow windows (before responsiveness kicks in)
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
header {
|
||||
padding: 0.5em 1.25em;
|
||||
|
||||
nav {
|
||||
a#header-logo {
|
||||
img#header-selfie {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
span#header-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
img#header-selfie {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
font-size: 1.6em;
|
||||
|
||||
li {
|
||||
margin-left: 1.15em;
|
||||
|
||||
a.header-menu-item {
|
||||
span.header-menu-icon {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
// hide text next to emojis on mobile
|
||||
span.header-menu-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode toggle
|
||||
&.theme-toggle {
|
||||
width: 0.9em;
|
||||
margin-left: 1em;
|
||||
|
||||
button {
|
||||
margin-right: -0.2em; // weirdness w/ svg ratio
|
||||
|
||||
svg {
|
||||
width: 1.08em; // ~27px, don't ask
|
||||
height: 1.08em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,301 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// all code
|
||||
code {
|
||||
font-size: 0.925em;
|
||||
letter-spacing: normal;
|
||||
page-break-inside: avoid; // stylelint-disable-line plugin/no-unsupported-browser-features
|
||||
}
|
||||
|
||||
// inline code in paragraphs/elsewhere (single backticks)
|
||||
:not(pre) > code {
|
||||
padding: 0.075em 0.25em;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
// allow for inline code in post/page titles -- override above styles and match title
|
||||
.title code {
|
||||
font-size: 1em;
|
||||
background: none !important;
|
||||
border: 0 !important;
|
||||
margin: 0 0.075em !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
// same as above but different font weight for big titles
|
||||
h1.title code {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// code fences
|
||||
div.highlight {
|
||||
line-height: 1.6;
|
||||
max-width: 100%;
|
||||
overflow-x: scroll;
|
||||
margin: 1em 0;
|
||||
border: 1px solid;
|
||||
position: relative;
|
||||
|
||||
pre {
|
||||
padding-left: 1.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
// code fences without syntax highlighting
|
||||
> pre > code {
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
button.copy-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.75em;
|
||||
border-width: 0 0 1px 1px;
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "links",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.octicon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.octicon-check {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "success",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// global table styles for line numbers and font styles
|
||||
.chroma {
|
||||
.lntable {
|
||||
border-spacing: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
width: auto;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// columns
|
||||
.lntd {
|
||||
vertical-align: top;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
|
||||
// add right padding to second column (the code) when fence overflows
|
||||
&:nth-of-type(2) {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
// line numbers
|
||||
.ln,
|
||||
.lnt {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.gh,
|
||||
.gi,
|
||||
.gu {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.kd,
|
||||
.vc,
|
||||
.vg,
|
||||
.vi,
|
||||
.ge {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.gl {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// Syntax Highlighting (light) - modified from Monokai Light: https://github.com/mlgill/pygments-style-monokailight
|
||||
html.light {
|
||||
div.highlight,
|
||||
button.copy-button,
|
||||
:not(pre) > code {
|
||||
background-color: #fbfbfb;
|
||||
border-color: #d5d5d5;
|
||||
}
|
||||
|
||||
div.highlight,
|
||||
button.copy-button {
|
||||
color: #313131;
|
||||
}
|
||||
|
||||
.chroma {
|
||||
.k,
|
||||
.kc,
|
||||
.kd,
|
||||
.kp,
|
||||
.kr,
|
||||
.kt,
|
||||
.no {
|
||||
color: #029cb9;
|
||||
}
|
||||
|
||||
.na,
|
||||
.nc,
|
||||
.nd,
|
||||
.ne,
|
||||
.nf,
|
||||
.nx {
|
||||
color: #70a800;
|
||||
}
|
||||
|
||||
.nt,
|
||||
.o,
|
||||
.ow,
|
||||
.kn {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.l,
|
||||
.se,
|
||||
.m,
|
||||
.mb,
|
||||
.mf,
|
||||
.mh,
|
||||
.mi,
|
||||
.il,
|
||||
.mo {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.ld,
|
||||
.s,
|
||||
.sa,
|
||||
.sb,
|
||||
.sc,
|
||||
.dl,
|
||||
.sd,
|
||||
.s2,
|
||||
.sh,
|
||||
.si,
|
||||
.sx,
|
||||
.sr,
|
||||
.s1,
|
||||
.ss {
|
||||
color: #d88200;
|
||||
}
|
||||
|
||||
.c,
|
||||
.ch,
|
||||
.cm,
|
||||
.c1,
|
||||
.cs,
|
||||
.cp,
|
||||
.cpf {
|
||||
color: #75715e;
|
||||
}
|
||||
|
||||
.lnt {
|
||||
color: #8a8a8a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Syntax Highlighting (dark) - modified from Dracula: https://github.com/dracula/pygments
|
||||
html.dark {
|
||||
div.highlight,
|
||||
button.copy-button,
|
||||
:not(pre) > code {
|
||||
background-color: #252525;
|
||||
border-color: #535353;
|
||||
}
|
||||
|
||||
div.highlight,
|
||||
button.copy-button {
|
||||
color: #e4e4e4;
|
||||
}
|
||||
|
||||
.chroma {
|
||||
.k,
|
||||
.kc,
|
||||
.kd,
|
||||
.kp,
|
||||
.kr,
|
||||
.kt,
|
||||
.no {
|
||||
color: #3b9dd2;
|
||||
}
|
||||
|
||||
.na,
|
||||
.nc,
|
||||
.nd,
|
||||
.ne,
|
||||
.nf,
|
||||
.nx {
|
||||
color: #78df55;
|
||||
}
|
||||
|
||||
.nt,
|
||||
.o,
|
||||
.ow,
|
||||
.kn {
|
||||
color: #f95757;
|
||||
}
|
||||
|
||||
.l,
|
||||
.se,
|
||||
.m,
|
||||
.mb,
|
||||
.mf,
|
||||
.mh,
|
||||
.mi,
|
||||
.il,
|
||||
.mo {
|
||||
color: #d588fb;
|
||||
}
|
||||
|
||||
.ld,
|
||||
.s,
|
||||
.sa,
|
||||
.sb,
|
||||
.sc,
|
||||
.dl,
|
||||
.sd,
|
||||
.s2,
|
||||
.sh,
|
||||
.si,
|
||||
.sx,
|
||||
.sr,
|
||||
.s1,
|
||||
.ss {
|
||||
color: #fd992a;
|
||||
}
|
||||
|
||||
.c,
|
||||
.ch,
|
||||
.cm,
|
||||
.c1,
|
||||
.cs,
|
||||
.cp,
|
||||
.cpf {
|
||||
color: #929292;
|
||||
}
|
||||
|
||||
.lnt {
|
||||
color: #b1b1b1;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
@use "abstracts/settings";
|
||||
|
||||
// Global Styles
|
||||
@use "abstracts/reset";
|
||||
@use "abstracts/typography";
|
||||
@use "components/global";
|
||||
@use "components/header";
|
||||
@use "components/footer";
|
||||
@use "components/content";
|
||||
@use "components/embeds";
|
||||
|
||||
// Pages
|
||||
@use "pages/home";
|
||||
@use "pages/list";
|
||||
@use "pages/single";
|
||||
@use "pages/videos";
|
||||
@use "pages/etc";
|
||||
@use "pages/projects";
|
||||
@use "pages/contact";
|
||||
@use "pages/fourOhFour";
|
||||
|
||||
// Miscellaneous
|
||||
@use "components/syntax";
|
||||
@use "components/animation";
|
||||
|
||||
// Responsive Awesomeness
|
||||
@media screen and (max-width: settings.$responsive-width) {
|
||||
@include global.responsive;
|
||||
@include header.responsive;
|
||||
@include footer.responsive;
|
||||
@include content.responsive;
|
||||
@include embeds.responsive;
|
||||
|
||||
@include home.responsive;
|
||||
@include list.responsive;
|
||||
@include single.responsive;
|
||||
@include videos.responsive;
|
||||
@include etc.responsive;
|
||||
@include projects.responsive;
|
||||
@include contact.responsive;
|
||||
@include fourOhFour.responsive;
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Contact Styles
|
||||
div.layout-contact {
|
||||
max-width: 600px;
|
||||
padding: 1.5em 0;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0.4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
code {
|
||||
background: none !important;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
word-spacing: -0.175em;
|
||||
white-space: normal; // re-enable "word" wrapping
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.8em;
|
||||
margin: 0.6em 0;
|
||||
border: 2px solid;
|
||||
border-radius: 0.3em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "text",
|
||||
background-color: "super-duper-light",
|
||||
border-color: "light",
|
||||
)
|
||||
);
|
||||
|
||||
&:focus {
|
||||
outline: none; // disable browsers' outer border
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
border-color: "links",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 12em;
|
||||
min-height: 6em;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5;
|
||||
|
||||
// allow vertical resizing & disable horizontal
|
||||
resize: vertical; // stylelint-disable-line plugin/no-unsupported-browser-features
|
||||
}
|
||||
|
||||
div#contact-form-action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 3.75em;
|
||||
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
padding: 1em 1.25em;
|
||||
margin-right: 1.5em;
|
||||
border: 0;
|
||||
border-radius: 0.3em;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "text",
|
||||
background-color: "kinda-light",
|
||||
)
|
||||
);
|
||||
|
||||
&:hover {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "super-duper-light",
|
||||
background-color: "links",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.emoji {
|
||||
margin-left: 0;
|
||||
margin-right: 0.4em;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
span.contact-form-result {
|
||||
font-weight: 600;
|
||||
|
||||
&#contact-form-result-success {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "success",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&#contact-form-result-error {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "error",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hcaptcha widget
|
||||
div#contact-form-captcha {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
div#contact-form-md-info {
|
||||
font-size: 0.825em;
|
||||
line-height: 1.75;
|
||||
|
||||
a {
|
||||
// disable fancy underline without `.no-underline`
|
||||
background: none !important;
|
||||
padding: 0;
|
||||
|
||||
&:first-of-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
// stylelint-disable-next-line block-no-empty
|
||||
@mixin responsive() {
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
// Video Styles
|
||||
div.layout-etc {
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 1.5em;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
div.layout-etc {
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
padding: 0 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
// 404 Styles
|
||||
div.layout-404 {
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 1.5em;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0.75em 0 0.5em 0;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
// stylelint-disable-next-line block-no-empty
|
||||
@mixin responsive() {
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
@use "sass:map";
|
||||
|
||||
@use "../abstracts/themes";
|
||||
@use "../abstracts/functions";
|
||||
|
||||
// Colorful Homepage
|
||||
$colors-home: (
|
||||
boston: (
|
||||
light: #fb4d42,
|
||||
dark: #ff5146,
|
||||
),
|
||||
js-vanilla: (
|
||||
light: #f48024,
|
||||
dark: #e18431,
|
||||
),
|
||||
js-frameworks: (
|
||||
light: #1091b3,
|
||||
dark: #6fcbe3,
|
||||
),
|
||||
jamstack: (
|
||||
light: #04a699,
|
||||
dark: #08bbac,
|
||||
),
|
||||
node: (
|
||||
light: #6fbc4e,
|
||||
dark: #84d95f,
|
||||
),
|
||||
golang: (
|
||||
light: #00acd7,
|
||||
dark: #2ad1fb,
|
||||
),
|
||||
php: (
|
||||
light: #8892bf,
|
||||
dark: #a4afe3,
|
||||
),
|
||||
ruby: (
|
||||
light: #d34135,
|
||||
dark: #f95a4d,
|
||||
),
|
||||
python: (
|
||||
light: #fea500,
|
||||
dark: #ffbb3c,
|
||||
),
|
||||
infosec: (
|
||||
light: #00b81a,
|
||||
dark: #57f06d,
|
||||
),
|
||||
server: (
|
||||
light: #0098ec,
|
||||
dark: #43b9fb,
|
||||
),
|
||||
devops: (
|
||||
light: #ff6200,
|
||||
dark: #f46c16,
|
||||
),
|
||||
frontend: (
|
||||
light: #4169e1,
|
||||
dark: #8ca9ff,
|
||||
),
|
||||
backend: (
|
||||
light: #9932cc,
|
||||
dark: #d588fb,
|
||||
),
|
||||
birthday: (
|
||||
light: #e40088,
|
||||
dark: #fd40b1,
|
||||
),
|
||||
github: (
|
||||
light: #8d4eff,
|
||||
dark: #a379f0,
|
||||
),
|
||||
linkedin: (
|
||||
light: #0073b1,
|
||||
dark: #3b9dd2,
|
||||
),
|
||||
twitter: (
|
||||
light: #00acee,
|
||||
dark: #3bc9ff,
|
||||
),
|
||||
email: (
|
||||
light: #de0c0c,
|
||||
dark: #ff5050,
|
||||
),
|
||||
pgp: (
|
||||
light: #757575,
|
||||
dark: #959595,
|
||||
),
|
||||
sms: (
|
||||
light: #6fcc01,
|
||||
dark: #8edb34,
|
||||
),
|
||||
news-1: (
|
||||
light: #ff1b1b,
|
||||
dark: #f06060,
|
||||
),
|
||||
news-2: (
|
||||
light: #f78200,
|
||||
dark: #fd992a,
|
||||
),
|
||||
news-3: (
|
||||
light: #f2b702,
|
||||
dark: #ffcc2e,
|
||||
),
|
||||
news-4: (
|
||||
light: #5ebd3e,
|
||||
dark: #78df55,
|
||||
),
|
||||
news-5: (
|
||||
light: #009cdf,
|
||||
dark: #29bfff,
|
||||
),
|
||||
news-6: (
|
||||
light: #3e49bb,
|
||||
dark: #7b87ff,
|
||||
),
|
||||
news-7: (
|
||||
light: #973999,
|
||||
dark: #db60dd,
|
||||
),
|
||||
);
|
||||
$color-serverless: #87cef7;
|
||||
$icon-wand: 'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="20" height="20"><g fill="none"><path fill="#292F33" d="M2.651 6.073l26.275 26.276c.391.391 2.888-2.107 2.497-2.497L5.148 3.576c-.39-.391-2.888 2.107-2.497 2.497z"/><path fill="#66757F" d="M29.442 31.23L3.146 4.934l.883-.883 26.296 26.296z"/><path fill="#E1E8ED" d="M33.546 33.483l-.412.412-.671.671a.967.967 0 01-.255.169.988.988 0 01-1.159-.169l-2.102-2.102.495-.495.883-.883 1.119-1.119 2.102 2.102a.999.999 0 010 1.414zM4.029 4.79l-.883.883-.495.495L.442 3.96a.988.988 0 01-.169-1.159.967.967 0 01.169-.255l.671-.671.412-.412a.999.999 0 011.414 0l2.208 2.208L4.029 4.79z"/><path fill="#F5F8FA" d="M30.325 30.497l2.809 2.809-.671.671a.967.967 0 01-.255.169l-2.767-2.767.884-.882zM3.146 5.084L.273 2.211a.967.967 0 01.169-.255l.671-.671 2.916 2.916-.883.883z"/><path fill="#FFAC33" d="M27.897 10.219l1.542.571.6 2.2a.667.667 0 001.287 0l.6-2.2 1.542-.571a.665.665 0 000-1.25l-1.534-.568-.605-2.415a.667.667 0 00-1.293 0l-.605 2.415-1.534.568a.665.665 0 000 1.25m-16.936 9.628l2.61.966.966 2.61a1.103 1.103 0 002.07 0l.966-2.61 2.609-.966a1.103 1.103 0 000-2.07l-2.609-.966-.966-2.61a1.105 1.105 0 00-2.07 0l-.966 2.61-2.61.966a1.104 1.104 0 000 2.07M23.13 4.36l1.383.512.512 1.382a.585.585 0 001.096 0l.512-1.382 1.382-.512a.584.584 0 000-1.096l-1.382-.512-.512-1.382a.585.585 0 00-1.096 0l-.512 1.382-1.383.512a.585.585 0 000 1.096"/></g></svg>';
|
||||
|
||||
// ----------------
|
||||
|
||||
// Home Styles
|
||||
div.layout-home {
|
||||
font-size: 1em;
|
||||
padding-top: 1.2em;
|
||||
padding-bottom: 0.6em;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.3em -0.03em; // TODO: why is this indented slightly?
|
||||
font-size: 1.8em;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.014em;
|
||||
|
||||
.emoji {
|
||||
margin-left: 0.1em; // a little extra social distancing for the hand
|
||||
}
|
||||
|
||||
.wave {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0.5em 0 0.5em -0.03em; // TODO: why is this indented slightly?
|
||||
font-size: 1.35em;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.022em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.85em 0;
|
||||
letter-spacing: -0.009em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
sup {
|
||||
letter-spacing: normal;
|
||||
position: relative;
|
||||
|
||||
&#key {
|
||||
font-size: 0.65em;
|
||||
word-spacing: -0.3em;
|
||||
margin-right: 0.1em;
|
||||
|
||||
.emoji {
|
||||
vertical-align: -0.2em; // magic number to align with text :\
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// easter egg emoji cursor
|
||||
a#birthday {
|
||||
&:hover {
|
||||
cursor: url($icon-wand) 0 0, auto;
|
||||
}
|
||||
}
|
||||
|
||||
// non-link colors
|
||||
span {
|
||||
&#serverless {
|
||||
color: $color-serverless;
|
||||
}
|
||||
|
||||
&#shh {
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-light",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through $colors-home (see above)
|
||||
@each $id, $colors in $colors-home {
|
||||
@each $theme, $color in $colors {
|
||||
@at-root html.#{$theme} div.layout-home a##{$id} {
|
||||
color: $color;
|
||||
background-image: functions.underline-hack($color, map.get(map.get(themes.$themes, $theme), "background-inner"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
div.layout-home {
|
||||
font-size: 0.975em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 0.2em;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
letter-spacing: -0.014em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.185em;
|
||||
letter-spacing: -0.017em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
p {
|
||||
letter-spacing: -0.012em;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Archive/List Styles
|
||||
div.layout-list {
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 0.25em;
|
||||
|
||||
section.list-section-year {
|
||||
font-size: 1.05em;
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
letter-spacing: -0.025em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
letter-spacing: -0.008em;
|
||||
line-height: 1.75;
|
||||
margin-bottom: 1em;
|
||||
|
||||
div.list-item-date {
|
||||
width: 5.25em;
|
||||
flex-shrink: 0;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 1.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
div.layout-list {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 0;
|
||||
|
||||
section.list-section-year {
|
||||
font-size: 1em;
|
||||
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Video Styles
|
||||
div.layout-projects {
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 0.75em;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0.4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div#content > p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div#github-cards {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
div.github-card {
|
||||
flex-grow: 1;
|
||||
width: 416px; // magic number
|
||||
padding: 1em 1.2em;
|
||||
margin: 0.6em;
|
||||
border: 1px solid;
|
||||
border-radius: 0.5em;
|
||||
font-size: 0.9em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-dark",
|
||||
border-color: "kinda-light",
|
||||
)
|
||||
);
|
||||
|
||||
a.repo-name {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p.repo-description {
|
||||
margin: 0.2em 0 0.8em 0;
|
||||
}
|
||||
|
||||
div.repo-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
div.repo-meta-item {
|
||||
margin-right: 1.5em;
|
||||
font-size: 0.925em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium",
|
||||
)
|
||||
);
|
||||
|
||||
a {
|
||||
background: none !important;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
// octicon will inherit this
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "links",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.octicon,
|
||||
span.repo-language-color {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
span.repo-language-color {
|
||||
display: inline-block;
|
||||
width: 1.15em;
|
||||
height: 1.15em;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
top: 0.175em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
div.layout-projects {
|
||||
h1 {
|
||||
margin-bottom: 0.6em;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Post Styles
|
||||
div.layout-single {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
|
||||
div#meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.3em;
|
||||
font-size: 0.825em;
|
||||
line-height: 2.3;
|
||||
letter-spacing: 0.04em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium",
|
||||
)
|
||||
);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
// disable fancy underline without `.no-underline`
|
||||
background: none !important;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: inline-flex;
|
||||
margin-right: 1.6em;
|
||||
white-space: nowrap;
|
||||
|
||||
span.meta-icon {
|
||||
margin-right: 0.4em;
|
||||
user-select: none;
|
||||
|
||||
.emoji {
|
||||
margin-right: 0.25em;
|
||||
vertical-align: -0.22em;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div#meta-date,
|
||||
div#meta-edit {
|
||||
a {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
div#meta-tags {
|
||||
white-space: normal;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
span.meta-tag {
|
||||
text-transform: lowercase;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.75em;
|
||||
|
||||
&::before {
|
||||
content: "#"; // cosmetically hashtagify tags
|
||||
padding-right: 0.125em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "light",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1.title {
|
||||
margin: 0.3em 0 0.5em -0.03em; // TODO: why is this indented slightly?
|
||||
font-size: 2.1em;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.006em;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
div.layout-single {
|
||||
padding-top: 0.8em;
|
||||
padding-bottom: 0.4em;
|
||||
|
||||
h1.title {
|
||||
margin-top: 0.4em;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
@use "../abstracts/themes";
|
||||
|
||||
// Video Styles
|
||||
div.layout-video {
|
||||
padding: 1.5em 0;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
font-size: 0.85em;
|
||||
letter-spacing: -0.005em;
|
||||
line-height: 1.5;
|
||||
margin: 1.25em 1em 0.5em 1em;
|
||||
|
||||
@include themes.themed(
|
||||
(
|
||||
color: "medium-light",
|
||||
)
|
||||
);
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.001em;
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@mixin responsive() {
|
||||
div.layout-video {
|
||||
padding: 1em 0;
|
||||
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
padding: 0 0.6em;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
corejs: 3,
|
||||
useBuiltIns: "entry",
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
pragma: "h",
|
||||
pragmaFrag: "Fragment",
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
5
components/Container.module.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.container {
|
||||
max-width: 865px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
30
components/Container.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { NextSeo } from "next-seo";
|
||||
import * as config from "../lib/config";
|
||||
|
||||
import styles from "./Container.module.scss";
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: unknown;
|
||||
};
|
||||
|
||||
export default function Container({ title, description, children }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={title}
|
||||
description={description}
|
||||
canonical={`${config.baseURL}${router.asPath}`}
|
||||
openGraph={{
|
||||
title: title,
|
||||
url: `${config.baseURL}${router.asPath}`,
|
||||
}}
|
||||
/>
|
||||
<div className={styles.container}>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
67
components/Content.module.scss
Normal file
@ -0,0 +1,67 @@
|
||||
.content {
|
||||
font-size: 0.925em;
|
||||
letter-spacing: -0.004em;
|
||||
line-height: 1.7;
|
||||
|
||||
b,
|
||||
strong {
|
||||
letter-spacing: 0.008em; // not sure why the discrepancy between weights
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1.5em;
|
||||
border-left: 3px solid var(--link);
|
||||
color: var(--medium-dark);
|
||||
}
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
letter-spacing: 0.001em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// special bottom border for H2s
|
||||
h2 {
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid var(--kinda-light);
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 1em auto;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
margin-top: 0.5em;
|
||||
color: var(--medium);
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 1.5em;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1.5em auto;
|
||||
height: 2px;
|
||||
border: 0;
|
||||
background-color: var(--light);
|
||||
}
|
||||
}
|
9
components/Content.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import styles from "./Content.module.scss";
|
||||
|
||||
type Props = {
|
||||
children: unknown;
|
||||
};
|
||||
|
||||
export default function Content({ children }: Props) {
|
||||
return <div className={styles.content}>{children}</div>;
|
||||
}
|
12
components/Layout.module.scss
Normal file
@ -0,0 +1,12 @@
|
||||
.main {
|
||||
width: 100%;
|
||||
padding: 1.5em;
|
||||
color: var(--text);
|
||||
background-color: var(--background-inner);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.main {
|
||||
padding: 1.25em;
|
||||
}
|
||||
}
|
14
components/Layout.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import Header from "./page-header/Header";
|
||||
import Footer from "./page-footer/Footer";
|
||||
|
||||
import styles from "./Layout.module.scss";
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className={styles.main}>{children}</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
23
components/clipboard/CopyButton.module.scss
Normal file
@ -0,0 +1,23 @@
|
||||
.copy_button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.75em;
|
||||
color: var(--text);
|
||||
background-color: var(--background-inner);
|
||||
border: 1px solid var(--kinda-light);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.octicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.octicon-check {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
@ -1,12 +1,18 @@
|
||||
import { h } from "preact";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import { useState, useEffect } from "react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import trimNewlines from "trim-newlines";
|
||||
|
||||
// react components:
|
||||
import { CopyIcon, CheckIcon } from "@primer/octicons-react";
|
||||
|
||||
const CopyButton = (props) => {
|
||||
import styles from "./CopyButton.module.scss";
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
export default function CopyButton({ content, timeout = 2000 }: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = (e) => {
|
||||
@ -16,7 +22,7 @@ const CopyButton = (props) => {
|
||||
e.target.blur();
|
||||
|
||||
// trim any surrounding whitespace from target block's content and send it to the clipboard
|
||||
const didCopy = copy(trimNewlines(props.content));
|
||||
const didCopy = copy(trimNewlines(content));
|
||||
|
||||
// indicate success
|
||||
setCopied(didCopy);
|
||||
@ -27,25 +33,27 @@ const CopyButton = (props) => {
|
||||
if (copied) {
|
||||
const id = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, props.timeout || 2000);
|
||||
}, timeout);
|
||||
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [props.timeout, copied]);
|
||||
}, [timeout, copied]);
|
||||
|
||||
return (
|
||||
<button
|
||||
class="copy-button"
|
||||
className={styles.copy_button}
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard"
|
||||
onClick={handleCopy}
|
||||
disabled={copied}
|
||||
>
|
||||
{copied ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
|
||||
{copied ? (
|
||||
<CheckIcon size={16} className={`${styles.octicon} ${styles["octicon-check"]}`} />
|
||||
) : (
|
||||
<CopyIcon size={16} className={styles.octicon} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyButton;
|
||||
}
|
96
components/contact/ContactForm.module.scss
Normal file
@ -0,0 +1,96 @@
|
||||
.form {
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.8em;
|
||||
margin: 0.6em 0;
|
||||
border: 2px solid;
|
||||
border-radius: 0.3em;
|
||||
color: var(--text);
|
||||
background-color: var(--super-duper-light);
|
||||
border-color: var(--light);
|
||||
|
||||
&:focus {
|
||||
outline: none; // disable browsers' outer border
|
||||
border-color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 12em;
|
||||
min-height: 6em;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5;
|
||||
|
||||
// allow vertical resizing & disable horizontal
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown_tip {
|
||||
font-size: 0.825em;
|
||||
line-height: 1.75;
|
||||
|
||||
a {
|
||||
background: none;
|
||||
padding-bottom: 0;
|
||||
|
||||
&:first-of-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.captcha {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.action_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 3.75em;
|
||||
}
|
||||
|
||||
.btn_submit {
|
||||
flex-shrink: 0;
|
||||
height: 3.25em;
|
||||
padding: 1em 1.25em;
|
||||
margin-right: 1.5em;
|
||||
border: 0;
|
||||
border-radius: 0.3em;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--text);
|
||||
background-color: var(--kinda-light);
|
||||
|
||||
&:hover {
|
||||
color: var(--super-duper-light);
|
||||
background-color: var(--link);
|
||||
}
|
||||
|
||||
.send_icon {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
vertical-align: -0.22em;
|
||||
border: 0;
|
||||
display: inline-block;
|
||||
margin-right: 0.4em;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.result_success,
|
||||
.result_error {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result_success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.result_error {
|
||||
color: var(--error);
|
||||
}
|
@ -1,14 +1,11 @@
|
||||
import { h, Fragment } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import fetch from "unfetch";
|
||||
import { isDark } from "../utils/theme.js";
|
||||
|
||||
// react components:
|
||||
import { useState } from "react";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import { CheckIcon, XIcon } from "@primer/octicons-react";
|
||||
import SendIcon from "../assets/send.svg";
|
||||
import { SendIcon } from "../icons";
|
||||
|
||||
const ContactForm = () => {
|
||||
import styles from "./ContactForm.module.scss";
|
||||
|
||||
export default function ContactForm() {
|
||||
// status/feedback:
|
||||
const [status, setStatus] = useState({ success: false, message: "" });
|
||||
// keep track of fetch:
|
||||
@ -30,14 +27,10 @@ const ContactForm = () => {
|
||||
};
|
||||
|
||||
// some client-side validation to save requests (these are also checked on the server to be safe)
|
||||
// TODO: change border color of the specific empty/missing field(s) to red
|
||||
if (!(formData.name && formData.email && formData.message && formData["h-captcha-response"])) {
|
||||
setSending(false);
|
||||
setStatus({ success: false, message: "Please make sure that all fields are filled in." });
|
||||
|
||||
// remove focus from the submit button
|
||||
document.activeElement.blur();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -83,19 +76,16 @@ const ContactForm = () => {
|
||||
// something else went wrong, and it's probably my fault...
|
||||
setStatus({ success: false, message: "Internal server error. Try again later?" });
|
||||
}
|
||||
|
||||
// remove focus from the submit button
|
||||
document.activeElement.blur();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} id="contact-form" action="/api/contact/" method="POST">
|
||||
<input type="text" name="name" placeholder="Name" disabled={status.success} />
|
||||
<input type="email" name="email" placeholder="Email" disabled={status.success} />
|
||||
<textarea name="message" placeholder="Write something..." disabled={status.success} />
|
||||
<form className={styles.form} onSubmit={onSubmit} action="/api/contact/" method="POST">
|
||||
<input type="text" name="name" placeholder="Name" required disabled={status.success} />
|
||||
<input type="email" name="email" placeholder="Email" required disabled={status.success} />
|
||||
<textarea name="message" placeholder="Write something..." required disabled={status.success} />
|
||||
|
||||
<div id="contact-form-md-info">
|
||||
<div className={styles.markdown_tip}>
|
||||
Basic{" "}
|
||||
<a
|
||||
href="https://commonmark.org/help/"
|
||||
@ -112,19 +102,17 @@ const ContactForm = () => {
|
||||
](https://jarv.is), and <code>`code`</code>.
|
||||
</div>
|
||||
|
||||
<div id="contact-form-captcha">
|
||||
<div className={styles.captcha}>
|
||||
<HCaptcha
|
||||
sitekey={process.env.HCAPTCHA_SITE_KEY}
|
||||
theme={isDark() ? "dark" : "light"}
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
|
||||
size="normal"
|
||||
reCaptchaCompat={false}
|
||||
onVerify={() => true} // this is allegedly optional but a function undefined error is thrown without it
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="contact-form-action-row">
|
||||
<div className={styles.action_row}>
|
||||
<button
|
||||
id="contact-form-btn-submit"
|
||||
className={styles.btn_submit}
|
||||
title="Send Message"
|
||||
aria-label="Send Message"
|
||||
disabled={sending}
|
||||
@ -134,14 +122,13 @@ const ContactForm = () => {
|
||||
<span>Sending...</span>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon class="emoji" /> <span>Send</span>
|
||||
<SendIcon className={styles.send_icon} /> <span>Send</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="contact-form-result"
|
||||
id={status.success ? "contact-form-result-success" : "contact-form-result-error"}
|
||||
className={status.success ? styles.result_success : styles.result_error}
|
||||
style={{ display: !status.message || sending ? "none" : null }}
|
||||
>
|
||||
{status.success ? <CheckIcon size={16} /> : <XIcon size={16} />} {status.message}
|
||||
@ -149,6 +136,4 @@ const ContactForm = () => {
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactForm;
|
||||
}
|
28
components/hits/Hits.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "../../lib/fetcher";
|
||||
import Loading from "../loading/Loading";
|
||||
|
||||
export default function Counter({ slug }) {
|
||||
// start fetching repos from API immediately
|
||||
const { data, error } = useSWR(`/api/hits/?slug=${encodeURIComponent(slug)}`, fetcher, {
|
||||
// avoid double (or more) counting views
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
// show spinning loading indicator if data isn't fetched yet
|
||||
if (!data) {
|
||||
return <Loading boxes={3} width={20} />;
|
||||
}
|
||||
|
||||
// fail secretly
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we have data!
|
||||
return (
|
||||
<span title={`${data.hits.toLocaleString("en-US")} ${data.hits === 1 ? "view" : "views"}`}>
|
||||
{data.hits.toLocaleString("en-US")}
|
||||
</span>
|
||||
);
|
||||
}
|
59
components/home/ColorLink.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import hexRgb from "hex-rgb";
|
||||
import isAbsoluteUrl from "is-absolute-url";
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
children: unknown;
|
||||
href: string;
|
||||
lightColor: string;
|
||||
darkColor: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
export default function ColorLink({
|
||||
children,
|
||||
href,
|
||||
lightColor,
|
||||
darkColor,
|
||||
title,
|
||||
className,
|
||||
external = false,
|
||||
}: Props) {
|
||||
external = external || isAbsoluteUrl(href);
|
||||
|
||||
// hacky hack to form a unique CSS var based on the light hex code, since they need to be set "globally"
|
||||
const varName = `Home__${lightColor.replace("#", "")}`;
|
||||
const alpha = 0.4;
|
||||
|
||||
return (
|
||||
<Link href={href} passHref={true} prefetch={false}>
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
className={className}
|
||||
title={title}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
{children}
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--${varName}: ${lightColor};
|
||||
--${varName}_alpha: ${hexRgb(lightColor, { alpha: alpha, format: "css" })};
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--${varName}: ${darkColor};
|
||||
--${varName}_alpha: ${hexRgb(darkColor, { alpha: alpha, format: "css" })};
|
||||
}
|
||||
`}</style>
|
||||
<style jsx>{`
|
||||
a {
|
||||
color: var(--${varName});
|
||||
background-image: linear-gradient(var(--${varName}_alpha), var(--${varName}_alpha));
|
||||
}
|
||||
`}</style>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
107
components/home/Home.module.scss
Normal file
@ -0,0 +1,107 @@
|
||||
.home {
|
||||
font-size: 1em;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.3em -0.03em; // TODO: why is this indented slightly?
|
||||
font-size: 1.8em;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.014em;
|
||||
|
||||
.wave {
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
animation: wave 5s infinite;
|
||||
animation-delay: 1s;
|
||||
transform-origin: 65% 80%;
|
||||
will-change: transform;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0.5em 0 0.5em -0.03em; // TODO: why is this indented slightly?
|
||||
font-size: 1.35em;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.022em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.85em 0;
|
||||
letter-spacing: -0.009em;
|
||||
line-height: 1.7;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
sup {
|
||||
letter-spacing: normal;
|
||||
position: relative;
|
||||
margin-left: 0.2em;
|
||||
|
||||
a {
|
||||
background: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pgp {
|
||||
font-size: 0.875em;
|
||||
word-spacing: -0.3em;
|
||||
margin-right: 0.075em;
|
||||
}
|
||||
|
||||
.light {
|
||||
color: var(--medium-light);
|
||||
}
|
||||
|
||||
.birthday:hover {
|
||||
cursor: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 36 36%27 width=%2720%27 height=%2720%27%3E%3Cg fill=%27none%27%3E%3Cpath fill=%27%23292F33%27 d=%27m2.651 6.073 26.275 26.276c.391.391 2.888-2.107 2.497-2.497L5.148 3.576c-.39-.391-2.888 2.107-2.497 2.497z%27/%3E%3Cpath fill=%27%2366757F%27 d=%27M29.442 31.23 3.146 4.934l.883-.883 26.296 26.296z%27/%3E%3Cpath fill=%27%23E1E8ED%27 d=%27m33.546 33.483-.412.412-.671.671a.967.967 0 0 1-.255.169.988.988 0 0 1-1.159-.169l-2.102-2.102.495-.495.883-.883 1.119-1.119 2.102 2.102a.999.999 0 0 1 0 1.414zM4.029 4.79l-.883.883-.495.495L.442 3.96a.988.988 0 0 1-.169-1.159.967.967 0 0 1 .169-.255l.671-.671.412-.412a.999.999 0 0 1 1.414 0l2.208 2.208L4.029 4.79z%27/%3E%3Cpath fill=%27%23F5F8FA%27 d=%27m30.325 30.497 2.809 2.809-.671.671a.967.967 0 0 1-.255.169l-2.767-2.767.884-.882zM3.146 5.084.273 2.211a.967.967 0 0 1 .169-.255l.671-.671 2.916 2.916-.883.883z%27/%3E%3Cpath fill=%27%23FFAC33%27 d=%27m27.897 10.219 1.542.571.6 2.2a.667.667 0 0 0 1.287 0l.6-2.2 1.542-.571a.665.665 0 0 0 0-1.25l-1.534-.568-.605-2.415a.667.667 0 0 0-1.293 0l-.605 2.415-1.534.568a.665.665 0 0 0 0 1.25m-16.936 9.628 2.61.966.966 2.61a1.103 1.103 0 0 0 2.07 0l.966-2.61 2.609-.966a1.103 1.103 0 0 0 0-2.07l-2.609-.966-.966-2.61a1.105 1.105 0 0 0-2.07 0l-.966 2.61-2.61.966a1.104 1.104 0 0 0 0 2.07M23.13 4.36l1.383.512.512 1.382a.585.585 0 0 0 1.096 0l.512-1.382 1.382-.512a.584.584 0 0 0 0-1.096l-1.382-.512-.512-1.382a.585.585 0 0 0-1.096 0l-.512 1.382-1.383.512a.585.585 0 0 0 0 1.096%27/%3E%3C/g%3E%3C/svg%3E")
|
||||
0 0,
|
||||
auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.home {
|
||||
font-size: 0.975em;
|
||||
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
5% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
// pause for 3.5 out of 5 seconds
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
247
components/home/Home.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import Link from "./ColorLink";
|
||||
import { WaveIcon, LockIcon } from "../icons";
|
||||
|
||||
import styles from "./Home.module.scss";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<h1>
|
||||
Hi there! I'm Jake.{" "}
|
||||
<span className={styles.wave}>
|
||||
<WaveIcon />
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<h2>
|
||||
I'm a frontend web developer based in{" "}
|
||||
<Link
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3"
|
||||
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
|
||||
lightColor="#fb4d42"
|
||||
darkColor="#ff5146"
|
||||
>
|
||||
Boston
|
||||
</Link>
|
||||
.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
I specialize in{" "}
|
||||
<Link
|
||||
href="https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/"
|
||||
title='"The Brutal Lifecycle of JavaScript Frameworks" by Ian Allen'
|
||||
lightColor="#1091b3"
|
||||
darkColor="#6fcbe3"
|
||||
>
|
||||
modern JS frameworks
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="http://vanilla-js.com/"
|
||||
title="The best JS framework in the world by Eric Wastl"
|
||||
lightColor="#f48024"
|
||||
darkColor="#e18431"
|
||||
>
|
||||
vanilla JavaScript
|
||||
</Link>{" "}
|
||||
to make nifty{" "}
|
||||
<Link href="https://jamstack.wtf/" title="WTF is JAMstack?" lightColor="#04a699" darkColor="#08bbac">
|
||||
JAMstack sites
|
||||
</Link>{" "}
|
||||
with dynamic{" "}
|
||||
<Link href="https://nodejs.org/en/" title="Node.js Official Website" lightColor="#6fbc4e" darkColor="#84d95f">
|
||||
Node.js
|
||||
</Link>{" "}
|
||||
services. But I'm fluent in non-buzzwords like{" "}
|
||||
<Link
|
||||
href="https://stitcher.io/blog/php-in-2020"
|
||||
title='"PHP in 2020" by Brent Roose'
|
||||
lightColor="#8892bf"
|
||||
darkColor="#a4afe3"
|
||||
>
|
||||
PHP
|
||||
</Link>
|
||||
,{" "}
|
||||
<Link
|
||||
href="https://www.ruby-lang.org/en/"
|
||||
title="Ruby Official Website"
|
||||
lightColor="#d34135"
|
||||
darkColor="#f95a4d"
|
||||
>
|
||||
Ruby
|
||||
</Link>
|
||||
, and{" "}
|
||||
<Link href="https://golang.org/" title="Golang Official Website" lightColor="#00acd7" darkColor="#2ad1fb">
|
||||
Go
|
||||
</Link>{" "}
|
||||
too.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Whenever possible, I also apply my experience in{" "}
|
||||
<Link
|
||||
href="https://github.com/jakejarvis/awesome-shodan-queries"
|
||||
title="jakejarvis/awesome-shodan-queries on GitHub"
|
||||
lightColor="#00b81a"
|
||||
darkColor="#57f06d"
|
||||
>
|
||||
application security
|
||||
</Link>
|
||||
,{" "}
|
||||
<Link
|
||||
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
|
||||
title='"What is serverless computing?" on Cloudflare'
|
||||
lightColor="#0098ec"
|
||||
darkColor="#43b9fb"
|
||||
>
|
||||
serverless stacks
|
||||
</Link>
|
||||
, and{" "}
|
||||
<Link href="https://xkcd.com/1319/" title='"Automation" on xkcd' lightColor="#ff6200" darkColor="#f46c16">
|
||||
DevOps automation
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
I fell in love with{" "}
|
||||
<Link
|
||||
href="/previously/"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
||||
lightColor="#4169e1"
|
||||
darkColor="#8ca9ff"
|
||||
>
|
||||
frontend web design
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="/notes/my-first-code/"
|
||||
title="Jake's Bulletin Board, circa 2003"
|
||||
lightColor="#9932cc"
|
||||
darkColor="#d588fb"
|
||||
>
|
||||
backend programming
|
||||
</Link>{" "}
|
||||
back when my only source of income was{" "}
|
||||
<Link
|
||||
className={styles.birthday}
|
||||
href="/birthday/"
|
||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||
lightColor="#e40088"
|
||||
darkColor="#fd40b1"
|
||||
>
|
||||
the Tooth Fairy
|
||||
</Link>
|
||||
. <span className={styles.light}>I've improved a bit since then, I think...</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Over the years, some of my side projects{" "}
|
||||
<Link
|
||||
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
|
||||
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
|
||||
lightColor="#ff1b1b"
|
||||
darkColor="#f06060"
|
||||
>
|
||||
have
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="/leo/"
|
||||
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
|
||||
lightColor="#f78200"
|
||||
darkColor="#fd992a"
|
||||
>
|
||||
been
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
|
||||
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
|
||||
lightColor="#f2b702"
|
||||
darkColor="#ffcc2e"
|
||||
>
|
||||
featured
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
|
||||
title='"The new Facebook is on a roll" on CNN Money'
|
||||
lightColor="#5ebd3e"
|
||||
darkColor="#78df55"
|
||||
>
|
||||
by
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://www.wired.com/2007/04/our-web-servers/"
|
||||
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
|
||||
lightColor="#009cdf"
|
||||
darkColor="#29bfff"
|
||||
>
|
||||
various
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
|
||||
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
|
||||
lightColor="#3e49bb"
|
||||
darkColor="#7b87ff"
|
||||
>
|
||||
media
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
|
||||
title='"Your Next Client? The CEO's Son" on Advertising Age'
|
||||
lightColor="#973999"
|
||||
darkColor="#db60dd"
|
||||
>
|
||||
outlets
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can find more of my work on{" "}
|
||||
<Link
|
||||
href="https://github.com/jakejarvis"
|
||||
title="Jake Jarvis on GitHub"
|
||||
lightColor="#8d4eff"
|
||||
darkColor="#a379f0"
|
||||
>
|
||||
GitHub
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="https://www.linkedin.com/in/jakejarvis/"
|
||||
title="Jake Jarvis on LinkedIn"
|
||||
lightColor="#0073b1"
|
||||
darkColor="#3b9dd2"
|
||||
>
|
||||
LinkedIn
|
||||
</Link>
|
||||
. I'm always available to connect over{" "}
|
||||
<Link href="/contact/" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
|
||||
email
|
||||
</Link>{" "}
|
||||
<sup className="monospace">
|
||||
<Link href="/pubkey.asc" title="My Public Key" lightColor="#757575" darkColor="#959595" external={true}>
|
||||
<span className={styles.pgp}>
|
||||
<LockIcon alt="PGP Key" /> 2B0C 9CF2 51E6 9A39
|
||||
</span>
|
||||
</Link>
|
||||
</sup>
|
||||
,{" "}
|
||||
<Link
|
||||
href="https://twitter.com/jakejarvis"
|
||||
title="Jake Jarvis on Twitter"
|
||||
lightColor="#00acee"
|
||||
darkColor="#3bc9ff"
|
||||
>
|
||||
Twitter
|
||||
</Link>
|
||||
, or{" "}
|
||||
<Link href="sms:+1-617-917-3737" title="Send SMS to +1 (617) 917-3737" lightColor="#6fcc01" darkColor="#8edb34">
|
||||
SMS
|
||||
</Link>{" "}
|
||||
as well!
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
51
components/icons/index.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import BotIcon from "./svg/bot.svg";
|
||||
import BulbOffIcon from "./svg/bulb-off.svg";
|
||||
import BulbOnIcon from "./svg/bulb-on.svg";
|
||||
import ContactIcon from "./svg/contact.svg";
|
||||
import DateIcon from "./svg/date.svg";
|
||||
import EditIcon from "./svg/edit.svg";
|
||||
import FloppyIcon from "./svg/floppy.svg";
|
||||
import HeartIcon from "./svg/heart.svg";
|
||||
import HomeIcon from "./svg/home.svg";
|
||||
import LaptopIcon from "./svg/laptop.svg";
|
||||
import LicenseIcon from "./svg/license.svg";
|
||||
import LockIcon from "./svg/lock.svg";
|
||||
import MailIcon from "./svg/mail.svg";
|
||||
import NextjsIcon from "./svg/nextjs.svg";
|
||||
import NotesIcon from "./svg/notes.svg";
|
||||
import PrivacyIcon from "./svg/privacy.svg";
|
||||
import ProjectsIcon from "./svg/projects.svg";
|
||||
import SendIcon from "./svg/send.svg";
|
||||
import SirenIcon from "./svg/siren.svg";
|
||||
import TagIcon from "./svg/tag.svg";
|
||||
import TapeIcon from "./svg/tape.svg";
|
||||
import ViewsIcon from "./svg/views.svg";
|
||||
import WandIcon from "./svg/wand.svg";
|
||||
import WaveIcon from "./svg/wave.svg";
|
||||
|
||||
export {
|
||||
BotIcon,
|
||||
BulbOffIcon,
|
||||
BulbOnIcon,
|
||||
ContactIcon,
|
||||
DateIcon,
|
||||
EditIcon,
|
||||
FloppyIcon,
|
||||
HeartIcon,
|
||||
HomeIcon,
|
||||
LaptopIcon,
|
||||
LicenseIcon,
|
||||
LockIcon,
|
||||
MailIcon,
|
||||
NextjsIcon,
|
||||
NotesIcon,
|
||||
PrivacyIcon,
|
||||
ProjectsIcon,
|
||||
SendIcon,
|
||||
SirenIcon,
|
||||
TagIcon,
|
||||
TapeIcon,
|
||||
ViewsIcon,
|
||||
WandIcon,
|
||||
WaveIcon,
|
||||
};
|
1
components/icons/svg/bot.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><ellipse fill="#F4900C" cx="33.5" cy="14.5" rx="2.5" ry="3.5"/><ellipse fill="#F4900C" cx="2.5" cy="14.5" rx="2.5" ry="3.5"/><path fill="#FFAC33" d="M34 19c0 .553-.447 1-1 1h-3c-.553 0-1-.447-1-1v-9c0-.552.447-1 1-1h3c.553 0 1 .448 1 1v9zM7 19c0 .553-.448 1-1 1H3c-.552 0-1-.447-1-1v-9c0-.552.448-1 1-1h3c.552 0 1 .448 1 1v9z"/><path fill="#FFCC4D" d="M28 5c0 2.761-4.478 4-10 4C12.477 9 8 7.761 8 5s4.477-5 10-5c5.522 0 10 2.239 10 5z"/><path fill="#F4900C" d="M25 4.083C25 5.694 21.865 7 18 7c-3.866 0-7-1.306-7-2.917 0-1.611 3.134-2.917 7-2.917 3.865 0 7 1.306 7 2.917z"/><path fill="#269" d="M30 5.5C30 6.881 28.881 7 27.5 7h-19C7.119 7 6 6.881 6 5.5S7.119 3 8.5 3h19C28.881 3 30 4.119 30 5.5z"/><path fill="#55ACEE" d="M30 6H6c-1.104 0-2 .896-2 2v26h28V8c0-1.104-.896-2-2-2z"/><path fill="#3B88C3" d="M35 33v-1c0-1.104-.896-2-2-2H22.071l-3.364 3.364c-.391.391-1.023.391-1.414 0L13.929 30H3c-1.104 0-2 .896-2 2v1c0 1.104-.104 2 1 2h32c1.104 0 1-.896 1-2z"/><circle fill="#FFF" cx="24.5" cy="14.5" r="4.5"/><circle fill="#DD2E44" cx="24.5" cy="14.5" r="2.721"/><circle fill="#FFF" cx="11.5" cy="14.5" r="4.5"/><path fill="#F5F8FA" d="M29 25.5c0 1.381-1.119 2.5-2.5 2.5h-17C8.119 28 7 26.881 7 25.5S8.119 23 9.5 23h17c1.381 0 2.5 1.119 2.5 2.5z"/><path fill="#CCD6DD" d="M17 23h2v5h-2zm-5 0h2v5h-2zm10 0h2v5h-2zM7 25.5c0 1.21.859 2.218 2 2.45v-4.9c-1.141.232-2 1.24-2 2.45zm20-2.45v4.899c1.141-.232 2-1.24 2-2.45s-.859-2.217-2-2.449z"/><circle fill="#DD2E44" cx="11.5" cy="14.5" r="2.721"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
components/icons/svg/bulb-off.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><g fill="none"><path d="m30 11.376c0 6.6229714-5.2272727 7.6515429-5.2272727 13.824 0 3.1865143-3.2649546 3.4549714-5.75 3.4549714-2.1463182 0-6.8853637-.8012571-6.8853637-3.4570285 0-6.1693715-5.1373636-7.1979429-5.1373636-13.8219429 0-6.20331429 5.5252273-11.232 11.5867727-11.232 6.0636364 0 11.4132273 5.02868571 11.4132273 11.232z" fill="#cccbcb"/><path d="m22.8564091 33.4285714c0 .8516572-2.3355455 2.5714286-4.3564091 2.5714286s-4.3564091-1.7197714-4.3564091-2.5714286c0-.8516571 2.3345-.5142857 4.3564091-.5142857 2.0208636 0 4.3564091-.3373714 4.3564091.5142857z" fill="#ccd6dd"/><path d="m23.4209545 10.5870857c-.4087727-.4021714-1.0695-.4021714-1.4782727 0l-3.4426818 3.3870857-3.4426818-3.3870857c-.4087727-.4021714-1.0695-.4021714-1.4782727 0-.4087728.4021714-.4087728 1.0522286 0 1.4544l3.8755 3.8129143v10.8884571c0 .5688.4683636 1.0285715 1.0454545 1.0285715s1.0454545-.4597715 1.0454545-1.0285715v-10.8884571l3.8755-3.8129143c.4087728-.4021714.4087728-1.0522286 0-1.4544z" fill="#7d7a72"/><path d="m24.7727273 31.8857143c0 1.1355428-.9367273 2.0571428-2.0909091 2.0571428h-8.3636364c-1.1541818 0-2.0909091-.9216-2.0909091-2.0571428v-6.1714286h12.5454546z" fill="#99aab5"/><path d="m12.2262273 32.9142857c-.5018182 0-.9450909-.3569143-1.0297728-.8598857-.0951363-.5595429.289591-1.0902857.8593637-1.1828571l12.5454545-2.0571429c.5687273-.1008 1.1081818.2849143 1.2022728.8454857.0951363.5595429-.289591 1.0902857-.8593637 1.1828572l-12.5454545 2.0571428c-.0575.0102857-.1160455.0144-.1725.0144zm0-4.1142857c-.5018182 0-.9450909-.3569143-1.0297728-.8598857-.0951363-.5595429.289591-1.0902857.8593637-1.1828572l12.5454545-2.0571428c.5687273-.0997714 1.1081818.2849143 1.2022728.8454857.0951363.5595429-.289591 1.0902857-.8593637 1.1828571l-12.5454545 2.0571429c-.0575.0102857-.1160455.0144-.1725.0144z" fill="#ccd6dd"/></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
components/icons/svg/bulb-on.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><g fill="none"><path d="m30 11.376c0 6.6229714-5.2272727 7.6515429-5.2272727 13.824 0 3.1865143-3.2649546 3.4549714-5.75 3.4549714-2.1463182 0-6.8853637-.8012571-6.8853637-3.4570285 0-6.1693715-5.1373636-7.1979429-5.1373636-13.8219429 0-6.20331429 5.5252273-11.232 11.5867727-11.232 6.0636364 0 11.4132273 5.02868571 11.4132273 11.232z" fill="#ffd983"/><path d="m22.8564091 33.4285714c0 .8516572-2.3355455 2.5714286-4.3564091 2.5714286s-4.3564091-1.7197714-4.3564091-2.5714286c0-.8516571 2.3345-.5142857 4.3564091-.5142857 2.0208636 0 4.3564091-.3373714 4.3564091.5142857z" fill="#b9c9d9"/><path d="m23.4209545 10.5870857c-.4087727-.4021714-1.0695-.4021714-1.4782727 0l-3.4426818 3.3870857-3.4426818-3.3870857c-.4087727-.4021714-1.0695-.4021714-1.4782727 0-.4087728.4021714-.4087728 1.0522286 0 1.4544l3.8755 3.8129143v10.8884571c0 .5688.4683636 1.0285715 1.0454545 1.0285715s1.0454545-.4597715 1.0454545-1.0285715v-10.8884571l3.8755-3.8129143c.4087728-.4021714.4087728-1.0522286 0-1.4544z" fill="#ffcc4d"/><path d="m24.7727273 31.8857143c0 1.1355428-.9367273 2.0571428-2.0909091 2.0571428h-8.3636364c-1.1541818 0-2.0909091-.9216-2.0909091-2.0571428v-6.1714286h12.5454546z" fill="#99aab5"/><path d="m12.2262273 32.9142857c-.5018182 0-.9450909-.3569143-1.0297728-.8598857-.0951363-.5595429.289591-1.0902857.8593637-1.1828571l12.5454545-2.0571429c.5687273-.1008 1.1081818.2849143 1.2022728.8454857.0951363.5595429-.289591 1.0902857-.8593637 1.1828572l-12.5454545 2.0571428c-.0575.0102857-.1160455.0144-.1725.0144zm0-4.1142857c-.5018182 0-.9450909-.3569143-1.0297728-.8598857-.0951363-.5595429.289591-1.0902857.8593637-1.1828572l12.5454545-2.0571428c.5687273-.0997714 1.1081818.2849143 1.2022728.8454857.0951363.5595429-.289591 1.0902857-.8593637 1.1828571l-12.5454545 2.0571429c-.0575.0102857-.1160455.0144-.1725.0144z" fill="#ccd6dd"/></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
components/icons/svg/contact.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#C1694F" d="M22 33c0 2.209-8 2.209-8 0V23c0-2.209 1.791-4 4-4s4 1.791 4 4v10z"/><path fill="#292F33" d="M10 3c-4.418 0-8 3.582-8 8v12h16V11c0-4.418-3.582-8-8-8z"/><path fill="#CCD6DD" d="M20 18.761C20 19.997 18.935 21 17.707 21H2.293C1.064 21 0 19.997 0 18.761V8.239C0 7.003 1.064 6 2.293 6h15.414C18.935 6 20 7.003 20 8.239v10.522z"/><path fill="#99AAB5" d="M20 8.239C20 7.003 18.935 6 17.707 6H2.293C1.064 6 0 7.003 0 8.239v1.419l4.879 4.904-4.78 4.806c.112.376.316.716.596.983l4.972-4.998 2.407 2.419c1.052 1.06 2.768 1.06 3.821 0l2.426-2.432 4.984 5.011c.28-.268.483-.608.596-.983l-4.792-4.818L20 9.646V8.239z"/><path fill="#E1E8ED" d="M17.707 6H2.293C1.127 6 .121 6.906.02 8.055l8.408 8.397c.869.874 2.277.84 3.145-.035l8.41-8.346C19.889 6.914 18.877 6 17.707 6z"/><path fill="#99AAB5" d="M26 3H10c4.418 0 8 3.582 8 8v12h16V11c0-4.418-3.582-8-8-8z"/><path fill="#DD2E44" d="M26 1h-4c-1.104 0-2 .896-2 2v12c0 1.104.896 2 2 2s2-.896 2-2V7h2c1.104 0 2-.896 2-2V3c0-1.104-.896-2-2-2z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
components/icons/svg/date.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m36 32c0 2.209-1.791 4-4 4h-28c-2.209 0-4-1.791-4-4v-23c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4z" fill="#e0e7ec"/><path d="m23.657 19.12h-5.787c-1.22 0-1.673-.791-1.673-1.56 0-.791.429-1.56 1.673-1.56h8.184c1.154 0 1.628 1.04 1.628 1.628 0 .452-.249.927-.52 1.492l-5.607 11.395c-.633 1.266-.882 1.717-1.899 1.717-1.244 0-1.877-.949-1.877-1.605 0-.271.068-.474.226-.791zm-12.768-.12h-.5c-1.085 0-1.538-.731-1.538-1.5 0-.792.565-1.5 1.538-1.5h2.015c.972 0 1.515.701 1.515 1.605v12.865c0 1.13-.558 1.763-1.53 1.763s-1.5-.633-1.5-1.763z" fill="#66757f"/><path d="m34 0h-3.277c.172.295.277.634.277 1 0 1.104-.896 2-2 2s-2-.896-2-2c0-.366.105-.705.277-1h-18.554c.172.295.277.634.277 1 0 1.104-.896 2-2 2s-2-.896-2-2c0-.366.105-.705.277-1h-3.277c-1.104 0-2 .896-2 2v11h36v-11c0-1.104-.896-2-2-2z" fill="#dd2f45"/><path d="m13.182 4.604c0-.5.32-.78.75-.78.429 0 .749.28.749.78v5.017h1.779c.51 0 .73.38.72.72-.02.33-.28.659-.72.659h-2.498c-.49 0-.78-.319-.78-.819zm-6.91 0c0-.5.32-.78.75-.78s.75.28.75.78v3.488c0 .92.589 1.649 1.539 1.649.909 0 1.529-.769 1.529-1.649v-3.488c0-.5.319-.78.749-.78s.75.28.75.78v3.568c0 1.679-1.38 2.949-3.028 2.949-1.669 0-3.039-1.25-3.039-2.949zm-.782 4.397c0 1.679-1.069 2.119-1.979 2.119-.689 0-1.839-.27-1.839-1.14 0-.269.23-.609.56-.609.4 0 .75.37 1.199.37.56 0 .56-.52.56-.84v-4.297c0-.5.32-.78.749-.78.431 0 .75.28.75.78z" fill="#f5f8fa"/><path d="m32 10c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1m0-3c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1m-3 3c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1m0-3c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1m-3 3c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1m0-3c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1m-3 0c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1m0 3c0 .552.447 1 1 1s1-.448 1-1-.447-1-1-1-1 .448-1 1" fill="#f4abba"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
components/icons/svg/edit.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m35.222 33.598c-.647-2.101-1.705-6.059-2.325-7.566-.501-1.216-.969-2.438-1.544-3.014-.575-.575-1.553-.53-2.143.058 0 0-2.469 1.675-3.354 2.783-1.108.882-2.785 3.357-2.785 3.357-.59.59-.635 1.567-.06 2.143.576.575 1.798 1.043 3.015 1.544 1.506.62 5.465 1.676 7.566 2.325.359.11 1.74-1.271 1.63-1.63z" fill="#d99e82"/><path d="m13.643 5.308c1.151 1.151 1.151 3.016 0 4.167l-4.167 4.168c-1.151 1.15-3.018 1.15-4.167 0l-4.168-4.168c-1.15-1.151-1.15-3.016 0-4.167l4.167-4.167c1.15-1.151 3.016-1.151 4.167 0z" fill="#ea596e"/><path d="m31.353 23.018-4.17 4.17-4.163 4.165-15.628-15.627 8.335-8.334z" fill="#ffcc4d"/><path d="m32.078 34.763s2.709 1.489 3.441.757-.765-3.435-.765-3.435-2.566.048-2.676 2.678z" fill="#292f33"/><path d="m2.183 10.517 8.335-8.335 5.208 5.209-8.334 8.335z" fill="#ccd6dd"/><path d="m3.225 11.558 8.334-8.334 1.042 1.042-8.334 8.334zm2.083 2.086 8.335-8.335 1.042 1.042-8.335 8.334z" fill="#99aab5"/></svg>
|
After Width: | Height: | Size: 997 B |
1
components/icons/svg/floppy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#31373D" d="M4 36s-4 0-4-4V4s0-4 4-4h26c1 0 2 1 2 1l3 3s1 1 1 2v26s0 4-4 4H4z"/><path fill="#55ACEE" d="M5 19v-1s0-2 2-2h21c2 0 2 2 2 2v1H5z"/><path fill="#E1E8ED" d="M5 32.021V19h25v13s0 2-2 2H7c-2 0-2-1.979-2-1.979zM10 3s0-1 1-1h18c1.048 0 1 1 1 1v10s0 1-1 1H11s-1 0-1-1V3zm12 10h5V3h-5v10z"/></svg>
|
After Width: | Height: | Size: 374 B |
1
components/icons/svg/heart.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DD2E44" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/></svg>
|
After Width: | Height: | Size: 369 B |
1
components/icons/svg/home.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m36 33.5c0 .828-.672 1.5-1.5 1.5h-33c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5h33c.828 0 1.5.672 1.5 1.5z" fill="#5c913b"/><path d="m12.344 14.702h-2c-.276 0-.5-.224-.5-.5v-7c0-.276.224-.5.5-.5h2c.276 0 .5.224.5.5v7c0 .276-.224.5-.5.5z" fill="#a0041e"/><path d="m5.942 32c-.137-4.657-.506-8-.942-8-.435 0-.804 3.343-.941 8z" fill="#ffcc4d"/><path d="m10 18.731c0 5.575-2.238 7.269-5 7.269-2.761 0-5-1.694-5-7.269 0-5.577 4-13.731 5-13.731s5 8.154 5 13.731z" fill="#77b255"/><path d="m8 16 13-13 13 13v16h-26z" fill="#ffe8b6"/><path d="m21 16h1v16h-1z" fill="#ffcc4d"/><path d="m34 17c-.256 0-.512-.098-.707-.293l-12.293-12.293-12.293 12.293c-.391.391-1.023.391-1.414 0s-.391-1.023 0-1.414l13-13c.391-.391 1.023-.391 1.414 0l13 13c.391.391.391 1.023 0 1.414-.195.195-.451.293-.707.293z" fill="#66757f"/><path d="m21 17c-.256 0-.512-.098-.707-.293-.391-.391-.391-1.023 0-1.414l6.5-6.5c.391-.391 1.023-.391 1.414 0s.391 1.023 0 1.414l-6.5 6.5c-.195.195-.451.293-.707.293z" fill="#66757f"/><path d="m13 26h4v6h-4z" fill="#c1694f"/><path d="m13 17h4v4h-4zm12.5 0h4v4h-4zm0 9h4v4h-4z" fill="#55acee"/><path d="m10.625 29.991c0 1.613-.858 2.103-1.917 2.103-1.058 0-1.917-.49-1.917-2.103s1.533-3.973 1.917-3.973 1.917 2.359 1.917 3.973zm25.25 0c0 1.613-.858 2.103-1.917 2.103-1.058 0-1.917-.49-1.917-2.103s1.533-3.973 1.917-3.973 1.917 2.359 1.917 3.973z" fill="#77b255"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
components/icons/svg/laptop.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CCD6DD" d="M34 29.096c-.417-.963-.896-2.008-2-2.008h-1c1.104 0 2-.899 2-2.008V8.008C33 6.899 32.104 6 31 6H5c-1.104 0-2 .899-2 2.008V25.08c0 1.109.896 2.008 2 2.008H4c-1.104 0-1.667 1.004-2 2.008l-2 4.895C0 35.101.896 36 2 36h32c1.104 0 2-.899 2-2.008l-2-4.896z"/><path fill="#9AAAB4" d="M.008 34.075l.006.057.17.692C.5 35.516 1.192 36 2 36h32c1.076 0 1.947-.855 1.992-1.925H.008z"/><path fill="#5DADEC" d="M31 24.075c0 .555-.447 1.004-1 1.004H6c-.552 0-1-.449-1-1.004V9.013c0-.555.448-1.004 1-1.004h24c.553 0 1 .45 1 1.004v15.062z"/><path fill="#AEBBC1" d="M32.906 31.042l-.76-2.175c-.239-.46-.635-.837-1.188-.837H5.11c-.552 0-.906.408-1.156 1.036l-.688 1.977c-.219.596.448 1.004 1 1.004h7.578s.937-.047 1.103-.608c.192-.648.415-1.624.463-1.796.074-.264.388-.531.856-.531h8.578c.5 0 .746.253.811.566.042.204.312 1.141.438 1.782.111.571 1.221.586 1.221.586h6.594c.551 0 1.217-.471.998-1.004z"/><path fill="#9AAAB4" d="M22.375 33.113h-7.781c-.375 0-.538-.343-.484-.675.054-.331.359-1.793.383-1.963.023-.171.274-.375.524-.375h7.015c.297 0 .49.163.55.489.059.327.302 1.641.321 1.941.019.301-.169.583-.528.583z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
components/icons/svg/license.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFD983" d="M32 0H10C7.791 0 6 1.791 6 4v24H4c-2.209 0-4 1.791-4 4s1.791 4 4 4h24c2.209 0 4-1.791 4-4V8c2.209 0 4-1.791 4-4s-1.791-4-4-4z"/><path fill="#E39F3D" d="M8 10h24V8H10L8 7z"/><path fill="#FFE8B6" d="M10 0C7.791 0 6 1.791 6 4v24.555C5.41 28.211 4.732 28 4 28c-2.209 0-4 1.791-4 4s1.791 4 4 4 4-1.791 4-4V7.445C8.59 7.789 9.268 8 10 8c2.209 0 4-1.791 4-4s-1.791-4-4-4z"/><path fill="#C1694F" d="M12 4c0 1.104-.896 2-2 2s-2-.896-2-2 .896-2 2-2 2 .896 2 2M6 32c0 1.104-.896 2-2 2s-2-.896-2-2 .896-2 2-2 2 .896 2 2m24-17c0 .552-.447 1-1 1H11c-.552 0-1-.448-1-1s.448-1 1-1h18c.553 0 1 .448 1 1m0 4c0 .553-.447 1-1 1H11c-.552 0-1-.447-1-1s.448-1 1-1h18c.553 0 1 .447 1 1m0 4c0 .553-.447 1-1 1H11c-.552 0-1-.447-1-1s.448-1 1-1h18c.553 0 1 .447 1 1m0 4c0 .553-.447 1-1 1H11c-.552 0-1-.447-1-1 0-.553.448-1 1-1h18c.553 0 1 .447 1 1"/></svg>
|
After Width: | Height: | Size: 913 B |
1
components/icons/svg/lock.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#AAB8C2" d="M13 3C7.477 3 3 7.477 3 13v10h4V13c0-3.313 2.687-6 6-6s6 2.687 6 6v10h4V13c0-5.523-4.477-10-10-10z"/><path fill="#FFAC33" d="M26 32c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V20c0-2.209 1.791-4 4-4h18c2.209 0 4 1.791 4 4v12z"/><path fill="#C1694F" d="M35 9c0-4.971-4.029-9-9-9s-9 4.029-9 9c0 3.917 2.507 7.24 6 8.477V33.5c0 1.381 1.119 2.5 2.5 2.5 1.213 0 2.223-.864 2.45-2.01.018.001.032.01.05.01.553 0 1-.447 1-1v-1c0-.553-.447-1-1-1v-1c.553 0 1-.447 1-1v-2c0-.553-.447-1-1-1v-2.277c.596-.347 1-.984 1-1.723v-4.523c3.493-1.236 6-4.559 6-8.477zm-9-7c1.104 0 2 .896 2 2s-.896 2-2 2-2-.896-2-2 .896-2 2-2z"/></svg>
|
After Width: | Height: | Size: 698 B |
1
components/icons/svg/mail.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CCD6DD" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V9c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v18z"/><path fill="#99AAB5" d="M11.95 17.636L.637 28.949c-.027.028-.037.063-.06.091.34.57.814 1.043 1.384 1.384.029-.023.063-.033.09-.06L13.365 19.05c.39-.391.39-1.023 0-1.414-.392-.391-1.024-.391-1.415 0M35.423 29.04c-.021-.028-.033-.063-.06-.09L24.051 17.636c-.392-.391-1.024-.391-1.415 0-.391.392-.391 1.024 0 1.414l11.313 11.314c.026.026.062.037.09.06.571-.34 1.044-.814 1.384-1.384"/><path fill="#99AAB5" d="M32 5H4C1.791 5 0 6.791 0 9v1.03l14.528 14.496c1.894 1.893 4.988 1.893 6.884 0L36 10.009V9c0-2.209-1.791-4-4-4z"/><path fill="#E1E8ED" d="M32 5H4C2.412 5 1.051 5.934.405 7.275l14.766 14.767c1.562 1.562 4.096 1.562 5.657 0L35.595 7.275C34.949 5.934 33.589 5 32 5z"/></svg>
|
After Width: | Height: | Size: 863 B |
1
components/icons/svg/nextjs.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728-1.1261 1.5717-1.8463 3.3543-2.1183 5.2427-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 0 1 -.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 0 0 4.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
components/icons/svg/notes.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m27.815 4h1.996v2.391h-1.996z" fill="#66757f"/><path d="m29 4h-20c-2.209 0-4 1.791-4 4v24c0 2.209 1.791 4 4 4h20c2.209 0 4-1.791 4-4v-24c0-2.209-1.791-4-4-4z" fill="#55acee"/><path d="m27 4h-20c-2.209 0-4 1.791-4 4v24c0 2.209 1.791 4 4 4h20c2.209 0 4-1.791 4-4v-24c0-2.209-1.791-4-4-4z" fill="#ccd6dd"/><path d="m28 15c0 .553-.447 1-1 1h-20c-.552 0-1-.447-1-1 0-.552.448-1 1-1h20c.553 0 1 .448 1 1zm0 5c0 .553-.447 1-1 1h-20c-.552 0-1-.447-1-1s.448-1 1-1h20c.553 0 1 .447 1 1zm0 5c0 .553-.447 1-1 1h-20c-.552 0-1-.447-1-1s.448-1 1-1h20c.553 0 1 .447 1 1zm-8 5c0 .553-.447 1-1 1h-12c-.552 0-1-.447-1-1s.448-1 1-1h12c.553 0 1 .447 1 1z" fill="#99aab5"/><path d="m7.836 8.731c-.702 0-1.271-.666-1.271-1.489 0-.822.569-1.489 1.271-1.489.701 0 1.27.667 1.27 1.489s-.569 1.489-1.27 1.489z" fill="#292f33"/><path d="m8.543 7.083c-.055-.48-.374-.792-.729-1.017-.485-.307-1-1.008-1-1.877 0-1.104.671-2.095 1.5-2.095s1.5.905 1.5 1.905h1.996c-.021-2-1.575-3.821-3.496-3.821-1.934 0-3.5 1.819-3.5 4.005 0 1.853 1.045 3.371 2.569 3.926.76.276 1.224-.447 1.16-1.026z" fill="#66757f"/><path d="m13.836 8.731c-.702 0-1.271-.666-1.271-1.489 0-.822.569-1.489 1.271-1.489.701 0 1.27.667 1.27 1.489s-.569 1.489-1.27 1.489z" fill="#292f33"/><path d="m14.543 7.083c-.055-.48-.374-.792-.729-1.017-.485-.307-1-1.008-1-1.877 0-1.104.671-2.095 1.5-2.095s1.5.905 1.5 1.905h1.996c-.02-2-1.575-3.821-3.496-3.821-1.934 0-3.5 1.819-3.5 4.005 0 1.853 1.045 3.371 2.569 3.926.76.276 1.224-.447 1.16-1.026z" fill="#66757f"/><path d="m19.836 8.731c-.702 0-1.271-.666-1.271-1.489 0-.822.569-1.489 1.271-1.489.701 0 1.271.667 1.271 1.489-.001.822-.57 1.489-1.271 1.489z" fill="#292f33"/><path d="m20.543 7.083c-.055-.48-.374-.792-.728-1.017-.485-.307-1-1.008-1-1.877 0-1.104.671-2.095 1.5-2.095s1.5.905 1.5 1.905h1.996c-.02-2-1.575-3.821-3.496-3.821-1.934 0-3.5 1.819-3.5 4.005 0 1.853 1.045 3.371 2.569 3.926.759.276 1.223-.447 1.159-1.026z" fill="#66757f"/><path d="m25.836 8.731c-.702 0-1.271-.666-1.271-1.489 0-.822.569-1.489 1.271-1.489.701 0 1.271.667 1.271 1.489-.001.822-.57 1.489-1.271 1.489z" fill="#292f33"/><path d="m26.543 7.083c-.055-.48-.374-.792-.728-1.017-.485-.307-1-1.008-1-1.877 0-1.104.671-2.095 1.5-2.095s1.5.905 1.5 1.905h1.996c-.02-2-1.575-3.821-3.496-3.821-1.934 0-3.5 1.819-3.5 4.005 0 1.853 1.045 3.371 2.569 3.926.759.276 1.223-.447 1.159-1.026z" fill="#66757f"/></svg>
|
After Width: | Height: | Size: 2.4 KiB |
1
components/icons/svg/privacy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m33 36v-1c0-3.313-2.687-6-6-6h-18c-3.313 0-6 2.687-6 6v1zm-6.25-15.565c1.188.208 2.619.129 2.416.917-.479 1.854-2.604 1.167-2.979 1.188-.375.02.563-2.105.563-2.105z" fill="#66757f"/><path d="m27.062 20.645c1.875.25 2.541.416 1.166.958-.772.305-2.243 4.803-3.331 4.118-1.087-.685 2.165-5.076 2.165-5.076z" fill="#292f33"/><path d="m9.255 20.435c-1.188.208-2.619.129-2.416.917.479 1.854 2.604 1.167 2.979 1.188.375.02-.563-2.105-.563-2.105z" fill="#66757f"/><path d="m8.943 20.645c-1.875.25-2.541.416-1.166.958.772.305 2.243 4.803 3.331 4.118s-2.165-5.076-2.165-5.076z" fill="#292f33"/><path d="m21.771 4.017c-1.958-.634-6.566-.461-7.718 1.037-2.995.058-6.508 2.764-6.969 6.335-.456 3.534.56 5.175.922 7.833.409 3.011 2.102 3.974 3.456 4.377 1.947 2.572 4.017 2.462 7.492 2.462 6.787 0 10.019-4.541 10.305-12.253.172-4.665-2.565-8.198-7.488-9.791z" fill="#ffac33"/><path d="m25.652 14.137c-.657-.909-1.497-1.641-3.34-1.901.691.317 1.353 1.411 1.44 2.016.086.605.173 1.094-.374.49-2.192-2.423-4.579-1.469-6.944-2.949-1.652-1.034-2.155-2.177-2.155-2.177s-.202 1.526-2.707 3.081c-.726.451-1.593 1.455-2.073 2.937-.346 1.066-.238 2.016-.238 3.64 0 4.74 3.906 8.726 8.726 8.726s8.726-4.02 8.726-8.726c-.004-2.948-.312-4.1-1.061-5.137z" fill="#ffdc5d"/><path d="m18.934 21.565h-1.922c-.265 0-.481-.215-.481-.481v-.174c0-.265.215-.482.481-.482h1.922c.265 0 .482.216.482.482v.174c0 .266-.216.481-.482.481" fill="#c1694f"/><path clip-rule="evenodd" d="m7.657 14.788c.148.147.888.591 1.036 1.034s.445 2.954 1.333 3.693c.916.762 4.37.478 5.032.149 1.48-.738 1.662-2.798 1.924-3.842.148-.591 1.036-.591 1.036-.591s.888 0 1.036.591c.262 1.044.444 3.104 1.924 3.841.662.33 4.116.614 5.034-.147.887-.739 1.183-3.25 1.331-3.694.146-.443.888-.886 1.035-1.034.148-.148.148-.739 0-.887-.296-.295-3.788-.559-7.548-.148-.75.082-1.035.295-2.812.295-1.776 0-2.062-.214-2.812-.295-3.759-.411-7.252-.148-7.548.148-.149.148-.149.74-.001.887z" fill="#292f33" fill-rule="evenodd"/><path d="m7.858 8.395s1.359-8.901 5.932-8.372c3.512.406 4.89.825 7.833.097 1.947-.482 4.065 1.136 5.342 4.379.816 2.068 1.224 4.041 1.224 4.041s3.938-.385 4.165 1.732c.228 2.117-4.354 4.716-15.889 4.716-6.465-.001-13.135-2.358-13.452-4.331s4.845-2.262 4.845-2.262z" fill="#66757f"/><path d="m8.125 7.15s-.27 1.104-.406 1.871c-.136.768.226 1.296 2.705 1.824 3.287.7 10.679.692 15.058-.383 1.759-.432 2.886-.72 2.751-1.583-.167-1.068-.196-1.066-.541-2.208 0 0-1.477.502-3.427.96-2.66.624-9.964.911-13.481.144-1.874-.41-2.659-.625-2.659-.625zm-.136 13.953c-.354.145 2.921 1.378 7.48 1.458 4.771.084 6.234.39 5.146 1.459-1.146 1.125-.852 2.894-.771 3.418s2.047 1.916 2.208 2.56c.161.645-1.229 5.961-1.229 5.961l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604-.241-1.008 0-1 0-1z" fill="#292f33"/><path d="m6.989 21.144c-.354.146 2.921 1.378 7.48 1.458 4.771.084 6.234.39 5.146 1.459-1.146 1.125-.664 2.894-.583 3.418s1.859 1.916 2.021 2.561c.16.644-1.231 5.96-1.231 5.96l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604-.24-1.008.001-1 .001-1z" fill="#66757f"/><path d="m28.052 21.103c.354.145-2.921 1.378-7.479 1.458-4.771.084-6.234.39-5.146 1.459 1.146 1.125 2.976 2.892 2.896 3.416-.081.524-4.172 1.918-4.333 2.562-.161.645 1.229 5.961 1.229 5.961l8.729-.252c2.565-8.844 2.883-8.501 4.104-13.604.241-1.008 0-1 0-1z" fill="#292f33"/><path d="m28.958 21.103c.354.145-2.921 1.378-7.479 1.458-4.771.084-6.234.39-5.146 1.459 1.146 1.125 2.977 2.892 2.896 3.416s-4.172 1.918-4.333 2.562c-.161.645 1.229 5.961 1.229 5.961l8.657.01c2.565-8.844 2.955-8.763 4.177-13.866.24-1.008-.001-1-.001-1z" fill="#66757f"/></svg>
|
After Width: | Height: | Size: 3.5 KiB |
1
components/icons/svg/projects.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#77B255" d="M35 36v-5c0-3.314-2.686-6-6-6H13c-3.313 0-6 2.686-6 6v5h28z"/><path fill="#FFDC5D" d="M16.64 25.106c0 .894 2.36 1.993 4.36 1.993s4.359-1.099 4.359-1.992V21.29h-8.72v3.816z"/><path fill="#F9CA55" d="M16.632 22.973c1.216 1.374 2.724 1.746 4.364 1.746 1.639 0 3.146-.373 4.363-1.746v-3.491h-8.728v3.491z"/><path fill="#FFDC5D" d="M14.444 12.936c0 1.448-.734 2.622-1.639 2.622s-1.639-1.174-1.639-2.622.734-2.623 1.639-2.623c.905-.001 1.639 1.174 1.639 2.623m16.389 0c0 1.448-.733 2.622-1.639 2.622-.905 0-1.639-1.174-1.639-2.622s.733-2.623 1.639-2.623c.906-.001 1.639 1.174 1.639 2.623"/><path fill="#FFDC5D" d="M12.477 13.96c0-5.589 3.816-10.121 8.523-10.121s8.522 4.532 8.522 10.121S25.707 24.081 21 24.081c-4.706-.001-8.523-4.532-8.523-10.121"/><path fill="#C1694F" d="M21 20.802c-2.754 0-3.6-.705-3.741-.848-.256-.256-.256-.671 0-.927.248-.248.646-.255.902-.023.052.037.721.487 2.839.487 2.2 0 2.836-.485 2.842-.49.256-.255.657-.243.913.015.256.256.242.683-.014.938-.141.143-.987.848-3.741.848"/><path fill="#FFAC33" d="M21 0c5.648 0 9.178 4.648 9.178 8.121 0 3.473-.706 4.863-1.412 3.473l-1.412-2.778s-4.235 0-5.647-1.39c0 0 2.118 4.168-2.118 0 0 0 .706 2.779-3.53-.694 0 0-2.118 1.389-2.824 4.862-.196.964-1.412 0-1.412-3.473C11.822 4.648 14.646 0 21 0"/><path fill="#662113" d="M17 14c-.55 0-1-.45-1-1v-1c0-.55.45-1 1-1s1 .45 1 1v1c0 .55-.45 1-1 1m8 0c-.55 0-1-.45-1-1v-1c0-.55.45-1 1-1s1 .45 1 1v1c0 .55-.45 1-1 1"/><path fill="#C1694F" d="M21.75 16.75h-1.5c-.413 0-.75-.337-.75-.75s.337-.75.75-.75h1.5c.413 0 .75.337.75.75s-.337.75-.75.75"/><path fill="#E1E8ED" d="M33 35c0 .553-.447 1-1 1H22c-.553 0-1-.447-1-1 0-.553.447-1 1-1h10c.553 0 1 .447 1 1z"/><path fill="#E1E8ED" d="M20.24 22H3.759c-1.524 0-3.478.771-2.478 3.531l3.072 8.475C4.354 34.006 4.75 36 7 36h20l-4-11.24c-.438-1.322-1.235-2.76-2.76-2.76z"/><path fill="#99AAB5" d="M19.24 22H2.759c-1.524 0-3.478.771-2.478 3.531l3.072 8.475C3.354 34.006 3.75 36 6 36h20l-4-11.24c-.438-1.322-1.235-2.76-2.76-2.76z"/><path fill="#E1E8ED" d="M14.019 29.283c.524 1.572.099 3.13-.949 3.479-1.048.35-2.322-.641-2.846-2.213s-.099-3.13.949-3.479c1.048-.349 2.323.641 2.846 2.213zM19 24.75H3c-.414 0-.75-.336-.75-.75s.336-.75.75-.75h16c.414 0 .75.336.75.75s-.336.75-.75.75z"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
components/icons/svg/send.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m36 32a4 4 0 0 1 -4 4h-28a4 4 0 0 1 -4-4v-9c0-2.209.791-3 3-3h30c2.209 0 3 .791 3 3z" fill="#d99e82"/><path d="m25 20a7 7 0 1 1 -14 0z" fill="#662113"/><path d="m4 36h28a4 4 0 0 0 4-4h-36a4 4 0 0 0 4 4z" fill="#c1694f"/><path d="m27.435 8.511-7.863-7.863a2.23 2.23 0 0 0 -3.145 0l-7.863 7.863c-.864.866-.571 1.489.652 1.489h4.784v6a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-6h4.784c1.223 0 1.516-.623.651-1.489z" fill="#dd2e44"/></svg>
|
After Width: | Height: | Size: 494 B |
1
components/icons/svg/siren.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m34.16 28.812-2.916-26.134c-.17-1.525-1.459-2.678-2.993-2.678h-20.587c-1.545 0-2.839 1.168-2.997 2.704l-2.67 26.108z" fill="#dd2e44"/><circle cx="18.069" cy="14" fill="#be1931" r="9.366"/><path d="m35.521 29.18h-35.042l-.479 4.82c0 2 2 2 2 2h32s2 0 2-2z" fill="#99aab5"/><path d="m35.594 29.912-.073-.732c-.141-.738-.77-1.18-1.521-1.18h-32c-.751 0-1.38.442-1.521 1.18l-.073.732z" fill="#ccd6dd"/><path d="m29.647 13.63-7.668-1.248 4.539-6.308c.107-.148.091-.354-.039-.484-.131-.129-.336-.146-.484-.039l-6.309 4.538-1.247-7.667c-.029-.181-.187-.314-.37-.314s-.341.133-.37.314l-1.248 7.667-6.308-4.538c-.149-.107-.353-.09-.484.039-.13.131-.146.335-.039.484l4.538 6.308-7.668 1.248c-.181.029-.314.186-.314.37s.133.341.314.37l7.668 1.248-4.538 6.308c-.107.149-.091.354.039.484.131.129.335.146.484.039l6.308-4.538 1.248 7.667c.029.182.187.314.37.314s.341-.134.37-.314l1.247-7.667 6.308 4.538c.148.106.354.09.484-.039.13-.131.146-.335.039-.484l-4.538-6.308 7.668-1.248c.182-.029.314-.187.314-.37s-.132-.341-.314-.37z" fill="#ec9dad"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
components/icons/svg/tag.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path d="m32.017 20.181-14.672-14.435c-.658-.659-1.522-.746-2.385-.746h-10.077c-1.854 0-2.883 1.029-2.883 2.883v10.082c0 .861.089 1.723.746 2.38l14.554 14.672c1.311 1.31 3.378 1.31 4.688 0l10.059-10.088c1.31-1.312 1.28-3.438-.03-4.748zm-23.596-8.76c-.585.585-1.533.585-2.118 0s-.586-1.533 0-2.118c.585-.586 1.533-.585 2.118 0 .585.586.586 1.533 0 2.118z" fill="#ffd983"/><path d="m9.952 7.772c-1.43-1.431-3.749-1.431-5.179 0-1.431 1.43-1.431 3.749 0 5.18 1.43 1.43 3.749 1.43 5.18 0 1.43-1.431 1.429-3.749-.001-5.18zm-1.53 3.65c-.585.585-1.534.585-2.119 0s-.586-1.534 0-2.119c.585-.587 1.534-.585 2.119 0s.586 1.533 0 2.119z" fill="#d99e82"/><path d="m8.507 10.501c-.391.391-1.023.391-1.415 0-.391-.391-.39-1.023 0-1.414l8.485-8.485c.391-.391 1.023-.391 1.415 0 .391.391.39 1.023 0 1.414z" fill="#c1694f"/></svg>
|
After Width: | Height: | Size: 873 B |
1
components/icons/svg/tape.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#31373D" d="M36 25s0 4-4 4H4s-4 0-4-4V10c0-4 4-4 4-4h28s4 0 4 4v15z"/><path fill="#CCD6DD" d="M32 12h-6s-1 0-1 1v8s0 1 1 1h6c1 0 2-2 2-5s-1-5-2-5z"/><path d="M29.894 12H26s-1 0-1 1v1c1.656 0 3 1.343 3 3s-1.344 3-3 3v1s0 1 1 1h3.895C31.193 20.73 32 18.96 32 17c0-1.959-.808-3.729-2.106-5z"/><path fill="#66757F" d="M28 17c0-1.657-1.344-3-3-3v6c1.656 0 3-1.343 3-3z"/><path fill="#CCD6DD" d="M4 12h6s1 0 1 1v8s0 1-1 1H4c-1 0-2-2-2-5s1-5 2-5z"/><path d="M11 20c-1.657 0-3-1.343-3-3s1.343-3 3-3v-1c0-1-1-1-1-1H6.106C4.808 13.271 4 15.04 4 17s.808 3.729 2.106 5H10c1 0 1-1 1-1v-1z"/><path fill="#66757F" d="M8 17c0 1.657 1.343 3 3 3v-6c-1.657 0-3 1.343-3 3z"/><path fill="#88C9F9" d="M13 14s0-1 1-1h8s1 0 1 1v6s0 1-1 1h-8s-1 0-1-1v-6z"/><path d="M34 26c0 .553-.447 1-1 1H3c-.552 0-1-.447-1-1 0-.553.448-1 1-1h30c.553 0 1 .447 1 1z"/></svg>
|
After Width: | Height: | Size: 907 B |