├── .dockerignore ├── .eslintrc ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── app ├── components │ ├── container │ │ ├── index.tsx │ │ └── styles.css │ ├── footer │ │ ├── index.tsx │ │ └── styles.css │ ├── header │ │ ├── index.tsx │ │ └── styles.css │ ├── hero │ │ ├── index.tsx │ │ └── styles.css │ ├── search │ │ ├── index.tsx │ │ └── styles.css │ ├── stack-grid │ │ ├── index.tsx │ │ └── styles.css │ └── svgs │ │ └── search.tsx ├── entry.client.tsx ├── entry.server.tsx ├── lib │ └── meta.ts ├── root.tsx ├── routes │ └── index.tsx └── styles │ └── base.css ├── content └── stacks.json ├── fly.toml ├── package-lock.json ├── package.json ├── public ├── assets │ └── og.png └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── start_with_migrations.sh └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:16-bullseye-slim as base 3 | 4 | # Install openssl for Prisma 5 | RUN apt-get update && apt-get install -y openssl 6 | 7 | # Install all node_modules, including dev dependencies 8 | FROM base as deps 9 | 10 | RUN mkdir /app 11 | WORKDIR /app 12 | 13 | ADD package.json package-lock.json ./ 14 | RUN npm install --production=false 15 | 16 | # Setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app 20 | WORKDIR /app 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json package-lock.json ./ 24 | RUN npm prune --production 25 | 26 | # Build the app 27 | FROM base as build 28 | 29 | ENV NODE_ENV=production 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | COPY --from=deps /app/node_modules /app/node_modules 35 | 36 | # If we're using Prisma, uncomment to cache the prisma schema 37 | # ADD prisma . 38 | # RUN npx prisma generate 39 | 40 | ADD . . 41 | RUN npm run build 42 | 43 | # Finally, build the production image with minimal footprint 44 | FROM base 45 | 46 | ENV NODE_ENV=production 47 | 48 | RUN mkdir /app 49 | WORKDIR /app 50 | 51 | COPY --from=production-deps /app/node_modules /app/node_modules 52 | 53 | # Uncomment if using Prisma 54 | # COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 55 | 56 | COPY --from=build /app/build /app/build 57 | COPY --from=build /app/public /app/public 58 | ADD . . 59 | 60 | CMD ["npm", "run", "start"] 61 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Nishiki Liu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/nshki/remix-directory/blob/main/public/assets/og.png?raw=true) 2 | 3 | # Remix Directory 4 | 5 | This is a community-powered directory of [Remix stacks](https://remix.run/stacks). 6 | 7 | Remix is a modern web framework that treats progressive enhancement and developer experience as first-class citizens. 8 | 9 | ## Adding your stack 10 | 11 | Simply add relevant changes to `content/stacks.json` and open a pull request! 12 | 13 | ## Running locally 14 | 15 | ``` 16 | npm install 17 | npm run dev 18 | ``` 19 | -------------------------------------------------------------------------------- /app/components/container/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import styles from './styles.css' 3 | 4 | export const containerLinks = () => [ 5 | { rel: 'stylesheet', href: styles } 6 | ] 7 | 8 | export const Container: FC = ({ children }) => ( 9 |
10 | {children} 11 |
12 | ) 13 | -------------------------------------------------------------------------------- /app/components/container/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 70rem; 3 | padding: 0 2rem; 4 | margin: 0 auto; 5 | } 6 | -------------------------------------------------------------------------------- /app/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.css' 2 | 3 | export const footerLinks = () => [ 4 | { rel: 'stylesheet', href: styles } 5 | ] 6 | 7 | export const Footer = () => ( 8 | 14 | ) 15 | -------------------------------------------------------------------------------- /app/components/footer/styles.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex; 3 | justify-content: flex-end; 4 | padding: 8rem 0 4rem; 5 | } 6 | -------------------------------------------------------------------------------- /app/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@remix-run/react' 2 | import styles from './styles.css' 3 | 4 | export const headerLinks = () => [ 5 | { rel: 'stylesheet', href: styles } 6 | ] 7 | 8 | export const Header = () => ( 9 |
10 | {`Remix Directory`} 11 | 12 | 22 |
23 | ) 24 | -------------------------------------------------------------------------------- /app/components/header/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: baseline; 5 | padding: 3rem 0; 6 | } 7 | 8 | .header__logo { 9 | font-size: 1.5rem; 10 | font-weight: 900; 11 | text-decoration: none; 12 | color: var(--color-white); 13 | } 14 | 15 | .header__items { 16 | padding: 0; 17 | list-style: none; 18 | } 19 | 20 | .header__link { 21 | font-weight: 700; 22 | text-decoration: none; 23 | color: var(--color-white); 24 | } 25 | 26 | .header__link:is(:focus, :hover) { 27 | text-decoration: underline; 28 | } 29 | -------------------------------------------------------------------------------- /app/components/hero/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEventHandler, FC } from 'react' 2 | import { Search, searchLinks } from '~/components/search' 3 | import styles from './styles.css' 4 | 5 | export const heroLinks = () => [ 6 | { rel: 'stylesheet', href: styles }, 7 | ...searchLinks() 8 | ] 9 | 10 | interface HeroProps { 11 | onSearchTermChange: ChangeEventHandler 12 | } 13 | 14 | export const Hero: FC = ({ onSearchTermChange }) => ( 15 |
16 |

17 | {`Find the `} 18 | {`Remix stack`} 19 | {` that works for you. Or `} 20 | add your own 21 | {`.`} 22 |

23 | 24 | 28 |
29 | ) 30 | -------------------------------------------------------------------------------- /app/components/hero/styles.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | gap: 3rem; 6 | padding: 5rem 0; 7 | } 8 | 9 | .hero__heading { 10 | max-width: 60rem; 11 | font-size: 2rem; 12 | font-weight: 900; 13 | text-align: center; 14 | color: var(--color-white); 15 | } 16 | 17 | .hero a { 18 | text-decoration: underline; 19 | color: var(--color-green); 20 | } 21 | 22 | .hero a:first-of-type { 23 | color: var(--color-yellow); 24 | } 25 | 26 | @media (min-width: 48rem) { 27 | .hero { 28 | padding: 10rem 0; 29 | } 30 | 31 | .hero__heading { 32 | font-size: 3rem; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/components/search/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEventHandler, FC } from 'react' 2 | import { SearchSvg } from '~/components/svgs/search' 3 | import styles from './styles.css' 4 | 5 | export const searchLinks = () => [ 6 | { rel: 'stylesheet', href: styles } 7 | ] 8 | 9 | interface SearchProps { 10 | placeholder: string 11 | onSearchTermChange: ChangeEventHandler 12 | } 13 | 14 | export const Search: FC = ({ 15 | placeholder = 'Search', 16 | onSearchTermChange 17 | }) => ( 18 |
19 |
20 | 21 |
22 | 23 | 29 |
30 | ) 31 | -------------------------------------------------------------------------------- /app/components/search/styles.css: -------------------------------------------------------------------------------- 1 | .search { 2 | background-color: var(--color-input); 3 | width: 100%; 4 | max-width: 28rem; 5 | border-radius: var(--radius-full); 6 | position: relative; 7 | } 8 | 9 | .search__icon { 10 | position: absolute; 11 | left: 1rem; 12 | top: 50%; 13 | transform: translateY(-50%); 14 | pointer-events: none; 15 | } 16 | 17 | .search__input { 18 | background-color: transparent; 19 | display: block; 20 | width: 100%; 21 | padding: 1rem 1rem 1rem 3.25rem; 22 | border: 0; 23 | font-size: 1.25rem; 24 | color: var(--color-white); 25 | } 26 | 27 | .search__input:focus { 28 | border-radius: var(--radius-full); 29 | outline: var(--color-purple) solid 0.125rem; 30 | } 31 | -------------------------------------------------------------------------------- /app/components/stack-grid/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import styles from './styles.css' 3 | 4 | export const stackGridLinks = () => [ 5 | { rel: 'stylesheet', href: styles } 6 | ] 7 | 8 | interface StackGridProps { 9 | stacks: { 10 | name: string 11 | description: string 12 | tags: string[] 13 | url: string 14 | }[] 15 | } 16 | 17 | export const StackGrid: FC = ({ stacks = [] }) => ( 18 |
19 | {stacks.map((stack, stackIndex) => ( 20 |
24 |
{stack.name}
25 | 26 |
    27 | {[stack.tags.map((tag, tagIndex) => ( 28 |
  • 32 | {tag} 33 |
  • 34 | ))]} 35 |
36 | 37 |

{stack.description}

38 | 39 | 43 | {`View on GitHub`} 44 | 45 |
46 | ))} 47 |
48 | ) 49 | -------------------------------------------------------------------------------- /app/components/stack-grid/styles.css: -------------------------------------------------------------------------------- 1 | .stack-grid { 2 | display: grid; 3 | gap: 1rem; 4 | } 5 | 6 | .stack-grid__item { 7 | background-color: var(--color-gray); 8 | display: flex; 9 | flex-direction: column; 10 | gap: 1rem; 11 | padding: 2rem; 12 | border-radius: var(--radius); 13 | } 14 | 15 | .stack-grid__name { 16 | font-size: 1.25rem; 17 | font-weight: 700; 18 | color: var(--color-white); 19 | } 20 | 21 | .stack-grid__tags { 22 | display: flex; 23 | flex-wrap: wrap; 24 | gap: 0.25rem; 25 | padding: 0; 26 | list-style: none; 27 | } 28 | 29 | .stack-grid__tag { 30 | background-color: rgba(0, 0, 0, 0.25); 31 | display: block; 32 | padding: 0.25rem 0.4rem; 33 | font-family: var(--font-mono); 34 | font-size: 0.9rem; 35 | } 36 | 37 | .stack-grid__link { 38 | margin-top: auto; 39 | margin-left: auto; 40 | font-weight: 700; 41 | } 42 | 43 | @media (min-width: 48rem) { 44 | .stack-grid { 45 | grid-template-columns: repeat(2, 1fr); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/components/svgs/search.tsx: -------------------------------------------------------------------------------- 1 | export const SearchSvg = ({ width = 25, height = 25 }) => ( 2 | 9 | 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from '@remix-run/react' 2 | import { hydrate } from 'react-dom' 3 | 4 | hydrate(, document) 5 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { EntryContext } from '@remix-run/node' 2 | import { RemixServer } from '@remix-run/react' 3 | import { renderToString } from 'react-dom/server' 4 | 5 | export default function handleRequest ( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | let markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set('Content-Type', 'text/html') 16 | 17 | return new Response('' + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /app/lib/meta.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns all meta tags associated with a page title. 3 | */ 4 | export function title (text: string) { 5 | return { 6 | title: text, 7 | 'twitter:title': text 8 | } 9 | } 10 | 11 | /** 12 | * Returns all meta tags associated with a page description. 13 | */ 14 | export function description (text: string) { 15 | return { 16 | description: text, 17 | 'og:description': text, 18 | 'twitter:description': text 19 | } 20 | } 21 | 22 | /** 23 | * Returns all meta tags associated with a page image. 24 | */ 25 | export function image (url: string) { 26 | return { 27 | 'og:image': url, 28 | 'twitter:image': url 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { LinksFunction, MetaFunction } from '@remix-run/node' 2 | import { 3 | Links, 4 | LiveReload, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration 9 | } from '@remix-run/react' 10 | import { title, description, image } from './lib/meta' 11 | import styles from '~/styles/base.css' 12 | 13 | export const meta: MetaFunction = () => ({ 14 | charset: 'utf-8', 15 | viewport: 'width=device-width,initial-scale=1', 16 | ...title('Remix Directory'), 17 | ...description('Find the Remix stack that works for you.'), 18 | ...image('https://remix.directory/assets/og.png'), 19 | 'twitter:card': 'summary_large_image' 20 | }) 21 | 22 | export const links: LinksFunction = () => [ 23 | { rel: 'stylesheet', href: styles } 24 | ] 25 | 26 | export default function App () { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEventHandler } from 'react' 2 | import { useState } from 'react' 3 | import { LinksFunction } from '@remix-run/node' 4 | import { Container, containerLinks } from '~/components/container' 5 | import { Header, headerLinks } from '~/components/header' 6 | import { Hero, heroLinks } from '~/components/hero' 7 | import { StackGrid, stackGridLinks } from '~/components/stack-grid' 8 | import { Footer, footerLinks } from '~/components/footer' 9 | 10 | import { stacks } from '../../content/stacks.json' 11 | 12 | export const links: LinksFunction = () => [ 13 | ...containerLinks(), 14 | ...headerLinks(), 15 | ...heroLinks(), 16 | ...stackGridLinks(), 17 | ...footerLinks() 18 | ] 19 | 20 | export default function Index () { 21 | let [searchTerm, setSearchTerm] = useState('') 22 | let onSearchTermChange: ChangeEventHandler = (event) => { 23 | setSearchTerm(event.target.value.toLowerCase()) 24 | } 25 | 26 | // Apply a really trivial filtering mechanism for stacks for now. If this 27 | // project starts housing a much larger number of stacks, we can revisit! 28 | let filteredStacks = stacks.filter((stack) => { 29 | let { name, description, tags } = stack 30 | let hasNameMatch = name.toLowerCase().includes(searchTerm) 31 | let hasDescriptionMatch = description.toLowerCase().includes(searchTerm) 32 | let hasTagMatch = tags.includes(searchTerm) 33 | return hasNameMatch || hasDescriptionMatch || hasTagMatch 34 | }) 35 | 36 | return ( 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 |