gatsby → remix

This commit is contained in:
Wes Bos 2023-02-20 15:41:29 -05:00
parent 6042ed7028
commit 1f461608ba
27 changed files with 8293 additions and 35523 deletions

2
.gitignore vendored
View file

@ -1,3 +1,4 @@
build/
# Logs # Logs
logs logs
*.log *.log
@ -54,7 +55,6 @@ typings/
# dotenv environment variable files # dotenv environment variable files
.env* .env*
# gatsby files
.cache/ .cache/
public public

View file

@ -1,33 +0,0 @@
module.exports = {
siteMetadata: {
title: `/uses`,
description: `A list of /uses pages detailing developer setups.`,
author: `@wesbos`,
siteUrl: 'https://uses.tech',
},
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
},
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `gatsby-starter-default`,
short_name: `starter`,
start_url: `/`,
background_color: `#663399`,
theme_color: `#663399`,
display: `minimal-ui`,
icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
},
},
`gatsby-plugin-react-helmet`,
`gatsby-plugin-styled-components`,
],
};

View file

@ -1,91 +0,0 @@
const { tags, countries, devices, normalizeTag } = require('./src/util/stats');
const people = require('./src/data.js');
function unique(arr) {
return Array.from(new Set(arr));
}
function sourceNodes({ actions, createNodeId, createContentDigest }) {
const normalizedTagMap = tags().reduce((acc, tag) => {
const normalizedTag = normalizeTag(tag.name);
acc[normalizedTag] = tag.name;
return acc;
}, {});
// Add People to the GraphQL API, we randomize the data on each build so no one gets their feelings hurt
people
.sort(() => Math.random() - 0.5)
.forEach((person) => {
const normalizedPerson = {
...person,
// Clean out people that added basically the same tags twice
tags: unique(
person.tags.map((tag) => normalizedTagMap[normalizeTag(tag)] || tag)
),
};
const nodeMeta = {
id: createNodeId(`person-${normalizedPerson.name}`),
parent: null,
children: [],
internal: {
type: `Person`,
mediaType: `text/html`,
content: JSON.stringify(normalizedPerson),
contentDigest: createContentDigest(normalizedPerson),
},
};
actions.createNode({ ...normalizedPerson, ...nodeMeta });
});
// Add tags to GraphQL API
tags().forEach((tag) => {
const nodeMeta = {
id: createNodeId(`tag-${tag.name}`),
parent: null,
children: [],
internal: {
type: `Tag`,
mediaType: `text/html`,
content: JSON.stringify(tag),
contentDigest: createContentDigest(tag),
},
};
actions.createNode({ ...tag, ...nodeMeta });
});
// Add Countries to GraphQL API
countries().forEach((country) => {
const nodeMeta = {
id: createNodeId(`country-${country.name}`),
parent: null,
children: [],
internal: {
type: `Country`,
mediaType: `text/html`,
content: JSON.stringify(country),
contentDigest: createContentDigest(country),
},
};
actions.createNode({ ...country, ...nodeMeta });
});
// Add Devices to GraphQL API
devices().forEach((device) => {
const nodeMeta = {
id: createNodeId(`device-${device.name}`),
parent: null,
children: [],
internal: {
type: `device`,
mediaType: `text/html`,
content: JSON.stringify(device),
contentDigest: createContentDigest(device),
},
};
actions.createNode({ ...device, ...nodeMeta });
});
}
exports.sourceNodes = sourceNodes;

View file

@ -1 +0,0 @@
export { wrapRootElement } from './gatsby-browser';

18
netlify.toml Normal file
View file

@ -0,0 +1,18 @@
[build]
command = "remix build"
functions = "netlify/functions"
publish = "public"
[dev]
command = "remix watch"
port = 3000
[[redirects]]
from = "/*"
to = "/.netlify/functions/server"
status = 200
[[headers]]
for = "/build/*"
[headers.values]
"Cache-Control" = "public, max-age=31536000, s-maxage=31536000"

34825
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,64 +5,53 @@
"author": "Wes Bos", "author": "Wes Bos",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"wesbos" "wesbos/typescript"
] ]
}, },
"engines": { "engines": {
"node": ">= 12" "node": ">= 16"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.2.1", "@actions/core": "^1.10.0",
"@actions/exec": "^1.0.3", "@actions/exec": "^1.1.1",
"@actions/github": "^2.0.1", "@actions/github": "^5.1.1",
"@babel/core": "^7.16.7", "@babel/core": "^7.21.0",
"@babel/eslint-parser": "^7.16.5", "@babel/preset-react": "^7.18.6",
"@babel/preset-react": "^7.16.7", "@netlify/functions": "^1.4.0",
"@types/node": "^16.11.19", "@remix-run/dev": "^1.13.0",
"@typescript-eslint/eslint-plugin": "^5.9.1", "@remix-run/netlify": "^1.13.0",
"@typescript-eslint/parser": "^5.9.1", "@remix-run/node": "^1.13.0",
"@remix-run/react": "^1.13.0",
"@remix-run/serve": "^1.13.0",
"@remix-run/server-runtime": "^1.13.0",
"@types/node": "^18.14.0",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"country-emoji": "^1.5.6", "country-emoji": "^1.5.6",
"eslint-config-airbnb-typescript": "^16.1.0", "joi": "^17.8.1",
"gatsby": "^4.5.0",
"gatsby-image": "^3.11.0",
"gatsby-plugin-manifest": "^4.5.0",
"gatsby-plugin-offline": "^5.5.0",
"gatsby-plugin-react-helmet": "^5.5.0",
"gatsby-plugin-sharp": "^4.5.0",
"gatsby-plugin-styled-components": "^5.5.0",
"gatsby-plugin-web-font-loader": "^1.0.4",
"gatsby-source-filesystem": "^4.5.0",
"gatsby-transformer-sharp": "^4.5.0",
"joi": "^17.5.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"styled-components": "5.3.3", "react-is": "^18.2.0",
"typescript": "^4.5.4" "styled-components": "5.3.6",
"typescript": "^4.9.5"
}, },
"scripts": { "scripts": {
"build": "gatsby build", "build": "remix build",
"develop": "gatsby develop", "dev": "NODE_ENV=development remix dev",
"start": "npm run develop", "start": "remix start"
"serve": "gatsby serve",
"clean": "gatsby clean"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.6.0", "@types/styled-components": "^5.1.26",
"eslint-config-airbnb": "^19.0.4", "eslint": "^8.34.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-wesbos": "^3.2.3",
"eslint-config-wesbos": "^3.0.2", "husky": "^8.0.3",
"eslint-plugin-html": "^6.2.0", "lint-staged": "^13.1.2",
"eslint-plugin-import": "^2.25.4", "postcss": "^8.4.21",
"eslint-plugin-jsx-a11y": "^6.5.1", "postcss-nesting": "^11.2.1",
"eslint-plugin-prettier": "^4.0.0", "prettier": "^2.8.4"
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^4.0.10",
"lint-staged": "^9.5.0",
"prettier": "^2.5.1"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

7607
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
const postcssNesting = require("postcss-nesting");
module.exports = {
plugins: [postcssNesting()],
};

8
remix.config.js Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('@remix-run/dev').AppConfig} */
const postcssNesting = require("postcss-nesting");
module.exports = {
appDirectory: "src",
future: {
unstable_postcss: true,
},
};

47
server.js Normal file
View file

@ -0,0 +1,47 @@
import { createRequestHandler } from "@remix-run/netlify";
import * as build from "@remix-run/dev/server-build";
/*
* Returns a context object with at most 3 keys:
* - `netlifyGraphToken`: raw authentication token to use with Netlify Graph
* - `clientNetlifyGraphAccessToken`: For use with JWTs generated by
* `netlify-graph-auth`.
* - `netlifyGraphSignature`: a signature for subscription events. Will be
* present if a secret is set.
*/
function getLoadContext(event, context) {
let rawAuthorizationString;
let netlifyGraphToken;
if (event.authlifyToken != null) {
netlifyGraphToken = event.authlifyToken;
}
const authHeader = event.headers["authorization"];
const graphSignatureHeader = event.headers["x-netlify-graph-signature"];
if (authHeader != null && /Bearer /gi.test(authHeader)) {
rawAuthorizationString = authHeader.split(" ")[1];
}
const loadContext = {
clientNetlifyGraphAccessToken: rawAuthorizationString,
netlifyGraphToken: netlifyGraphToken,
netlifyGraphSignature: graphSignatureHeader,
};
// Remove keys with undefined values
Object.keys(loadContext).forEach((key) => {
if (loadContext[key] == null) {
delete loadContext[key];
}
});
return loadContext;
}
export const handler = createRequestHandler({
build,
getLoadContext,
mode: process.env.NODE_ENV,
});

View file

@ -1,28 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
const BackToTopLink = styled.a`
position: fixed;
bottom: 1%;
right: 1%;
background: var(--pink);
color: white;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
border-radius: 3px;
padding: 1rem;
transition: opacity 0.2s;
opacity: 0;
text-decoration: none;
${props =>
props.percent > 0.25 &&
`
opacity: 1;
`}
@media screen and (max-width: 500px) {
display: none;
}
`;
function useScrollPosition() { function useScrollPosition() {
const [percent, setPercent] = useState(0); const [percent, setPercent] = useState(0);
@ -49,8 +25,8 @@ function useScrollPosition() {
export default function BackToTop() { export default function BackToTop() {
const percent = useScrollPosition(); const percent = useScrollPosition();
return ( return (
<BackToTopLink href="#top" title="Back To Top" percent={percent}> <a className="BackToTopLink" href="#top" title="Back To Top" percent={percent}>
&uarr; &uarr;
</BackToTopLink> </a>
); );
} }

View file

@ -1,37 +1,19 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { name } from 'country-emoji'; import { name } from 'country-emoji';
import styled from 'styled-components';
import { Tag, Tags } from './Topics';
import * as icons from '../util/icons'; import * as icons from '../util/icons';
import { useParams } from '@remix-run/react';
function useIntersectionObserver(ref) { export default function Person({ person }) {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(function() {
const observer = new IntersectionObserver(function([entry]) {
console.log('Run once for every time its on screen');
console.log(entry);
});
// Observe the element we want to observve
observer.observe(ref.current);
return () => {
observer.unobserve(ref.current);
};
});
}
export default function Person({ person, currentTag }) {
const url = new URL(person.url); const url = new URL(person.url);
const twitter = person.twitter ? `https://unavatar.io/${person.twitter.replace('@', '')}` : null; const twitter = person.twitter ? `https://unavatar.io/${person.twitter.replace('@', '')}` : null;
const website = `https://unavatar.io/${url.host}`; const website = `https://unavatar.io/${url.host}`;
const unavatar = person.twitter ? `${twitter}?fallback=${website}` : website; const unavatar = person.twitter ? `${twitter}?fallback=${website}` : website;
const img = `https://images.weserv.nl/?url=${unavatar}&w=100&l=9&af&il&n=-1`; const img = `https://images.weserv.nl/?url=${unavatar}&w=100&l=9&af&il&n=-1`;
const { tag: currentTag } = useParams();
return ( return (
<PersonWrapper> <div className="PersonWrapper">
<PersonInner> <div className="PersonInner">
<header> <header>
<img <img
width="50" width="50"
@ -57,15 +39,15 @@ export default function Person({ person, currentTag }) {
</a> </a>
</header> </header>
<p>{person.description}</p> <p>{person.description}</p>
<Tags> <ul className="Tags">
{person.tags.map(tag => ( {person.tags.map(tag => (
<Tag key={tag} as="li" currentTag={tag === currentTag} small> <li className={`Tag small ${tag === currentTag ? 'currentTag' : ''}`} key={tag}>
{tag} {tag}
</Tag> </li>
))} ))}
</Tags> </ul>
</PersonInner> </div>
<PersonDeets> <div className="PersonDeets">
<span className="country" title={name(person.country)}> <span className="country" title={name(person.country)}>
{person.country} {person.country}
</span> </span>
@ -85,7 +67,7 @@ export default function Person({ person, currentTag }) {
)} )}
{person.twitter && ( {person.twitter && (
<TwitterHandle> <div className="TwitterHandle">
<a <a
href={`https://twitter.com/${person.twitter.replace('@', '')}`} href={`https://twitter.com/${person.twitter.replace('@', '')}`}
target="_blank" target="_blank"
@ -94,15 +76,14 @@ export default function Person({ person, currentTag }) {
<span className="at">@</span> <span className="at">@</span>
{person.twitter.replace('@', '')} {person.twitter.replace('@', '')}
</a> </a>
</TwitterHandle> </div>
)} )}
</PersonDeets> </div>
</PersonWrapper> </div>
); );
} }
Person.propTypes = { Person.propTypes = {
currentTag: PropTypes.string,
person: PropTypes.shape({ person: PropTypes.shape({
github: PropTypes.string, github: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
@ -112,7 +93,7 @@ Person.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string), tags: PropTypes.arrayOf(PropTypes.string),
country: PropTypes.string, country: PropTypes.string,
computer: PropTypes.oneOf(['apple', 'windows', 'linux']), computer: PropTypes.oneOf(['apple', 'windows', 'linux']),
phone: PropTypes.oneOf(['iphone', 'android', 'windowsphone']), phone: PropTypes.oneOf(['iphone', 'android', 'windowsphone', 'flipphone']),
twitter(props, propName, componentName) { twitter(props, propName, componentName) {
if (!/^@?(\w){1,15}$/.test(props[propName])) { if (!/^@?(\w){1,15}$/.test(props[propName])) {
return new Error( return new Error(
@ -124,96 +105,3 @@ Person.propTypes = {
}), }),
}; };
// Component Styles
const PersonWrapper = styled.div`
border: 1px solid var(--vape);
border-radius: 5.34334px;
box-shadow: 10px -10px 0 var(--blue2);
display: grid;
grid-template-rows: 1fr auto auto;
`;
const PersonInner = styled.div`
padding: 2rem;
h3 {
margin: 0;
a:visited {
color: var(--purple);
}
}
header {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: auto 1fr;
grid-gap: 0 1rem;
@media all and (max-width: 400px) {
grid-template-columns: 1fr;
}
img {
grid-row: 1 / -1;
background: var(--lightblue);
font-size: 1rem;
}
.displayLink {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
color: var(--vape);
letter-spacing: 1px;
font-size: 1.2rem;
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
:hover,
:visited {
color: var(--pink);
}
}
}
`;
const PersonDeets = styled.div`
display: flex;
border-top: 1px solid var(--vape);
> * {
flex: 1;
border-left: 1px solid var(--vape);
text-align: center;
padding: 1rem;
display: grid;
align-items: center;
justify-content: center;
grid-template-columns: auto auto;
&:first-child {
border-left: 0;
}
}
a {
color: var(--vape);
}
.country {
font-size: 3rem;
padding-top: 2rem;
}
.phone {
padding: 0;
}
@media all and (max-width: 400px) {
display: grid;
grid-template-columns: 1fr 1fr;
> *:nth-child(1),
> *:nth-child(2) {
/* lol */
border-bottom: 1px solid var(--vape);
}
}
`;
const TwitterHandle = styled.span`
font-size: 1.24323423426928098420394802rem;
.at {
color: var(--yellow);
margin-right: 2px;
}
`;

View file

@ -1,138 +1,52 @@
import React, { useCallback, useContext } from 'react'; import { Link, useParams, useRouteLoaderData } from '@remix-run/react';
import styled from 'styled-components';
import FilterContext from '../context/FilterContext';
import * as icons from '../util/icons'; import * as icons from '../util/icons';
export default function Topics() { export default function Topics() {
const { countries, tags, devices, currentTag, setCurrentTag } = useContext( const { tags, countries, devices } = useRouteLoaderData("root");
FilterContext const params = useParams();
); const currentTag = params.tag || 'all';
const handleKeyDown = useCallback(
tagName => e => {
if (e.keyCode === 13) {
setCurrentTag(tagName);
}
},
[setCurrentTag]
);
return ( return (
<Tags> <div className="Tags">
{tags.map(tag => ( {tags.map((tag) => (
<Tag <Link
currentTag={tag.name === currentTag} prefetch="intent"
htmlFor={`filter-${tag.name}`} key={`tag-${tag.name}`}
key={`filter-${tag.name}`} to={
clickable tag.name === "all" ? "/" : `/like/${encodeURIComponent(tag.name)}`
onKeyDown={handleKeyDown(tag.name)} }
tabIndex="0" className={`Tag ${currentTag === tag.name ? "currentTag" : ""}`}
> >
<input
type="radio"
name="tag"
id={`filter-${tag.name}`}
value={tag.name}
checked={tag.name === currentTag}
onChange={e => setCurrentTag(e.currentTarget.value)}
/>
{tag.name} {tag.name}
<TagCount>{tag.count}</TagCount> <span className="TagCount">{tag.count}</span>
</Tag> </Link>
))} ))}
{countries.map(tag => ( {countries.map((tag) => (
<Tag <Link
currentTag={tag.emoji === currentTag} to={`/like/${tag.emoji}`}
htmlFor={`filter-${tag.name}`} prefetch="intent"
className={`Tag ${currentTag === tag.emoji ? "currentTag" : ""}`}
key={`filter-${tag.name}`} key={`filter-${tag.name}`}
title={tag.name} title={tag.name}
clickable
onKeyDown={handleKeyDown(tag.name)}
tabIndex="0"
> >
<input <span className="TagEmoji">{tag.emoji}</span>
type="radio" <span className="TagCount">{tag.count}</span>
name="tag" </Link>
id={`filter-${tag.name}`}
value={tag.emoji}
checked={tag.emoji === currentTag}
onChange={e => setCurrentTag(e.currentTarget.value)}
/>
<TagEmoji>{tag.emoji}</TagEmoji>
<TagCount>{tag.count}</TagCount>
</Tag>
))} ))}
{devices.map(tag => ( {devices.map((tag) => (
<Tag <Link
currentTag={tag.name === currentTag} to={`/like/${tag.name}`}
htmlFor={`filter-${tag.name}`} className={`Tag ${currentTag === tag.name ? "currentTag" : ""}`}
prefetch="intent"
key={`filter-${tag.name}`} key={`filter-${tag.name}`}
title={tag.name} title={tag.name}
clickable
onKeyDown={handleKeyDown(tag.name)}
tabIndex="0"
> >
<input
type="radio"
name="computer"
id={`filter-${tag.name}`}
value={tag.name}
checked={tag.name === currentTag}
onChange={e => setCurrentTag(e.currentTarget.value)}
/>
<img height="20px" src={icons[tag.name]} alt={tag.name} /> <img height="20px" src={icons[tag.name]} alt={tag.name} />
<TagCount>{tag.count}</TagCount> <span className="TagCount">{tag.count}</span>
</Tag> </Link>
))} ))}
</Tags> </div>
); );
} }
// Component Styles
const Tags = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
`;
const Tag = styled.label`
background: var(--pink);
margin: 2px;
border-radius: 3px;
font-size: ${props => (props.small ? `1.2rem;` : `1.7rem;`)};
padding: 5px;
color: hsla(0, 100%, 100%, 0.8);
transition: background-color 0.2s;
cursor: ${props => (props.clickable ? 'pointer' : 'default')};
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
input {
display: none;
}
${props =>
props.currentTag &&
`
background: var(--yellow);
color: hsla(0, 100%, 0%, 0.8);
`}
`;
const TagEmoji = styled.span`
transform: scale(1.45);
`;
const TagCount = styled.span`
background: var(--blue);
font-size: 1rem;
color: white;
padding: 2px;
border-radius: 2px;
margin-left: 5px;
`;
export { Tag, Tags };

View file

@ -1,35 +1,23 @@
import React from 'react'; import React from 'react';
import { Link } from 'gatsby';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import styled from 'styled-components';
import FavIcon from './FavIcon'; import FavIcon from './FavIcon';
function Header({ siteTitle, siteDescription, siteUrl }) { function Header({ siteTitle, siteDescription, siteUrl }) {
return ( return (
<HeaderWrapper className="header"> <div className="header HeaderWrapper">
<FavIcon /> <FavIcon />
<Helmet>
<html lang="en" amp />
<title>{siteTitle}</title>
<meta name="description" content={siteDescription} />
<link rel="canonical" href={siteUrl} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@wesbos" />
<meta name="twitter:title" content={siteTitle} />
<meta name="twitter:description" content={siteDescription} />
<meta name="twitter:image" content={`${siteUrl}/twitter-card.png`} />
</Helmet>
<div> <div>
<h1 id="top"> <h1 id="top">
<Link to="/">/uses</Link> <a href="/">/uses</a>
</h1> </h1>
<p> <p>
A list of <code>/uses</code> pages detailing developer setups, gear, A list of <code>/uses</code> pages detailing developer setups, gear,
software and configs. software and configs.
</p> </p>
</div> </div>
</HeaderWrapper> </div>
); );
} }
Header.propTypes = { Header.propTypes = {
@ -45,11 +33,3 @@ Header.defaultProps = {
}; };
export default Header; export default Header;
// Component Styles
const HeaderWrapper = styled.header`
text-align: center;
h1 {
font-size: 6rem;
}
`;

View file

@ -1,47 +1,19 @@
/**
* Layout component that queries for data
* with Gatsby's useStaticQuery component
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useStaticQuery, graphql } from 'gatsby';
import styled, { createGlobalStyle } from 'styled-components';
import Header from './header'; import Header from './header';
import 'normalize.css'; import 'normalize.css';
const Layout = ({ children }) => { export default function Layout({ children }) {
const data = useStaticQuery(graphql`
query SiteTitleQuery {
site {
siteMetadata {
title
description
siteUrl
}
}
}
`);
return ( return (
<> <main className="Main">
<GlobalStyle /> <Header />
<Main>
<Header
siteTitle={data.site.siteMetadata.title}
siteDescription={data.site.siteMetadata.description}
siteUrl={data.site.siteMetadata.siteUrl}
/>
{children} {children}
<footer> <footer>
<center ya-i-used-a-center-tag="sue me"> <center ya-i-used-a-center-tag="sue me">
<p> <p>
Made by <a href="https://wesbos.com">Wes Bos</a> with{' '} Made by <a href="https://wesbos.com">Wes Bos</a> with{' '}
<a href="https://www.gatsbyjs.org">Gatsby</a> ©{' '} <a href="https://www.remix.run">Remix</a> ©{' '}
{new Date().getFullYear() - Math.floor(Math.random() * 777)} {new Date().getFullYear()}
</p> </p>
<p> <p>
Source on{' '} Source on{' '}
@ -59,76 +31,10 @@ const Layout = ({ children }) => {
</p> </p>
</center> </center>
</footer> </footer>
</Main> </main>
</>
); );
}; };
Layout.propTypes = { Layout.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
}; };
export default Layout;
// Global Styles
const GlobalStyle = createGlobalStyle`
html {
--purple: #b066ff;
--blue: #203447;
--lightblue: #1f4662;
--blue2: #1C2F40;
--yellow: #ffc600;
--pink: #EB4471;
--vape: #d7d7d7;
background: var(--blue);
color: var(--vape);
font-family: 'Fira Mono', monospace;
font-weight: 100;
font-size: 10px;
scroll-behavior: smooth;
}
body {
font-size: 2rem;
overflow-y: scroll;
}
h1,h2,h3,h4,h5,h6 {
font-weight: 500;
}
a {
color: var(--yellow);
text-decoration-color: var(--pink);
font-style: italic;
}
code {
background: var(--lightblue);
}
::selection {
background: var(--yellow);
color: var(--blue);
}
body::-webkit-scrollbar {
width: 12px;
}
html {
scrollbar-width: thin;
scrollbar-color: var(--yellow) var(--blue);
}
body::-webkit-scrollbar-track {
background: var(--blue);
}
body::-webkit-scrollbar-thumb {
background-color: var(--yellow) ;
border-radius: 6px;
border: 3px solid var(--blue);
}
`;
// Component Styles
const Main = styled.main`
display: grid;
grid-gap: 3rem;
max-width: 1900px;
padding: 0 3rem;
margin: 5rem auto;
`;

View file

@ -1,53 +0,0 @@
import React, { createContext, useState } from 'react';
import { useStaticQuery, graphql } from 'gatsby';
import PropTypes from 'prop-types';
const FilterContext = createContext();
const FilterProvider = function({ children }) {
const [currentTag, setCurrentTag] = useState('all');
const { allTag, allCountry, allDevice } = useStaticQuery(graphql`
query FilterQuery {
allTag {
nodes {
name
count
}
}
allCountry {
nodes {
count
emoji
name
}
}
allDevice {
nodes {
count
name
}
}
}
`);
return (
<FilterContext.Provider
value={{
tags: allTag.nodes,
countries: allCountry.nodes,
devices: allDevice.nodes,
currentTag,
setCurrentTag,
}}
>
{children}
</FilterContext.Provider>
);
};
FilterProvider.propTypes = {
children: PropTypes.element,
};
export default FilterContext;
export { FilterProvider };

View file

@ -4351,7 +4351,7 @@ module.exports = [
description: 'Experienced Full Stack Software Engineer & Computers Lover', description: 'Experienced Full Stack Software Engineer & Computers Lover',
url: 'https://adelrosarioh.me/uses', url: 'https://adelrosarioh.me/uses',
twitter: '@adelrosarioh', twitter: '@adelrosarioh',
emoji: '💻 💻 💻', emoji: '💻',
country: '🇩🇴', country: '🇩🇴',
computer: 'linux', computer: 'linux',
phone: 'android', phone: 'android',

21
src/entry.client.tsx Normal file
View file

@ -0,0 +1,21 @@
import { RemixBrowser } from '@remix-run/react';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
const hydrate = () => {
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
};
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1);
}

48
src/entry.server.tsx Normal file
View file

@ -0,0 +1,48 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { renderToPipeableStream } from 'react-dom/server';
const ABORT_DELAY = 5000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let didError = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer context={remixContext} url={request.url} />,
{
onShellReady: () => {
const body = new PassThrough();
responseHeaders.set('Content-Type', 'text/html');
resolve(
new Response(body, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
})
);
pipe(body);
},
onShellError: (err: unknown) => {
reject(err);
},
onError: (error: unknown) => {
didError = true;
console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}

View file

@ -1,62 +0,0 @@
import React, { useContext } from 'react';
import { useStaticQuery, graphql } from 'gatsby';
import styled from 'styled-components';
import FilterContext from '../context/FilterContext';
import Layout from '../components/layout';
import Person from '../components/Person';
import Topics from '../components/Topics';
import BackToTop from '../components/BackToTop';
function IndexPage() {
const { currentTag } = useContext(FilterContext);
const { allPerson } = useStaticQuery(graphql`
query People {
allPerson {
nodes {
computer
country
description
emoji
id
name
phone
tags
twitter
url
}
}
}
`);
const people = allPerson.nodes.filter(
person =>
currentTag === 'all' ||
person.tags.includes(currentTag) ||
currentTag === person.country ||
currentTag === person.computer ||
currentTag === person.phone
);
return (
<Layout>
<Topics />
<People>
{people.map(person => (
<Person key={person.name} person={person} currentTag={currentTag} />
))}
</People>
<BackToTop />
</Layout>
);
}
export default IndexPage;
// Component Styles
const People = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
grid-gap: 5rem;
@media all and (max-width: 400px) {
grid-template-columns: 1fr;
}
`;

65
src/root.tsx Normal file
View file

@ -0,0 +1,65 @@
import type { LinksFunction, LoaderArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import Layout from './components/layout'
import styles from './styles.css';
import { countries, devices, tags } from './util/stats';
export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: styles },
];
export function loader() {
return {
tags: tags(),
countries: countries(),
devices: devices(),
}
}
const metaData = {
description: `A list of /uses pages detailing developer setups.`,
siteUrl: 'https://uses.tech',
author: `@wesbos`,
title: '/uses',
}
export const meta: MetaFunction = () => ({
charset: 'utf-8',
title: '/uses',
viewport: 'width=device-width,initial-scale=1',
});
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<link rel="icon" href="https://fav.farm/🖥" />
<meta name="description" content={metaData.description} />
<link rel="canonical" href={metaData.siteUrl} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@wesbos" />
<meta name="twitter:title" content={metaData.title} />
<meta name="twitter:description" content={metaData.description} />
<meta name="twitter:image" content={`${metaData.siteUrl}/twitter-card.png`} />
<Links />
</head>
<body>
<Layout>
<Outlet />
{/* <ScrollRestoration /> */}
<Scripts />
<LiveReload />
</Layout>
</body>
</html>
);
}

27
src/routes/index.tsx Normal file
View file

@ -0,0 +1,27 @@
import { useLoaderData, useParams } from '@remix-run/react';
import { json, LoaderArgs } from '@remix-run/server-runtime';
import React, { useContext } from 'react';
import Topics from '../components/Topics';
import BackToTop from '../components/BackToTop';
import Person from '../components/Person';
import { getPeople } from 'src/util/stats';
export async function loader({ params }: LoaderArgs) {
const people = getPeople(params.tag);
return {people};
}
export default function Index() {
const { people } = useLoaderData();
return (
<>
<Topics />
<div className="People">
{people.map(person => (
<Person key={person.name} person={person} />
))}
</div>
<BackToTop />
</>
);
}

1
src/routes/like/$tag.tsx Normal file
View file

@ -0,0 +1 @@
export { default, loader } from '../index';

280
src/styles.css Normal file
View file

@ -0,0 +1,280 @@
/* Global Styles */
:root {
--purple: #b066ff;
--blue: #203447;
--lightblue: #1f4662;
--blue2: #1C2F40;
--yellow: #ffc600;
--pink: #EB4471;
--vape: #d7d7d7;
background: var(--blue);
color: var(--vape);
font-family: 'Fira Mono', monospace;
font-weight: 100;
font-size: 10px;
scroll-behavior: smooth;
}
body {
font-size: 2rem;
overflow-y: scroll;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 500;
}
a {
color: var(--yellow);
text-decoration-color: var(--pink);
font-style: italic;
}
code {
background: var(--lightblue);
}
::selection {
background: var(--yellow);
color: var(--blue);
}
body::-webkit-scrollbar {
width: 12px;
}
html {
scrollbar-width: thin;
scrollbar-color: var(--yellow) var(--blue);
}
body::-webkit-scrollbar-track {
background: var(--blue);
}
body::-webkit-scrollbar-thumb {
background-color: var(--yellow);
border-radius: 6px;
border: 3px solid var(--blue);
}
.PersonWrapper {
border: 1px solid var(--vape);
border-radius: 5.34334px;
box-shadow: 10px -10px 0 var(--blue2);
display: grid;
grid-template-rows: 1fr auto auto;
}
.PersonInner {
padding: 2rem;
& h3 {
margin: 0;
& a:visited {
color: var(--purple);
}
}
& header {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: auto 1fr;
grid-gap: 0 1rem;
@media all and (max-width: 400px) {
grid-template-columns: 1fr;
}
& img {
grid-row: 1 / -1;
background: var(--lightblue);
font-size: 1rem;
}
& .displayLink {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
color: var(--vape);
letter-spacing: 1px;
font-size: 1.2rem;
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
&:hover,
&:visited {
color: var(--pink);
}
}
}
}
.PersonDeets {
display: flex;
border-top: 1px solid var(--vape);
>* {
flex: 1;
border-left: 1px solid var(--vape);
text-align: center;
padding: 1rem;
display: grid;
align-items: center;
justify-content: center;
grid-template-columns: auto auto;
&:first-child {
border-left: 0;
}
}
& a {
color: var(--vape);
}
& .country {
font-size: 3rem;
padding-top: 2rem;
}
& .phone {
padding: 0;
}
@media all and (max-width: 400px) {
display: grid;
grid-template-columns: 1fr 1fr;
>*:nth-child(1),
>*:nth-child(2) {
/* lol */
border-bottom: 1px solid var(--vape);
}
}
}
.TwitterHandle {
font-size: 1.24323423426928098420394802rem;
& .at {
color: var(--yellow);
margin-right: 2px;
}
}
.Tags {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
}
.Tag {
background: var(--pink);
margin: 2px;
border-radius: 3px;
font-size: 1.7rem;
text-decoration: none;
&.small {
font-size: 1.2rem;
}
padding: 5px;
color: hsla(0, 100%, 100%, 0.8);
transition: background-color 0.2s;
/* cursor: ${props => (props.clickable ? 'pointer' : 'default')}; */
cursor: pointer;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
& input {
display: none;
}
&.currentTag {
background: var(--yellow);
color: hsla(0, 100%, 0%, 0.8);
}
}
.TagEmoji {
transform: scale(1.45);
}
.TagCount {
background: var(--blue);
font-size: 1rem;
color: white;
padding: 2px;
border-radius: 2px;
margin-left: 5px;
}
.BackToTopLink {
position: fixed;
bottom: 1%;
right: 1%;
background: var(--pink);
color: white;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
border-radius: 3px;
padding: 1rem;
transition: opacity 0.2s;
opacity: 0;
text-decoration: none;
/* ${props =>
props.percent > 0.25 &&
`
opacity: 1;
`} */
@media screen and (max-width: 500px) {
display: none;
}
}
.HeaderWrapper {
text-align: center;
& h1 {
font-size: 6rem;
}
}
.Main {
display: grid;
grid-gap: 3rem;
max-width: 1900px;
padding: 0 3rem;
margin: 5rem auto;
}
.People {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
grid-gap: 5rem;
@media all and (max-width: 400px) {
grid-template-columns: 1fr;
}
}
.People {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
grid-gap: 5rem;
@media all and (max-width: 400px) {
grid-template-columns: 1fr;
}
}

View file

@ -1,8 +1,9 @@
const { name } = require('country-emoji'); import { name } from 'country-emoji';
const people = require('../data.js'); import people from '../data.js';
type Person = typeof people[0];
function merge(prop) { function merge(prop: string) {
return function (acc, obj) { return function (acc: any, obj: Record<any, any>) {
// Remove duplicated values. // Remove duplicated values.
const values = [...new Set(obj[prop])]; const values = [...new Set(obj[prop])];
return [...values, ...acc]; return [...values, ...acc];
@ -14,24 +15,24 @@ function countInstances(acc, tag) {
return acc; return acc;
} }
function normalizeTag(tag) { export function normalizeTag(tag) {
return ( return (
tag tag
// Common mispellings currently seen in the data // Common mispellings currently seen in the data
// Do we want to go this far? // Do we want to go this far?
.replace(/frontend/i, 'Front End') .replace(/frontend/i, 'Front End')
.replace(/TailwindCSS/i, 'Tailwind CSS')
.replace(/backend/i, 'Back End') .replace(/backend/i, 'Back End')
.replace(/fullstack/i, 'Full Stack') .replace(/fullstack/i, 'Full Stack')
.replace(/a11y/i, 'Accessibility') .replace(/a11y/i, 'Accessibility')
.replace(/next.?js/i, 'Next') .replace(/next.?js/i, 'Next')
.replace(/react.?js/i, 'React') .replace(/react.?js/i, 'React')
// Or is lowercase enough? // Or is lowercase enough?
.toLowerCase() .toLowerCase()
); );
} }
function countries() { export function countries() {
const data = people const data = people
.map((person) => ({ .map((person) => ({
name: name(person.country), name: name(person.country),
@ -52,12 +53,13 @@ function countries() {
const sorted = Object.entries(data) const sorted = Object.entries(data)
.map(([, country]) => country) .map(([, country]) => country)
.sort((a, b) => b.count - a.count); .sort((a, b) => b.count - a.count)
.filter(Boolean);
return sorted; return sorted;
} }
function tags() { export function tags() {
const allTags = people.reduce(merge('tags'), []); const allTags = people.reduce(merge('tags'), []);
const counts = allTags.reduce(countInstances, {}); const counts = allTags.reduce(countInstances, {});
// sort and filter for any tags that only have 1 // sort and filter for any tags that only have 1
@ -88,18 +90,52 @@ function tags() {
return [{ name: 'all', count: people.length }, ...normalizedTags]; return [{ name: 'all', count: people.length }, ...normalizedTags];
} }
function devices() { export function devices() {
const all = [ const all = [
...people.map((person) => person.computer), ...people.map((person) => person.computer),
...people.map((person) => person.phone), ...people.map((person) => person.phone),
]; ].filter(Boolean);
return Object.entries(all.reduce(countInstances, {})) return Object.entries(all.reduce(countInstances, {}))
.map(([device, count]) => ({ name: device, count })) .map(([device, count]) => ({ name: device, count }))
.sort((a, b) => b.count - a.count); .sort((a, b) => b.count - a.count)
.map((device) => {
return device;
})
} }
exports.normalizeTag = normalizeTag; function unique(arr: string[]) {
exports.countries = countries; return Array.from(new Set(arr));
exports.tags = tags; }
exports.devices = devices;
const normalizedTagMap = tags().reduce((acc, tag) => {
const normalizedTag = normalizeTag(tag.name);
acc[normalizedTag] = tag.name;
return acc;
}, {});
export function getPeople(tag?: string) {
return people
.sort(() => Math.random() - 0.5)
.map((person) => {
const normalizedPerson = {
...person,
// Clean out people that added basically the same tags twice
tags: unique(
person.tags.map((tag) => normalizedTagMap[normalizeTag(tag)] || tag)
),
};
return {
...normalizedPerson,
id: `person-${normalizedPerson.name}`,
};
})
.filter((person) => {
if (!tag) {
return true;
}
return person.tags.includes(tag) || person.country === tag || person.phone === tag || person.computer === tag;
})
}

19
tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"include": ["remix.env.d.ts", "./**/*", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"strict": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "node",
"esModuleInterop": true,
"module": "esnext",
"target": "esnext",
"baseUrl": ".",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
"noEmit": true,
"resolveJsonModule": true
}
}