feat(rewrite): Rewrote the whole thing in ReactJS 💅 (#22)

* feat(rewrite): Rewrote the whole thing in ReactJS 💅

* chore(docs): Updated
This commit is contained in:
Techno Tim 2021-08-25 19:16:09 -05:00 committed by GitHub
parent d9c4c97606
commit 84b1a800c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 10190 additions and 899 deletions

6
.babelrc Normal file
View File

@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}

23
.env.example Normal file
View File

@ -0,0 +1,23 @@
# this is used for local debugging
# RAZZLE_ variable is passed to the server
# copy this to .env for testing
RAZZLE_META_TITLE=Techno Tim
RAZZLE_META_DESCRIPTION=Techno Tim Link page
RAZZLE_META_AUTHOR=Techno Tim
RAZZLE_THEME=Dark
RAZZLE_FAVICON_URL=https://pbs.twimg.com/profile_images/1286144221217316864/qIAsKOpB_200x200.jpg
RAZZLE_AVATAR_URL=https://pbs.twimg.com/profile_images/1286144221217316864/qIAsKOpB_200x200.jpg
RAZZLE_AVATAR_2X_URL=https://pbs.twimg.com/profile_images/1286144221217316864/qIAsKOpB_400x400.jpg
RAZZLE_AVATAR_ALT=Techno Tim Profile Pic
RAZZLE_NAME=TechnoTim
RAZZLE_BIO=Hey! Just a place where you can connect with me!
RAZZLE_GITHUB=https://github.com/timothystewart6
RAZZLE_TWITTER=https://twitter.com/TechnoTimLive
RAZZLE_INSTAGRAM=https://www.instagram.com/techno.tim
RAZZLE_YOUTUBE=https://www.youtube.com/channel/UCOk-gHyjcWZNj3Br4oxwh0A
RAZZLE_TWITCH=https://www.twitch.tv/technotim/
RAZZLE_DISCORD=https://discord.gg/DJKexrJ
RAZZLE_TIKTOK=https://www.tiktok.com/@technotim
RAZZLE_KIT=https://kit.co/TechnoTim
RAZZLE_FOOTER=Thanks for stopping by!

13
.eslintignore Normal file
View File

@ -0,0 +1,13 @@
logs
*.log
npm-debug.log*
.DS_Store
coverage
node_modules
build
.env.local
.env.development.local
.env.test.local
.env.production.local
cache

42
.eslintrc Normal file
View File

@ -0,0 +1,42 @@
{
"parser": "@babel/eslint-parser",
"settings": {
"react": {
"pragma": "React",
"version": "detect"
}
},
"plugins": ["jsx","prettier"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"prettier"
],
"env": {
"jest": true,
"jasmine": true,
"node": true,
"browser": true,
"es6": true
},
"rules": {
"prettier/prettier":
["error", {
"singleQuote": true,
"jsxSingleQuote": false,
"arrowParens": "avoid",
"semi": true,
"trailingComma": "all",
"spaceAfterFunction": true
}],
"no-console": ["error", { "allow": ["warn", "error"] }],
"no-alert": "error",
"no-debugger": "error",
"prefer-const": "error",
"prefer-arrow-callback": "error",
"no-unused-vars": ["error", {"args": "none"}],
"react/prop-types": 0,
"react/no-unescaped-entities": 0
}
}

View File

@ -20,6 +20,8 @@ jobs:
- name: Install Dependencies, Test, and Build
run: |
yarn install --frozen-lockfile --check-files
yarn lint
yarn test
env:
CI: true

View File

@ -23,5 +23,7 @@ jobs:
- name: Install Dependencies, Test, and Build
run: |
yarn install --frozen-lockfile --check-files
yarn lint
yarn test
env:
CI: true

30
.gitignore vendored
View File

@ -1,26 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
logs
*.log
npm-debug.log*
.DS_Store
coverage
node_modules
build
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# this is is the output
www/index.html
.env
cache

13
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to dev server",
"type": "node",
"request": "attach",
"protocol": "inspector",
"address": "localhost",
"port": 9229
}
]
}

View File

@ -1,18 +1,23 @@
FROM node:14.17.4-alpine
FROM node:14.17.4-alpine AS node-build
RUN apk --no-cache add \
gettext \
bash
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --check-files --production=true
COPY www ./www
COPY template ./template
COPY env.js ./
COPY app.js ./
COPY ./entrypoint.sh /
RUN chmod +x /entrypoint.sh
EXPOSE 3000
ENTRYPOINT ["/entrypoint.sh"]
CMD [ "node", "app.js" ]
WORKDIR /usr/src/app
COPY package.json ./
COPY yarn.lock ./
COPY src ./src
COPY public ./public
RUN yarn install --frozen-lockfile --check-files
RUN yarn build --noninteractive
RUN yarn install --frozen-lockfile --check-files --production --modules-folder node_modules_prod
FROM node:14.17.4-alpine
WORKDIR /usr/src/app
ENV NODE_ENV production
RUN mkdir -p /node_modules
COPY --from=node-build /usr/src/app/build ./build
COPY --from=node-build /usr/src/app/node_modules_prod ./node_modules
EXPOSE 3000
CMD [ "node", "build/server.js" ]

View File

@ -1,7 +1,7 @@
# 🔗 LittleLink-Server
This project is based on the great work from [littlelink](https://github.com/sethcottle/littlelink)
It takes the same simple approach to a link page and hosts it within a nodeJS server containerized for you to use. Now, customizing `LittleLink` with `littlelink-server` is as easy as passing in some environment variables. If you need help configuring this, please see [this video that explains everything](https://www.youtube.com/watch?v=42SqfI_AjXU).
It takes the same simple approach to a link page and hosts it within a NodeJS server with React Server Side Rendering, containerized for you to use. Now, customizing `LittleLink` with `littlelink-server` is as easy as passing in some environment variables. If you need help configuring this, please see [this video that explains everything](https://www.youtube.com/watch?v=42SqfI_AjXU).
# 🚀 Getting Started
@ -45,7 +45,9 @@ services:
# - MEDIUM=https://medium.com
# - PINTEREST=https://www.pinterest.com/
# - EMAIL=you@example.com
# - EMAIL_TEXT=Email Me!
# - EMAIL_ALT=you@example.com
# - EMAIL_ALT_TEXT=Email me!
# - SOUND_CLOUD=https://souncloud.com
# - FIGMA=https://figma.com
# - TELEGRAM=https://telegram.org/
@ -87,20 +89,23 @@ docker run -d \
-e DISCORD='https://discord.gg/DJKexrJ' \
-e TIKTOK='https://www.tiktok.com/@technotim' \
-e KIT='https://kit.co/TechnoTim' \
-e EMAIL='someone@example.com' \
-e EMAIL_TEXT='Email me!' \
-e FOOTER=Thanks for stopping by! \
--restart unless-stopped \
ghcr.io/techno-tim/littlelink-server:latest
```
## Kubernetes
[unoffical helm chart provided by k8s-at-home](https://github.com/k8s-at-home/charts/tree/master/charts/stable/littlelink-server)
[Unofficial helm chart provided by k8s-at-home](https://github.com/k8s-at-home/charts/tree/master/charts/stable/littlelink-server)
```
helm repo add k8s-at-home https://k8s-at-home.com/charts/
helm repo update
helm install littlelink-server \
--set env.TZ="America/New York" \
--set env.META_TITLE="Technotim"
--set env.META_TITLE="TechnoTim"
k8s-at-home/littlelink-server
```
Or use a values.yaml files

31
app.js
View File

@ -1,31 +0,0 @@
const express = require('express')
const morgan = require('morgan')
const compression = require('compression')
const fs = require('fs')
const jsdom = require('jsdom')
const useEnv = require('./env')
fs.readFile('./template/index.html', 'utf8', (err, file) => {
if (err) {
throw err;
}
const { JSDOM } = jsdom
const dom = new JSDOM(file);
const html = "<!DOCTYPE html>\n" + useEnv(dom.window.document).documentElement.outerHTML;
fs.writeFile('./www/index.html', html, 'utf8', (err) => {
if (err) {
throw err;
}
});
});
const app = express();
app.use(morgan('combined'));
app.use(express.static('www'));
app.use(compression())
const server = app.listen(process.env.PORT || 3000, () => {
const host = server.address().address
const port = server.address().port
console.log('Express app listening at http://%s:%s', host, port)
})

View File

@ -19,13 +19,12 @@ services:
- BIO=Hey! Just a place where you can connect with me!
- GITHUB=https://github.com/timothystewart6
- TWITTER=https://twitter.com/TechnoTimLive
- MASTODON=https://mastodon.social/TechnoTimLive
- INSTAGRAM=https://www.instagram.com/techno.tim
- YOUTUBE=https://www.youtube.com/channel/UCOk-gHyjcWZNj3Br4oxwh0A/
- TWITCH=https://www.twitch.tv/technotim/
- DISCORD=https://discord.gg/DJKexrJ
- KIT=https://kit.co/TechnoTim
- FOOTER=Thanks for stopping by!
- FOOTER=Thanks for stopping by!!!
ports:
- 8080:3000

View File

@ -1,6 +0,0 @@
#!/bin/bash
originalfile="/usr/src/app/env.js"
tmpfile=$(mktemp)
cp --attributes-only --preserve $originalfile $tmpfile
cat $originalfile | envsubst | tee $tmpfile && mv $tmpfile $originalfile
exec "$@"

390
env.js
View File

@ -1,390 +0,0 @@
module.exports = useEnv;
var env = {
META_TITLE: '$META_TITLE',
META_DESCRIPTION: '$META_DESCRIPTION',
META_AUTHOR: '$META_AUTHOR',
THEME: '$THEME',
FAVICON_URL: '$FAVICON_URL',
AVATAR_URL: '$AVATAR_URL',
AVATAR_ALT: '$AVATAR_ALT',
AVATAR_2X_URL: '$AVATAR_2X_URL',
NAME: '$NAME',
BIO: '$BIO',
GITHUB: '$GITHUB',
TWITTER: '$TWITTER',
MASTODON: '$MASTODON',
MICRO_BLOG: '$MICRO_BLOG',
INSTAGRAM: '$INSTAGRAM',
FACEBOOK: '$FACEBOOK',
FACEBOOK_MESSENGER: '$FACEBOOK_MESSENGER',
LINKED_IN: '$LINKED_IN',
YOUTUBE: '$YOUTUBE',
DISCORD: '$DISCORD',
TWITCH: '$TWITCH',
PRODUCT_HUNT: '$PRODUCT_HUNT',
SNAPCHAT: '$SNAPCHAT',
SPOTIFY: '$SPOTIFY',
REDDIT: '$REDDIT',
MEDIUM: '$MEDIUM',
PINTEREST: '$PINTEREST',
TIKTOK: '$TIKTOK',
EMAIL: '$EMAIL',
EMAIL_TEXT: '$EMAIL_TEXT',
EMAIL_ALT: '$EMAIL_ALT',
EMAIL_ALT_TEXT: '$EMAIL_ALT_TEXT',
SOUND_CLOUD: '$SOUND_CLOUD',
FIGMA: '$FIGMA',
KIT: '$KIT',
TELEGRAM: '$TELEGRAM',
TUMBLR: '$TUMBLR',
STEAM: '$STEAM',
VIMEO: '$VIMEO',
WORDPRESS: '$WORDPRESS',
GOODREADS: '$GOODREADS',
SKOOB: '$SKOOB',
WHATSAPP: '$WHATSAPP',
LETTERBOXD: '$LETTERBOXD',
FOOTER: '$FOOTER',
}
function useEnv(document) {
var metalTitleEl = document.getElementById('meta-title');
if (env.META_TITLE) {
metalTitleEl.textContent = env.META_TITLE;
} else {
metalTitleEl.remove()
}
var metalDescriptionEl = document.getElementById('meta-description');
if (env.META_DESCRIPTION) {
metalDescriptionEl.content = env.META_DESCRIPTION;
} else {
metalDescriptionEl.remove()
}
var metaAuthorEl = document.getElementById('meta-author');
if (env.META_AUTHOR) {
metaAuthorEl.content = env.META_AUTHOR;
} else {
metaAuthorEl.remove()
}
var themeEl = document.getElementById('theme');
if (env.THEME && env.THEME.toLocaleLowerCase() === 'dark') {
themeEl.href = 'css/skeleton-dark.css';
} else {
themeEl.href = 'css/skeleton-light.css';
}
var faviconEl = document.getElementById('favicon');
if (env.FAVICON_URL) {
faviconEl.href = env.FAVICON_URL;
}
var avatarEl = document.getElementById('avatar');
if (env.AVATAR_URL) {
avatarEl.src = env.AVATAR_URL;
avatarEl.alt = env.AVATAR_ALT;
avatarEl.srcset = env.AVATAR_2X_URL + ' 2x';
} else {
avatarEl.remove()
}
var nameEl = document.getElementById('name');
if (env.NAME) {
nameEl.innerHTML = env.NAME;
} else {
nameEl.remove()
}
var bioEl = document.getElementById('bio');
if (env.BIO) {
bioEl.innerHTML = env.BIO;
} else {
bioEl.remove()
}
var githubEl = document.getElementById('github');
if (env.GITHUB) {
githubEl.href = env.GITHUB;
} else {
githubEl.nextElementSibling.remove()
githubEl.remove()
}
var twitterEl = document.getElementById('twitter');
if (env.TWITTER) {
twitterEl.href = env.TWITTER;
} else {
twitterEl.nextElementSibling.remove()
twitterEl.remove()
}
var mastodonEl = document.getElementById('mastodon');
if (env.MASTODON) {
mastodonEl.href = env.MASTODON;
} else {
mastodonEl.nextElementSibling.remove()
mastodonEl.remove()
}
var microblogEl = document.getElementById('microblog');
if (env.MICRO_BLOG) {
microblogEl.href = env.MICRO_BLOG;
} else {
microblogEl.nextElementSibling.remove()
microblogEl.remove()
}
var instagramEl = document.getElementById('instagram');
if (env.INSTAGRAM) {
instagramEl.href = env.INSTAGRAM;
} else {
instagramEl.nextElementSibling.remove()
instagramEl.remove()
}
var facebookEl = document.getElementById('facebook');
if (env.FACEBOOK) {
facebookEl.href = env.FACEBOOK;
} else {
facebookEl.nextElementSibling.remove()
facebookEl.remove()
}
var facebookMessengerEl = document.getElementById('facebook-messenger');
if (env.FACEBOOK_MESSENGER) {
facebookMessengerEl.href = env.FACEBOOK_MESSENGER;
} else {
facebookMessengerEl.nextElementSibling.remove()
facebookMessengerEl.remove()
}
var linkedInEl = document.getElementById('linkedin');
if (env.LINKED_IN) {
linkedInEl.href = env.LINKED_IN;
} else {
linkedInEl.nextElementSibling.remove()
linkedInEl.remove()
}
var youTubeEl = document.getElementById('youtube');
if (env.YOUTUBE) {
youTubeEl.href = env.YOUTUBE;
} else {
youTubeEl.nextElementSibling.remove()
youTubeEl.remove()
}
var discordEl = document.getElementById('discord');
if (env.DISCORD) {
discordEl.href = env.DISCORD;
} else {
discordEl.nextElementSibling.remove()
discordEl.remove()
}
var twitchEl = document.getElementById('twitch');
if (env.TWITCH) {
twitchEl.href = env.TWITCH;
} else {
twitchEl.nextElementSibling.remove()
twitchEl.remove()
}
var productHunEl = document.getElementById('producthunt');
if (env.PRODUCT_HUNT) {
productHunEl.href = env.PRODUCT_HUNT;
} else {
productHunEl.nextElementSibling.remove()
productHunEl.remove()
}
var snapchatEl = document.getElementById('snapchat');
if (env.SNAPCHAT) {
snapchatEl.href = env.SNAPCHAT;
} else {
snapchatEl.nextElementSibling.remove()
snapchatEl.remove()
}
var spotifyEl = document.getElementById('spotify');
if (env.SPOTIFY) {
spotifyEl.href = env.SPOTIFY;
} else {
spotifyEl.nextElementSibling.remove()
spotifyEl.remove()
}
var redditEl = document.getElementById('reddit');
if (env.REDDIT) {
redditEl.href = env.REDDIT;
} else {
redditEl.nextElementSibling.remove()
redditEl.remove()
}
var mediumEl = document.getElementById('medium');
if (env.MEDIUM) {
mediumEl.href = env.MEDIUM;
} else {
mediumEl.nextElementSibling.remove()
mediumEl.remove()
}
var pinterestEl = document.getElementById('pinterest');
if (env.PINTEREST) {
pinterestEl.href = env.PINTEREST;
} else {
pinterestEl.nextElementSibling.remove()
pinterestEl.remove()
}
var tiktokEl = document.getElementById('tiktok');
if (env.TIKTOK) {
tiktokEl.href = env.TIKTOK;
} else {
tiktokEl.nextElementSibling.remove()
tiktokEl.remove()
}
var emailEl = document.getElementById('email');
if (env.EMAIL) {
emailEl.innerHTML = env.EMAIL_TEXT || env.EMAIL
emailEl.href = 'mailto:' + env.EMAIL;
} else {
emailEl.nextElementSibling.remove()
emailEl.remove()
}
var emailAltEl = document.getElementById('email-alt');
if (env.EMAIL_ALT) {
emailAltEl.innerHTML = env.EMAIL_ALT_TEXT || env.EMAIL_ALT
emailAltEl.href = 'mailto:' + env.EMAIL_ALT;
} else {
emailAltEl.nextElementSibling.remove()
emailAltEl.remove()
}
var soundCloudEl = document.getElementById('soundcloud');
if (env.SOUND_CLOUD) {
soundCloudEl.href = env.SOUND_CLOUD;
} else {
soundCloudEl.nextElementSibling.remove()
soundCloudEl.remove()
}
var figmaEl = document.getElementById('figma');
if (env.FIGMA) {
figmaEl.href = env.FIGMA;
} else {
figmaEl.nextElementSibling.remove()
figmaEl.remove()
}
var kitEl = document.getElementById('kit');
if (env.KIT) {
kitEl.href = env.KIT;
} else {
kitEl.nextElementSibling.remove()
kitEl.remove()
}
var telegramEl = document.getElementById('telegram');
if (env.TELEGRAM) {
telegramEl.href = env.TELEGRAM;
} else {
telegramEl.nextElementSibling.remove()
telegramEl.remove()
}
var temblrEl = document.getElementById('tumblr');
if (env.TUMBLR) {
temblrEl.href = env.TUMBLR;
} else {
temblrEl.nextElementSibling.remove()
temblrEl.remove()
}
var steamEl = document.getElementById('steam');
if (env.STEAM) {
steamEl.href = env.STEAM;
} else {
steamEl.nextElementSibling.remove()
steamEl.remove()
}
var vimeoEl = document.getElementById('vimeo');
if (env.VIMEO) {
vimeoEl.href = env.VIMEO;
} else {
vimeoEl.nextElementSibling.remove()
vimeoEl.remove()
}
var wordpressEl = document.getElementById('wordpress');
if (env.WORDPRESS) {
wordpressEl.href = env.WORDPRESS;
} else {
wordpressEl.nextElementSibling.remove()
wordpressEl.remove()
}
var goodreadsEl = document.getElementById('goodreads');
if (env.GOODREADS) {
goodreadsEl.href = env.GOODREADS;
} else {
goodreadsEl.nextElementSibling.remove()
goodreadsEl.remove()
}
var skoobEl = document.getElementById('skoob');
if (env.SKOOB) {
skoobEl.href = env.SKOOB;
} else {
skoobEl.nextElementSibling.remove()
skoobEl.remove()
}
var whatsappEl = document.getElementById('whatsapp');
if (env.WHATSAPP) {
whatsappEl.href = env.WHATSAPP;
} else {
whatsappEl.nextElementSibling.remove()
whatsappEl.remove()
}
var letterboxdEl = document.getElementById('letterboxd');
if (env.LETTERBOXD) {
letterboxdEl.href = env.LETTERBOXD;
} else {
letterboxdEl.nextElementSibling.remove()
letterboxdEl.remove();
}
var footerEl = document.getElementById('footer');
if (env.FOOTER) {
footerEl.innerHTML = env.FOOTER;
} else {
footerEl.remove()
}
return document;
}

View File

@ -6,33 +6,40 @@
"license": "MIT",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
"start": "razzle start",
"build": "razzle build",
"debug": "yarn start --inspect --inspect-port 9230",
"test": "razzle test --env=jsdom",
"start:prod": "NODE_ENV=production node build/server.js",
"lint": "eslint ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/timothystewart6/littlelink.git"
},
"keywords": [
"littlelink",
"linktree",
"linktr.ee",
"javascript",
"open",
"source",
"html",
"css",
"docker"
],
"author": "Timothy Stewart",
"bugs": {
"url": "https://github.com/timothystewart6/littlelink/issues"
},
"homepage": "https://github.com/timothystewart6/littlelink#readme",
"dependencies": {
"compression": "^1.7.4",
"express": "^4.17.1",
"morgan": "^1.10.0",
"jsdom": "^17.0.0"
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"serialize-javascript": "^6.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"babel-preset-razzle": "4.0.6",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.24.1",
"eslint-plugin-jsx": "^0.1.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.24.0",
"html-webpack-plugin": "^4.5.2",
"mini-css-extract-plugin": "^0.9.0",
"prettier": "^2.3.2",
"razzle": "^4.0.6",
"razzle-dev-utils": "^4.0.6",
"webpack": "^4.44.1",
"webpack-dev-server": "^3.11.2"
}
}

View File

@ -317,11 +317,3 @@ button:hover,
.button.button-wordpress:hover,
.button.button-wordpress:focus {
filter: brightness(90%) }
/* WhatsApp */
.button.button-whatsapp {
color: #FFFFFF;
background-color: #2DB842 }
.button.button-whatsapp:hover,
.button.button-whatsapp:focus {
filter: brightness(90%) }

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *

5
sandbox.config.json Normal file
View File

@ -0,0 +1,5 @@
{
"container": {
"port": 3000
}
}

15
src/client.js Normal file
View File

@ -0,0 +1,15 @@
import App from './components/App/App';
import { BrowserRouter } from 'react-router-dom';
import React from 'react';
import { hydrate } from 'react-dom';
hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root'),
);
if (module.hot) {
module.hot.accept();
}

13
src/components/App/App.js Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Home from '../Home/Home';
const App = () => (
<div className="container">
<Switch>
<Route exact path="/" component={Home} />
</Switch>
</div>
);
export default App;

View File

@ -0,0 +1,16 @@
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';
import { MemoryRouter } from 'react-router-dom';
describe('<App />', () => {
test('renders without exploding', () => {
const div = document.createElement('div');
ReactDOM.render(
<MemoryRouter>
<App />
</MemoryRouter>,
div,
);
});
});

View File

@ -0,0 +1,6 @@
.avatar {
vertical-align: middle;
width: 128px;
height: 128px;
border-radius: 50%;
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import { string } from 'prop-types';
import './Avatar.css';
function Avatar(props) {
const { src, srcSet, alt } = props;
return (
<img id="avatar" className="avatar" src={src} srcSet={srcSet} alt={alt} />
);
}
export default Avatar;
Avatar.propType = {
src: string,
srcSet: string,
alt: string,
};

View File

@ -0,0 +1,33 @@
import React from 'react';
import { string } from 'prop-types';
function Button(props) {
const { name, href, displayName, logo } = props;
return (
<>
<a
id={name}
className={`button button-${name}`}
href={href}
target="_blank"
rel="noopener noreferrer"
>
{logo && (
<img className="icon" src={logo} alt={`${displayName} logo`} />
)}
{displayName}
</a>
</>
);
}
export default Button;
Button.propType = {
src: string.isRequired,
srcSet: string,
alt: string.isRequired,
href: string,
};

View File

@ -0,0 +1,10 @@
import Button from './Button';
import React from 'react';
import ReactDOM from 'react-dom';
describe('<Button />', () => {
test('renders without exploding', () => {
const div = document.createElement('div');
ReactDOM.render(<Button />, div);
});
});

288
src/components/Home/Home.js Normal file
View File

@ -0,0 +1,288 @@
import React from 'react';
import Avatar from '../Avatar/Avatar';
import Button from '../Button/Button';
import { runtimeConfig } from '../../config';
import githubLogo from '../../icons/github.svg';
import instagramLogo from '../../icons/instagram.svg';
import kitLogo from '../../icons/kit.svg';
import tiktokLogo from '../../icons/tiktok.svg';
import twitchLogo from '../../icons/twitch.svg';
import twitterLogo from '../../icons/twitter.svg';
import youtubeLogo from '../../icons/youtube.svg';
import facebookLogo from '../../icons/facebook.svg';
import messengerLogo from '../../icons/messenger.svg';
import linkedinLogo from '../../icons/linkedin.svg';
import producthuntLogo from '../../icons/producthunt.svg';
import snapchatLogo from '../../icons/snapchat.svg';
import spotifyLogo from '../../icons/spotify.svg';
import redditLogo from '../../icons/reddit.svg';
import mediumLogo from '../../icons/medium.svg';
import pinterestLogo from '../../icons/pinterest.svg';
import soundcloudLogo from '../../icons/soundcloud.svg';
import figmaLogo from '../../icons/figma.svg';
import telegramLogo from '../../icons/telegram.svg';
import tumblrLogo from '../../icons/tumblr.svg';
import steamLogo from '../../icons/steam.svg';
import vimeoLogo from '../../icons/vimeo.svg';
import wordpressLogo from '../../icons/wordpress.svg';
import goodreadsLogo from '../../icons/goodreads.svg';
import skoobLogo from '../../icons/skoob.svg';
import letterboxdLogo from '../../icons/letterboxd.svg';
import mastodonLogo from '../../icons/mastodon.svg';
function Home(props) {
return (
<>
<div className="container">
<div className="row">
<div className="column" style={{ marginTop: '10%' }}>
<Avatar
src={runtimeConfig.AVATAR_URL}
srcSet={runtimeConfig.AVATAR_2X_URL}
alt={runtimeConfig.AVATAR_ALT}
/>
<h1 id="name">{`${runtimeConfig.NAME}`}</h1>
<p id="bio">{runtimeConfig.BIO}</p>
{runtimeConfig.GITHUB && (
<Button
name="github"
href={runtimeConfig.GITHUB}
displayName="GitHub"
logo={githubLogo}
/>
)}
{runtimeConfig.TWITTER && (
<Button
name="twitter"
href={runtimeConfig.TWITTER}
displayName="Twitter"
logo={twitterLogo}
/>
)}
{runtimeConfig.INSTAGRAM && (
<Button
name="instagram"
href={runtimeConfig.INSTAGRAM}
displayName="Instagram"
logo={instagramLogo}
/>
)}
{runtimeConfig.YOUTUBE && (
<Button
name="youtube"
href={runtimeConfig.YOUTUBE}
displayName="YouTube"
logo={youtubeLogo}
/>
)}
{runtimeConfig.TWITCH && (
<Button
name="twitch"
href={runtimeConfig.TWITCH}
displayName="Twitch"
logo={twitchLogo}
/>
)}
{runtimeConfig.TIKTOK && (
<Button
name="tiktok"
href={runtimeConfig.TIKTOK}
displayName="TikTok"
logo={tiktokLogo}
/>
)}
{runtimeConfig.KIT && (
<Button
name="kit"
href={runtimeConfig.KIT}
displayName="Kit"
logo={kitLogo}
/>
)}
{runtimeConfig.FACEBOOK && (
<Button
name="facebook"
href={runtimeConfig.FACEBOOK}
displayName="Facebook"
logo={facebookLogo}
/>
)}
{runtimeConfig.FACEBOOK_MESSENGER && (
<Button
name="messenger"
href={runtimeConfig.FACEBOOK_MESSENGER}
displayName="Messenger"
logo={messengerLogo}
/>
)}
{runtimeConfig.LINKED_IN && (
<Button
name="linkedin"
href={runtimeConfig.LINKED_IN}
displayName="LinkedIn"
logo={linkedinLogo}
/>
)}
{runtimeConfig.PRODUCT_HUNT && (
<Button
name="producthunt"
href={runtimeConfig.PRODUCT_HUNT}
displayName="Product Hunt"
logo={producthuntLogo}
/>
)}
{runtimeConfig.SNAPCHAT && (
<Button
name="snapchat"
href={runtimeConfig.SNAPCHAT}
displayName="SnapChat"
logo={snapchatLogo}
/>
)}
{runtimeConfig.SPOTIFY && (
<Button
name="spotify"
href={runtimeConfig.SPOTIFY}
displayName="Spotify"
logo={spotifyLogo}
/>
)}
{runtimeConfig.REDDIT && (
<Button
name="reddit"
href={runtimeConfig.REDDIT}
displayName="Reddit"
logo={redditLogo}
/>
)}
{runtimeConfig.MEDIUM && (
<Button
name="medium"
href={runtimeConfig.MEDIUM}
displayName="Medium"
logo={mediumLogo}
/>
)}
{runtimeConfig.PINTEREST && (
<Button
name="pinterest"
href={runtimeConfig.PINTEREST}
displayName="Pinterest"
logo={pinterestLogo}
/>
)}
{runtimeConfig.EMAIL && (
<Button
name="default"
href={`mailto:${runtimeConfig.EMAIL}`}
displayName={runtimeConfig.EMAIL_TEXT}
/>
)}
{runtimeConfig.EMAIL_ALT && (
<Button
name="default"
href={`mailto:${runtimeConfig.EMAIL_ALT_TEXT}`}
displayName={runtimeConfig.EMAIL_ALT_TEXT}
/>
)}
{runtimeConfig.SOUND_CLOUD && (
<Button
name="soundcloud"
href={runtimeConfig.SOUND_CLOUD}
displayName="SoundCloud"
logo={soundcloudLogo}
/>
)}
{runtimeConfig.FIGMA && (
<Button
name="figma"
href={runtimeConfig.FIGMA}
displayName="Figma"
logo={figmaLogo}
/>
)}
{runtimeConfig.TELEGRAM && (
<Button
name="telegram"
href={runtimeConfig.TELEGRAM}
displayName="Telegram"
logo={telegramLogo}
/>
)}
{runtimeConfig.TUMBLR && (
<Button
name="tumblr"
href={runtimeConfig.TUMBLR}
displayName="Tumblr"
logo={tumblrLogo}
/>
)}
{runtimeConfig.STEAM && (
<Button
name="steam"
href={runtimeConfig.STEAM}
displayName="Steam"
logo={steamLogo}
/>
)}
{runtimeConfig.VIMEO && (
<Button
name="vimeo"
href={runtimeConfig.VIMEO}
displayName="Vimeo"
logo={vimeoLogo}
/>
)}
{runtimeConfig.WORDPRESS && (
<Button
name="wordpress"
href={runtimeConfig.WORDPRESS}
displayName="Wordpress"
logo={wordpressLogo}
/>
)}
{runtimeConfig.GOODREADS && (
<Button
name="goodreads"
href={runtimeConfig.GOODREADS}
displayName="Goodreads"
logo={goodreadsLogo}
/>
)}
{runtimeConfig.SKOOB && (
<Button
name="skoob"
href={runtimeConfig.SKOOB}
displayName="Skoob"
logo={skoobLogo}
/>
)}
{runtimeConfig.LETTERBOXD && (
<Button
name="letterboxd"
href={runtimeConfig.LETTERBOXD}
displayName="LetterBoxd"
logo={letterboxdLogo}
/>
)}
{runtimeConfig.MASTODON && (
<Button
name="mastodon"
href={runtimeConfig.MASTODON}
displayName="Mastodon"
logo={mastodonLogo}
/>
)}
<p id="footer">{runtimeConfig.FOOTER}</p>
</div>
</div>
</div>
</>
);
}
export default Home;

View File

@ -0,0 +1,10 @@
import Home from './Home';
import React from 'react';
import ReactDOM from 'react-dom';
describe('<Home />', () => {
test('renders without exploding', () => {
const div = document.createElement('div');
ReactDOM.render(<Home />, div);
});
});

162
src/config.js Normal file
View File

@ -0,0 +1,162 @@
// config.js
const nodeIsProduction = process.env.NODE_ENV === 'production';
export const runtimeConfig =
typeof window !== 'undefined'
? {
// client
META_TITLE: window?.env?.META_TITLE,
META_DESCRIPTION: window?.env?.META_DESCRIPTION,
META_AUTHOR: window?.env?.META_AUTHOR,
THEME: window?.env?.THEME,
FAVICON_URL: window?.env?.FAVICON_URL,
AVATAR_URL: window?.env?.AVATAR_URL,
AVATAR_ALT: window?.env?.AVATAR_ALT,
AVATAR_2X_URL: window?.env?.AVATAR_2X_URL,
NAME: window?.env?.NAME,
BIO: window?.env?.BIO,
GITHUB: window?.env?.GITHUB,
TWITTER: window?.env?.TWITTER,
MICRO_BLOG: window?.env?.MICRO_BLOG,
INSTAGRAM: window?.env?.INSTAGRAM,
FACEBOOK: window?.env?.FACEBOOK,
FACEBOOK_MESSENGER: window?.env?.FACEBOOK_MESSENGER,
LINKED_IN: window?.env?.LINKED_IN,
YOUTUBE: window?.env?.YOUTUBE,
DISCORD: window?.env?.DISCORD,
TWITCH: window?.env?.TWITCH,
PRODUCT_HUNT: window?.env?.PRODUCT_HUNT,
SNAPCHAT: window?.env?.SNAPCHAT,
SPOTIFY: window?.env?.SPOTIFY,
REDDIT: window?.env?.REDDIT,
MEDIUM: window?.env?.MEDIUM,
PINTEREST: window?.env?.PINTEREST,
TIKTOK: window?.env?.TIKTOK,
EMAIL: window?.env?.EMAIL,
EMAIL_TEXT: window?.env?.EMAIL_TEXT,
EMAIL_ALT: window?.env?.EMAIL_ALT,
EMAIL_ALT_TEXT: window?.env?.EMAIL_ALT_TEXT,
SOUND_CLOUD: window?.env?.SOUND_CLOUD,
FIGMA: window?.env?.FIGMA,
KIT: window?.env?.KIT,
TELEGRAM: window?.env?.TELEGRAM,
TUMBLR: window?.env?.TUMBLR,
STEAM: window?.env?.STEAM,
VIMEO: window?.env?.VIMEO,
WORDPRESS: window?.env?.WORDPRESS,
GOODREADS: window?.env?.GOODREADS,
SKOOB: window?.env?.SKOOB,
LETTERBOXD: window?.env?.LETTERBOXD,
MASTODON: window?.env?.MASTODON,
FOOTER: window?.env?.FOOTER,
}
: {
// server
META_TITLE: nodeIsProduction
? process.env.META_TITLE
: process.env.RAZZLE_META_TITLE,
META_DESCRIPTION: nodeIsProduction
? process.env.META_DESCRIPTION
: process.env.RAZZLE_META_DESCRIPTION,
META_AUTHOR: nodeIsProduction
? process.env.META_AUTHOR
: process.env.RAZZLE_META_AUTHOR,
THEME: nodeIsProduction ? process.env.THEME : process.env.RAZZLE_THEME,
FAVICON_URL: nodeIsProduction
? process.env.FAVICON_URL
: process.env.RAZZLE_FAVICON_URL,
AVATAR_URL: nodeIsProduction
? process.env.AVATAR_URL
: process.env.RAZZLE_AVATAR_URL,
AVATAR_ALT: nodeIsProduction
? process.env.AVATAR_ALT
: process.env.RAZZLE_AVATAR_ALT,
AVATAR_2X_URL: nodeIsProduction
? process.env.AVATAR_2X_URL
: process.env.RAZZLE_AVATAR_2X_URL,
NAME: nodeIsProduction ? process.env.NAME : process.env.RAZZLE_NAME,
BIO: nodeIsProduction ? process.env.BIO : process.env.RAZZLE_BIO,
GITHUB: nodeIsProduction
? process.env.GITHUB
: process.env.RAZZLE_GITHUB,
TWITTER: nodeIsProduction
? process.env.GITHUB
: process.env.RAZZLE_TWITTER,
INSTAGRAM: nodeIsProduction
? process.env.INSTAGRAM
: process.env.RAZZLE_INSTAGRAM,
YOUTUBE: nodeIsProduction
? process.env.YOUTUBE
: process.env.RAZZLE_YOUTUBE,
TWITCH: nodeIsProduction
? process.env.TWITCH
: process.env.RAZZLE_TWITCH,
TIKTOK: nodeIsProduction
? process.env.TIKTOK
: process.env.RAZZLE_TIKTOK,
KIT: nodeIsProduction ? process.env.KIT : process.env.RAZZLE_KIT,
FACEBOOK: nodeIsProduction
? process.env.FACEBOOK
: process.env.RAZZLE_FACEBOOK,
FACEBOOK_MESSENGER: nodeIsProduction
? process.env.FACEBOOK_MESSENGER
: process.env.RAZZLE_FACEBOOK_MESSENGER,
LINKED_IN: nodeIsProduction
? process.env.LINKED_IN
: process.env.RAZZLE_LINKED_IN,
PRODUCT_HUNT: nodeIsProduction
? process.env.PRODUCT_HUNT
: process.env.RAZZLE_PRODUCT_HUNT,
SNAPCHAT: nodeIsProduction
? process.env.SNAPCHAT
: process.env.RAZZLE_SNAPCHAT,
SPOTIFY: nodeIsProduction
? process.env.SPOTIFY
: process.env.RAZZLE_SPOTIFY,
REDDIT: nodeIsProduction
? process.env.REDDIT
: process.env.RAZZLE_REDDIT,
MEDIUM: nodeIsProduction
? process.env.MEDIUM
: process.env.RAZZLE_MEDIUM,
PINTEREST: nodeIsProduction
? process.env.PINTEREST
: process.env.RAZZLE_PINTEREST,
EMAIL: nodeIsProduction ? process.env.EMAIL : process.env.RAZZLE_EMAIL,
EMAIL_TEXT: nodeIsProduction
? process.env.EMAIL_TEXT
: process.env.RAZZLE_EMAIL_TEXT,
EMAIL_ALT: nodeIsProduction
? process.env.EMAIL_ALT
: process.env.RAZZLE_EMAIL_ALT,
EMAIL_ALT_TEXT: nodeIsProduction
? process.env.EMAIL_ALT_TEXT
: process.env.RAZZLE_EMAIL_ALT_TEXT,
SOUND_CLOUD: nodeIsProduction
? process.env.SOUND_CLOUD
: process.env.RAZZLE_SOUND_CLOUD,
FIGMA: nodeIsProduction ? process.env.FIGMA : process.env.RAZZLE_FIGMA,
TELEGRAM: nodeIsProduction
? process.env.TELEGRAM
: process.env.RAZZLE_TELEGRAM,
TUMBLR: nodeIsProduction
? process.env.TUMBLR
: process.env.RAZZLE_TUMBLR,
STEAM: nodeIsProduction ? process.env.STEAM : process.env.RAZZLE_STEAM,
VIMEO: nodeIsProduction ? process.env.VIMEO : process.env.RAZZLE_VIMEO,
WORDPRESS: nodeIsProduction
? process.env.WORDPRESS
: process.env.RAZZLE_WORDPRESS,
GOODREADS: nodeIsProduction
? process.env.GOODREADS
: process.env.RAZZLE_GOODREADS,
SKOOB: nodeIsProduction ? process.env.SKOOB : process.env.RAZZLE_SKOOB,
LETTERBOXD: nodeIsProduction
? process.env.LETTERBOXD
: process.env.RAZZLE_LETTERBOXD,
MASTODON: nodeIsProduction
? process.env.MICRO_BLOG
: process.env.RAZZLE_MICRO_BLOG,
FOOTER: nodeIsProduction
? process.env.FOOTER
: process.env.RAZZLE_FOOTER,
};

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 677 B

After

Width:  |  Height:  |  Size: 677 B

View File

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 482 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 642 B

After

Width:  |  Height:  |  Size: 642 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 736 B

After

Width:  |  Height:  |  Size: 736 B

View File

Before

Width:  |  Height:  |  Size: 907 B

After

Width:  |  Height:  |  Size: 907 B

View File

Before

Width:  |  Height:  |  Size: 722 B

After

Width:  |  Height:  |  Size: 722 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 729 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1005 B

After

Width:  |  Height:  |  Size: 1005 B

View File

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 944 B

View File

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 458 B

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 579 B

After

Width:  |  Height:  |  Size: 579 B

View File

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 454 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 816 B

28
src/index.js Normal file
View File

@ -0,0 +1,28 @@
/* eslint-disable no-console */
import express from 'express';
let app = require('./server').default;
if (module.hot) {
module.hot.accept('./server', () => {
console.log('🔁 HMR Reloading `./server`...');
try {
app = require('./server').default;
} catch (error) {
console.error(error);
}
});
console.info('✅ Server-side HMR Enabled!');
}
const port = process.env.PORT || 3000;
export default express()
.use((req, res) => app.handle(req, res))
.listen(port, err => {
if (err) {
console.error(err);
return;
}
console.log(`> Started on port ${port}`);
});

92
src/server.js Normal file
View File

@ -0,0 +1,92 @@
import App from './components/App/App';
import React from 'react';
import { StaticRouter } from 'react-router-dom';
import express from 'express';
import { renderToString } from 'react-dom/server';
import { runtimeConfig } from './config';
import serialize from 'serialize-javascript'; // Safer stringify, prevents XSS attacks
import morgan from 'morgan';
import compression from 'compression';
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
const cssLinksFromAssets = (assets, entrypoint) => {
return assets[entrypoint]
? assets[entrypoint].css
? assets[entrypoint].css
.map(asset => `<link rel="stylesheet" href="${asset}">`)
.join('')
: ''
: '';
};
const jsScriptTagsFromAssets = (assets, entrypoint, extra = '') => {
return assets[entrypoint]
? assets[entrypoint].js
? assets[entrypoint].js
.map(asset => `<script src="${asset}"${extra}></script>`)
.join('')
: ''
: '';
};
const theme = runtimeConfig.THEME === 'Dark' ? 'dark.css' : 'light.css';
const server = express();
if (process.env.NODE_ENV === 'production') {
server.use(morgan('combined'));
server.use(compression());
}
server
.disable('x-powered-by')
.use(express.static(process.env.RAZZLE_PUBLIC_DIR))
.get('/*', (req, res) => {
const context = {};
const markup = renderToString(
<StaticRouter context={context} location={req.url}>
<App />
</StaticRouter>,
);
if (context.url) {
res.redirect(context.url);
} else {
res.status(200).send(
`<!doctype html>
<html lang="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8" />
<title id="meta-title">${runtimeConfig.META_TITLE}</title>
<meta id="meta-description" name="description" content="${
runtimeConfig.META_DESCRIPTION
}">
<meta id="meta-author" name="author" content="${
runtimeConfig.META_AUTHOR
}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/normalize.css">
<link id="theme" rel="stylesheet" href="css/${theme}">
<link rel="stylesheet" href="css/brands.css">
${cssLinksFromAssets(assets, 'client')}
<link id="favicon" rel="icon" type="image/png" href="${
runtimeConfig.FAVICON_URL
}">
</head>
<body>
<div id="root">${markup}</div>
<script>window.env = ${serialize(runtimeConfig)};</script>
${jsScriptTagsFromAssets(assets, 'client', ' defer crossorigin')}
</body>
</html>`,
);
}
});
export default server;
// ${jsScriptTagsFromAssets(assets, 'client', ' defer crossorigin')}

View File

@ -1,172 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title id="meta-title">LittleLink</title>
<meta id="meta-description" name="description" content="Find us online!">
<meta id="meta-author" name="author" content="Seth Cottle">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/normalize.css">
<link id="theme" rel="stylesheet" href="css/skeleton-light.css">
<link rel="stylesheet" href="css/brands.css">
<link id="favicon" rel="icon" type="image/png" href="images/avatar.png">
</head>
<body>
<div class="container">
<div class="row">
<div class="column" style="margin-top: 10%">
<img id="avatar" class="avatar" src="images/avatar.png" srcset="images/avatar@2x.png 2x"
alt="LittleLink Logo">
<h1 id="name">LittleLink</h1>
<p id="bio">LittleLink is an open source DIY alternative to services like <a href="https://linktr.ee"
target="_blank" rel="noopener">Linktree</a> and <a href="https://many.link" target="_blank"
rel="noopener">many.link</a>. LittleLink was built using <a href="http://www.getskeleton.com"
target="_blank" rel="noopener">Skeleton</a>, a dead simple, responsive boilerplate—weve just
created some
branded buttons and stripped out the things you won't need. 😊</p>
<a id="github" class="button button-github" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/github.svg" alt="GitHub Logo">GitHub</a>
<br>
<a id="twitter" class="button button-twitter" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/twitter.svg" alt="Twitter Logo">Twitter</a>
<br>
<a id="mastodon" class="button button-mastodon" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/mastodon.svg" alt="Mastodon Logo">Mastodon</a>
<br>
<a id="microblog" class="button button-microblog" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/microblog.svg" alt="Micro.blog Logo" />Micro.blog</a>
<br>
<a id="instagram" class="button button-instagram" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/instagram.svg" alt="Instagram Logo">Instagram</a>
<br>
<a id="letterboxd" class="button button-letterboxd" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/letterboxd.svg" alt="Letterboxd Logo">Letterboxd</a>
<br>
<a id="facebook" class="button button-facebook" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/facebook.svg" alt="Facebook Logo">Find us on Facebook</a>
<br>
<a id="facebook-messenger" class="button button-messenger" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/messenger.svg" alt="Facebook Messenger Logo">Chat on Messenger</a>
<br>
<a id="linkedin" class="button button-linkedin" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/linkedin.svg" alt="LinkedIn Logo">LinkedIn</a>
<br>
<a id="youtube" class="button button-youtube" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/youtube.svg" alt="YouTube Logo">YouTube</a>
<br>
<a id="discord" class="button button-discord" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/discord.svg" alt="Discord Logo">Discord</a>
<br>
<a id="twitch" class="button button-twitch" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/twitch.svg" alt="Twitch Logo">Twitch</a>
<br>
<a id="producthunt" class="button button-producthunt" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/producthunt.svg" alt="Product Hunt Logo">Product Hunt</a>
<br>
<a id="snapchat" class="button button-snapchat" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/snapchat.svg" alt="Snapchat Logo">Snapchat</a>
<br>
<a id="spotify" class="button button-spotify" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/spotify.svg" alt="Spotify Logo">Spotify</a>
<br>
<a id="reddit" class="button button-reddit" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/reddit.svg" alt="Reddit Logo">Reddit</a>
<br>
<a id="medium" class="button button-medium" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/medium.svg" alt="Medium Logo">Medium</a>
<br>
<a id="pinterest" class="button button-pinterest" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/pinterest.svg" alt="Pinterest Logo">Follow on Pinterest</a>
<br>
<a id="tiktok" class="button button-tiktok" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/tiktok.svg" alt="TikTok Logo">TikTok</a>
<br>
<a id="email" class="button button-default" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/email.svg" alt="Email Icon">hello@littlelink.io</a>
<br>
<a id="email-alt" class="button button-default" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/email_alt.svg" alt="Email Icon">hello@littlelink.io</a>
<br>
<a id="soundcloud" class="button button-soundcloud" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/soundcloud.svg" alt="SoundCloud Logo">SoundCloud</a>
<br>
<a id="figma" class="button button-figma" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/figma.svg" alt="Figma Logo">Figma</a>
<br>
<a id="kit" class="button button-kit" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/kit.svg" alt="Kit Logo">Kit</a>
<br>
<a id="telegram" class="button button-telegram" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/telegram.svg" alt="Telegram Logo">Telegram</a>
<br>
<a id="tumblr" class="button button-tumblr" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/tumblr.svg" alt="Tumblr Logo">Tumblr</a>
<br>
<a id="steam" class="button button-steam" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/steam.svg" alt="Steam Logo">Steam</a>
<br>
<a id="vimeo" class="button button-vimeo" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/vimeo.svg" alt="Vimeo Logo">Vimeo</a>
<br>
<a id="wordpress" class="button button-wordpress" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/wordpress.svg" alt="Wordpress Logo">Wordpress</a>
<br>
<a id="goodreads" class="button button-goodreads" href="#" target="_blank" rel="noopener"><img
class="icon" src="icons/goodreads.svg" alt="Goodreads Logo">Goodreads</a>
<br>
<a id="skoob" class="button button-skoob" href="#" target="_blank" rel="noopener"><img class="icon"
src="icons/skoob.svg" alt="Skoob Logo">Skoob</a>
<br>
<a id="whatsapp" class="button button-whatsapp" href="$WHATSAPP" target="_blank" rel="noopener"><img
class="icon" src="icons/whatsapp.svg" alt="WhatsApp Logo">WhatsApp</a>
<br>
<br>
<br>
<p id="footer">Build your own by forking <a href="https://littlelink.io" target="_blank"
rel="noopener">LittleLink</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2489" height="2500" viewBox="0 0 1219.547 1225.016"><path fill="#E0E0E0" d="M1041.858 178.02C927.206 63.289 774.753.07 612.325 0 277.617 0 5.232 272.298 5.098 606.991c-.039 106.986 27.915 211.42 81.048 303.476L0 1225.016l321.898-84.406c88.689 48.368 188.547 73.855 290.166 73.896h.258.003c334.654 0 607.08-272.346 607.222-607.023.056-162.208-63.052-314.724-177.689-429.463zm-429.533 933.963h-.197c-90.578-.048-179.402-24.366-256.878-70.339l-18.438-10.93-191.021 50.083 51-186.176-12.013-19.087c-50.525-80.336-77.198-173.175-77.16-268.504.111-278.186 226.507-504.503 504.898-504.503 134.812.056 261.519 52.604 356.814 147.965 95.289 95.36 147.728 222.128 147.688 356.948-.118 278.195-226.522 504.543-504.693 504.543z"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="609.77" y1="1190.114" x2="609.77" y2="21.084"><stop offset="0" stop-color="#20b038"/><stop offset="1" stop-color="#60d66a"/></linearGradient><path fill="url(#a)" d="M27.875 1190.114l82.211-300.18c-50.719-87.852-77.391-187.523-77.359-289.602.133-319.398 260.078-579.25 579.469-579.25 155.016.07 300.508 60.398 409.898 169.891 109.414 109.492 169.633 255.031 169.57 409.812-.133 319.406-260.094 579.281-579.445 579.281-.023 0 .016 0 0 0h-.258c-96.977-.031-192.266-24.375-276.898-70.5l-307.188 80.548z"/><image overflow="visible" opacity=".08" width="682" height="639" xlink:href="FCC0802E2AF8A915.png" transform="translate(270.984 291.372)"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#FFF" d="M462.273 349.294c-11.234-24.977-23.062-25.477-33.75-25.914-8.742-.375-18.75-.352-28.742-.352-10 0-26.25 3.758-39.992 18.766-13.75 15.008-52.5 51.289-52.5 125.078 0 73.797 53.75 145.102 61.242 155.117 7.5 10 103.758 166.266 256.203 226.383 126.695 49.961 152.477 40.023 179.977 37.523s88.734-36.273 101.234-71.297c12.5-35.016 12.5-65.031 8.75-71.305-3.75-6.25-13.75-10-28.75-17.5s-88.734-43.789-102.484-48.789-23.75-7.5-33.75 7.516c-10 15-38.727 48.773-47.477 58.773-8.75 10.023-17.5 11.273-32.5 3.773-15-7.523-63.305-23.344-120.609-74.438-44.586-39.75-74.688-88.844-83.438-103.859-8.75-15-.938-23.125 6.586-30.602 6.734-6.719 15-17.508 22.5-26.266 7.484-8.758 9.984-15.008 14.984-25.008 5-10.016 2.5-18.773-1.25-26.273s-32.898-81.67-46.234-111.326z"/><path fill="#FFF" d="M1036.898 176.091C923.562 62.677 772.859.185 612.297.114 281.43.114 12.172 269.286 12.039 600.137 12 705.896 39.633 809.13 92.156 900.13L7 1211.067l318.203-83.438c87.672 47.812 186.383 73.008 286.836 73.047h.255.003c330.812 0 600.109-269.219 600.25-600.055.055-160.343-62.328-311.108-175.649-424.53zm-424.601 923.242h-.195c-89.539-.047-177.344-24.086-253.93-69.531l-18.227-10.805-188.828 49.508 50.414-184.039-11.875-18.867c-49.945-79.414-76.312-171.188-76.273-265.422.109-274.992 223.906-498.711 499.102-498.711 133.266.055 258.516 52 352.719 146.266 94.195 94.266 146.031 219.578 145.992 352.852-.118 274.999-223.923 498.749-498.899 498.749z"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,165 +0,0 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title id="meta-title">$META_TITLE</title>
<meta id="meta-description" name="description" content="$META_DESCRIPTION">
<meta id="meta-author" name="author" content="$META_AUTHOR">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/normalize.css">
<link id="theme" rel="stylesheet" href="css/skeleton-light.css">
<link rel="stylesheet" href="css/brands.css">
<link id="favicon" rel="icon" type="image/png" href="$FAVICON_URL">
</head>
<body>
<div class="container">
<div class="row">
<div class="column" style="margin-top: 10%">
<img id="avatar" class="avatar" src="$AVATAR_URL" srcset="$AVATAR_2X_URL 2x" alt="$AVATAR_ALT">
<h1 id="name">$NAME</h1>
<p id="bio">$BIO</p>
<a id="github" class="button button-github" href="$GITHUB" target="_blank" rel="noopener"><img class="icon"
src="icons/github.svg" alt="GitHub Logo">GitHub</a>
<br>
<a id="twitter" class="button button-twitter" href="$TWITTER" target="_blank" rel="noopener"><img class="icon"
src="icons/twitter.svg" alt="Twitter Logo">Twitter</a>
<br>
<a id="mastodon" class="button button-mastodon" href="$MASTODON" target="_blank" rel="noopener"><img
class="icon" src="icons/mastodon.svg" alt="Mastodon Logo">Mastodon</a>
<br>
<a id="microblog" class="button button-microblog" href="$MICRO_BLOG" target="_blank" rel="noopener"><img
class="icon" src="icons/microblog.svg" alt="Micro.blog Logo">Micro.blog</a>
<br>
<a id="instagram" class="button button-instagram" href="$INSTAGRAM" target="_blank" rel="noopener"><img
class="icon" src="icons/instagram.svg" alt="Instagram Logo">Instagram</a>
<br>
<a id="letterboxd" class="button button-letterboxd" href="$LETTERBOXD" target="_blank" rel="noopener"><img
class="icon" src="icons/letterboxd.svg" alt="Letterboxd Logo">Letterboxd</a>
<br>
<a id="facebook" class="button button-facebook" href="$FACEBOOK" target="_blank" rel="noopener"><img
class="icon" src="icons/facebook.svg" alt="Facebook Logo">Find us on Facebook</a>
<br>
<a id="facebook-messenger" class="button button-messenger" href="$FACEBOOK_MESSENGER" target="_blank"
rel="noopener"><img class="icon" src="icons/messenger.svg" alt="Facebook Messenger Logo">Chat on Messenger</a>
<br>
<a id="linkedin" class="button button-linkedin" href="$LINKED_IN" target="_blank" rel="noopener"><img
class="icon" src="icons/linkedin.svg" alt="LinkedIn Logo">LinkedIn</a>
<br>
<a id="youtube" class="button button-youtube" href="$YOUTUBE" target="_blank" rel="noopener"><img class="icon"
src="icons/youtube.svg" alt="YouTube Logo">YouTube</a>
<br>
<a id="discord" class="button button-discord" href="$DISCORD" target="_blank" rel="noopener"><img class="icon"
src="icons/discord.svg" alt="Discord Logo">Discord</a>
<br>
<a id="twitch" class="button button-twitch" href="$TWITCH" target="_blank" rel="noopener"><img class="icon"
src="icons/twitch.svg" alt="Twitch Logo">Twitch</a>
<br>
<a id="producthunt" class="button button-producthunt" href="$PRODUCT_HUNT" target="_blank" rel="noopener"><img
class="icon" src="icons/producthunt.svg" alt="Product Hunt Logo">Product Hunt</a>
<br>
<a id="snapchat" class="button button-snapchat" href="$SNAPCHAT" target="_blank" rel="noopener"><img
class="icon" src="icons/snapchat.svg" alt="Snapchat Logo">Snapchat</a>
<br>
<a id="spotify" class="button button-spotify" href="$SPOTIFY" target="_blank" rel="noopener"><img class="icon"
src="icons/spotify.svg" alt="Spotify Logo">Spotify</a>
<br>
<a id="reddit" class="button button-reddit" href="$REDDIT" target="_blank" rel="noopener"><img class="icon"
src="icons/reddit.svg" alt="Reddit Logo">Reddit</a>
<br>
<a id="medium" class="button button-medium" href="$MEDIUM" target="_blank" rel="noopener"><img class="icon"
src="icons/medium.svg" alt="Medium Logo">Medium</a>
<br>
<a id="pinterest" class="button button-pinterest" href="$PINTEREST" target="_blank" rel="noopener"><img
class="icon" src="icons/pinterest.svg" alt="Pinterest Logo">Follow on Pinterest</a>
<br>
<a id="tiktok" class="button button-tiktok" href="$TIKTOK" target="_blank" rel="noopener"><img class="icon"
src="icons/tiktok.svg" alt="TikTok Logo">TikTok</a>
<br>
<a id="email" class="button button-default" href="mailto:$EMAIL" target="_blank" rel="noopener">$EMAIL_TEXT</a>
<br>
<a id="email-alt" class="button button-default" href="mailto:$EMAIL_ALT" target="_blank"
rel="noopener">$EMAIL_ALT_TEXT</a>
<br>
<a id="soundcloud" class="button button-soundcloud" href="$SOUND_CLOUD" target="_blank" rel="noopener"><img
class="icon" src="icons/soundcloud.svg" alt="SoundCloud Logo">SoundCloud</a>
<br>
<a id="figma" class="button button-figma" href="$FIGMA" target="_blank" rel="noopener"><img class="icon"
src="icons/figma.svg" alt="Figma Logo">Figma</a>
<br>
<a id="kit" class="button button-kit" href="$KIT" target="_blank" rel="noopener"><img class="icon"
src="icons/kit.svg" alt="Kit Logo">Kit</a>
<br>
<a id="telegram" class="button button-telegram" href="$TELEGRAM" target="_blank" rel="noopener"><img
class="icon" src="icons/telegram.svg" alt="Telegram Logo">Telegram</a>
<br>
<a id="tumblr" class="button button-tumblr" href="$TUMBLR" target="_blank" rel="noopener"><img class="icon"
src="icons/tumblr.svg" alt="Tumblr Logo">Tumblr</a>
<br>
<a id="steam" class="button button-steam" href="$STEAM" target="_blank" rel="noopener"><img class="icon"
src="icons/steam.svg" alt="Steam Logo">Steam</a>
<br>
<a id="vimeo" class="button button-vimeo" href="$VIMEO" target="_blank" rel="noopener"><img class="icon"
src="icons/vimeo.svg" alt="Vimeo Logo">Vimeo</a>
<br>
<a id="wordpress" class="button button-wordpress" href="$WORDPRESS" target="_blank" rel="noopener"><img
class="icon" src="icons/wordpress.svg" alt="Wordpress Logo">Wordpress</a>
<br>
<a id="goodreads" class="button button-goodreads" href="$GOODREADS" target="_blank" rel="noopener"><img
class="icon" src="icons/goodreads.svg" alt="Goodreads Logo">Goodreads</a>
<br>
<a id="skoob" class="button button-skoob" href="$SKOOB" target="_blank" rel="noopener"><img class="icon"
src="icons/skoob.svg" alt="Skoob Logo">Skoob</a>
<br>
<a id="whatsapp" class="button button-whatsapp" href="$WHATSAPP" target="_blank" rel="noopener"><img
class="icon" src="icons/whatsapp.svg" alt="WhatsApp Logo">WhatsApp</a>
<br>
<br>
<br>
<p id="footer">$FOOTER</p>
</div>
</div>
</div>
</body>
</html>

9373
yarn.lock

File diff suppressed because it is too large Load Diff