Skip to content

Commit 1dbf014

Browse files
Add showcase (#17)
1 parent 4e6aef9 commit 1dbf014

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+418
-0
lines changed

app/lib/showcase.server.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import yaml from "yaml";
2+
import showcaseYamlFileContents from "../../data/showcase.yaml?raw";
3+
4+
export const showcaseExamples: ShowcaseExample[] = yaml.parse(
5+
showcaseYamlFileContents,
6+
);
7+
8+
export interface ShowcaseExample {
9+
name: string;
10+
description: string;
11+
link: string;
12+
imgSrc: string;
13+
videoSrc: string;
14+
}

app/routes/_extras.showcase.tsx

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { Fragment, forwardRef, useRef } from "react";
2+
import type { ShowcaseExample } from "~/lib/showcase.server";
3+
import { showcaseExamples } from "~/lib/showcase.server";
4+
import { clsx } from "clsx";
5+
import { useHydrated } from "~/ui/primitives/utils";
6+
import type { Route } from "./+types/_extras.showcase";
7+
8+
export const loader = async ({ request }: Route.LoaderArgs) => {
9+
let requestUrl = new URL(request.url);
10+
let siteUrl = requestUrl.protocol + "//" + requestUrl.host;
11+
12+
return { siteUrl, showcaseExamples };
13+
};
14+
15+
// Stolen from _marketing._index.tsx. eventually would like to replace
16+
export function meta({ data }: Route.MetaArgs) {
17+
let { siteUrl } = data;
18+
let title = "Remix Showcase";
19+
let image = siteUrl ? `${siteUrl}/img/og.1.jpg` : null;
20+
let description = "See who is using Remix to build better websites.";
21+
22+
return [
23+
{ title },
24+
{ name: "description", content: description },
25+
{ property: "og:url", content: `${siteUrl}/showcase` },
26+
{ property: "og:title", content: title },
27+
{ property: "og:description", content: description },
28+
{ property: "og:image", content: image },
29+
{ name: "twitter:card", content: "summary_large_image" },
30+
{ name: "twitter:creator", content: "@remix_run" },
31+
{ name: "twitter:site", content: "@remix_run" },
32+
{ name: "twitter:title", content: title },
33+
{ name: "twitter:description", content: description },
34+
{ name: "twitter:image", content: image },
35+
];
36+
}
37+
38+
export default function Showcase({ loaderData }: Route.ComponentProps) {
39+
// Might be a bit silly to declare here and then prop-drill, but was a little concerned about a needless useEffect+useState for every card
40+
let isHydrated = useHydrated();
41+
42+
return (
43+
<main
44+
className="container mt-8 flex flex-1 flex-col items-center"
45+
tabIndex={-1} // is this every gonna be focused? just copy pasta
46+
>
47+
<div className="max-w-3xl text-center">
48+
<h1 className="text-4xl font-bold md:text-5xl lg:text-6xl">
49+
Remix Showcase
50+
</h1>
51+
<p className="mt-4 max-w-2xl text-lg font-light">
52+
Checkout the companies, organizations, nonprofits, and indie
53+
developers building better websites with Remix
54+
</p>
55+
</div>
56+
<ul className="mt-8 grid w-full max-w-md grid-cols-1 gap-x-6 gap-y-10 self-center md:max-w-3xl md:grid-cols-2 lg:max-w-6xl lg:grid-cols-3 lg:gap-x-8">
57+
{loaderData.showcaseExamples.map((example, i) => {
58+
let loading: ShowcaseTypes["loading"] = i < 6 ? "eager" : "lazy";
59+
return (
60+
<Fragment key={example.name}>
61+
<DesktopShowcase
62+
// Non-focusable since focusing on the anchor tag starts the
63+
// video -- need to ensure that this is fine for screen
64+
// readers, but I'm fairly confident the video is not critical
65+
// information and just visual flair so I don't think we're
66+
// providing an unusable or even bad experience to
67+
// screen-reader users
68+
isHydrated={isHydrated}
69+
loading={loading}
70+
{...example}
71+
/>
72+
<MobileShowcase
73+
isHydrated={isHydrated}
74+
loading={loading}
75+
{...example}
76+
/>
77+
</Fragment>
78+
);
79+
})}
80+
</ul>
81+
</main>
82+
);
83+
}
84+
85+
type ShowcaseTypes = ShowcaseExample & {
86+
loading?: "lazy" | "eager";
87+
isHydrated: boolean;
88+
};
89+
90+
function DesktopShowcase({
91+
name,
92+
description,
93+
link,
94+
imgSrc,
95+
videoSrc,
96+
loading,
97+
isHydrated,
98+
}: ShowcaseTypes) {
99+
let videoRef = useRef<HTMLVideoElement | null>(null);
100+
101+
return (
102+
<li className="relative hidden overflow-hidden rounded-md border border-gray-100 shadow hover:shadow-blue-200 dark:border-gray-800 md:block">
103+
<ShowcaseVideo
104+
ref={videoRef}
105+
videoSrc={videoSrc}
106+
poster={imgSrc}
107+
autoPlay={false}
108+
loading={loading}
109+
isHydrated={isHydrated}
110+
/>
111+
<ShowcaseDescription
112+
name={name}
113+
description={description}
114+
link={link}
115+
isHydrated={isHydrated}
116+
playVideo={() => videoRef.current?.play()}
117+
pauseVideo={() => videoRef.current?.pause()}
118+
/>
119+
</li>
120+
);
121+
}
122+
123+
function MobileShowcase({
124+
name,
125+
description,
126+
link,
127+
imgSrc,
128+
isHydrated,
129+
loading,
130+
}: Omit<ShowcaseTypes, "videoSrc">) {
131+
return (
132+
<li className="relative block overflow-hidden rounded-md border border-gray-100 shadow hover:shadow-blue-200 dark:border-gray-800 md:hidden">
133+
<div className={"aspect-[4/3] object-cover object-top"}>
134+
<img
135+
className="max-h-full w-full max-w-full"
136+
width={800}
137+
height={600}
138+
alt=""
139+
src={imgSrc}
140+
loading={loading}
141+
/>
142+
</div>
143+
<ShowcaseDescription
144+
name={name}
145+
description={description}
146+
link={link}
147+
isHydrated={isHydrated}
148+
/>
149+
</li>
150+
);
151+
}
152+
153+
let ShowcaseVideo = forwardRef<
154+
HTMLVideoElement,
155+
Pick<ShowcaseTypes, "videoSrc" | "isHydrated" | "loading"> &
156+
React.VideoHTMLAttributes<HTMLVideoElement>
157+
>(({ videoSrc, className, isHydrated, loading, ...props }, ref) => {
158+
return (
159+
<div className={clsx("aspect-[4/3] object-cover object-top", className)}>
160+
<video
161+
ref={ref}
162+
className="max-h-full w-full max-w-full"
163+
disablePictureInPicture
164+
disableRemotePlayback
165+
playsInline
166+
loop
167+
muted
168+
width={800}
169+
height={600}
170+
// Note: autoplay must be off for this strategy to work, if autoplay is turned on all assets will be downloaded automatically
171+
tabIndex={isHydrated ? -1 : 0}
172+
preload={loading === "eager" ? "auto" : "none"}
173+
{...props}
174+
>
175+
{["webm", "mp4"].map((ext) => (
176+
<source
177+
key={ext}
178+
src={`${videoSrc}.${ext}`}
179+
type={`video/${ext}`}
180+
width={800}
181+
height={600}
182+
// avoid video assets downloading on mobile
183+
media="(min-width: 768px)"
184+
/>
185+
))}
186+
</video>
187+
</div>
188+
);
189+
});
190+
191+
ShowcaseVideo.displayName = "ShowcaseVideo";
192+
193+
function ShowcaseDescription({
194+
description,
195+
link,
196+
name,
197+
isHydrated,
198+
playVideo,
199+
pauseVideo,
200+
}: Pick<ShowcaseTypes, "description" | "link" | "name" | "isHydrated"> & {
201+
playVideo?: () => void;
202+
pauseVideo?: () => void;
203+
}) {
204+
return (
205+
<div
206+
className={clsx("p-4", {
207+
// relative position in combination with the inner span makes the whole
208+
// card clickable only after hydration do we want it to be the size of
209+
// the whole card, otherwise this will get in the way of controlling
210+
// the video
211+
relative: !isHydrated,
212+
})}
213+
>
214+
<h2 className="font-medium hover:text-blue-brand">
215+
<a
216+
href={link}
217+
rel="noopener noreferrer"
218+
target="_blank"
219+
onFocus={playVideo}
220+
onBlur={pauseVideo}
221+
>
222+
<span
223+
onMouseOver={playVideo}
224+
onMouseOut={pauseVideo}
225+
className="absolute inset-0"
226+
/>
227+
228+
{name}
229+
</a>
230+
</h2>
231+
<p className="pt-2 text-xs font-light">{description}</p>
232+
</div>
233+
);
234+
}

data/showcase.yaml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Note: all video sources should have a .webm and a .mp4 for a fallback, hence why the file type isn't specified
2+
3+
- name: "Shopify"
4+
description: "Ecommerce platform website"
5+
link: "https://www.shopify.com"
6+
imgSrc: "/showcase-assets/shopify.webp"
7+
videoSrc: "/showcase-assets/shopify"
8+
9+
- name: "Sanity Learn"
10+
description: "Interactive educational platform for Sanity.io"
11+
link: "https://www.sanity.io/learn"
12+
imgSrc: "/showcase-assets/sanity-learn.webp"
13+
videoSrc: "/showcase-assets/sanity-learn"
14+
15+
- name: "Docker"
16+
description: "Docker Scout Dashboard"
17+
link: "https://www.docker.com/products/docker-scout/"
18+
imgSrc: "/showcase-assets/docker-scout.webp"
19+
videoSrc: "/showcase-assets/docker-scout"
20+
21+
- name: "Shop"
22+
description: "Shop by Shopify on web"
23+
link: "https://shop.app/"
24+
imgSrc: "/showcase-assets/shop.webp"
25+
videoSrc: "/showcase-assets/shop"
26+
27+
- name: "NASA"
28+
description: "GCN: NASA's Time-Domain and Multimessenger Alert System"
29+
link: "https://github.com/nasa-gcn/gcn.nasa.gov"
30+
imgSrc: "/showcase-assets/nasa-gnc.webp"
31+
videoSrc: "/showcase-assets/nasa-gnc"
32+
33+
- name: "Hydrogen"
34+
description: "Shopify's headless commerce framework"
35+
link: "https://hydrogen.shopify.dev/"
36+
imgSrc: "/showcase-assets/hydrogen.webp"
37+
videoSrc: "/showcase-assets/hydrogen"
38+
39+
- name: "Nour Hammour"
40+
description: "Luxury fashion outwear brand from Paris"
41+
link: "https://nour-hammour.com/"
42+
imgSrc: "/showcase-assets/nour_hammour.webp"
43+
videoSrc: "/showcase-assets/nour_hammour"
44+
45+
- name: "Daffy"
46+
description: "Donor-Advised Fund"
47+
link: "https://www.daffy.org/"
48+
imgSrc: "/showcase-assets/daffy.webp"
49+
videoSrc: "/showcase-assets/daffy"
50+
51+
- name: "Winamp"
52+
description: "Winamp Web Player"
53+
link: "https://www.winamp.com/"
54+
imgSrc: "/showcase-assets/winamp.webp"
55+
videoSrc: "/showcase-assets/winamp"
56+
57+
- name: "Atmos Financial"
58+
description: "Bank accounts that fund solar"
59+
link: "https://www.joinatmos.com"
60+
imgSrc: "/showcase-assets/atmos.webp"
61+
videoSrc: "/showcase-assets/atmos"
62+
63+
- name: "Webstudio"
64+
description: "Open source visual development platform"
65+
link: "https://webstudio.is/"
66+
imgSrc: "/showcase-assets/webstudio.webp"
67+
videoSrc: "/showcase-assets/webstudio"
68+
69+
- name: "Oxide"
70+
description: "Rack-scale servers for on-premises infrastructure"
71+
link: "https://oxide.computer/"
72+
imgSrc: "/showcase-assets/oxide.webp"
73+
videoSrc: "/showcase-assets/oxide"
74+
75+
- name: "Trigger.dev"
76+
description: "Open source background jobs framework"
77+
link: "https://trigger.dev/"
78+
imgSrc: "/showcase-assets/trigger.webp"
79+
videoSrc: "/showcase-assets/trigger"
80+
81+
- name: "Robots Guide"
82+
description: "Interactive guide to the world of Robotics"
83+
link: "https://robotsguide.com/"
84+
imgSrc: "/showcase-assets/robots-guide.webp"
85+
videoSrc: "/showcase-assets/robots-guide"
86+
87+
- name: "UDisc"
88+
description: "App for Disk Golfers"
89+
link: "https://udisc.com"
90+
imgSrc: "/showcase-assets/udisk.webp"
91+
videoSrc: "/showcase-assets/udisk"
92+
93+
- name: "Kent C. Dodds Tech"
94+
description: "Kent C. Dodds' gamified Blog, Call-in Podcast, and portfolio"
95+
link: "https://kentcdodds.com"
96+
imgSrc: "/showcase-assets/kentcdodds.webp"
97+
videoSrc: "/showcase-assets/kentcdodds"
98+
99+
- name: "SaasRock"
100+
description: "Remix SaaS Boilerplate"
101+
link: "https://saasrock.com"
102+
imgSrc: "/showcase-assets/saasrock.webp"
103+
videoSrc: "/showcase-assets/saasrock"
104+
105+
- name: "Passionfroot"
106+
description: "The next-gen platform for creator-brand partnerships"
107+
link: "https://www.passionfroot.me"
108+
imgSrc: "/showcase-assets/passionfroot.webp"
109+
videoSrc: "/showcase-assets/passionfroot"
110+
111+
- name: "Solin"
112+
description: "The world's largest marketplace for fitness challenges from creators"
113+
link: "https://www.solin.stream/"
114+
imgSrc: "/showcase-assets/solin.webp"
115+
videoSrc: "/showcase-assets/solin"
116+
117+
- name: "Forge 42"
118+
description: "Forge42 is a development agency that specializes in web development and design with Remix.run"
119+
link: "https://forge42.dev"
120+
imgSrc: "/showcase-assets/forge42.webp"
121+
videoSrc: "/showcase-assets/forge42"
122+
123+
- name: "OSS Capital"
124+
description: "Early-stage VC for commercial open source"
125+
link: "https://oss.capital/"
126+
imgSrc: "/showcase-assets/oss-capital.webp"
127+
videoSrc: "/showcase-assets/oss-capital"

public/showcase-assets/atmos.webm

1.13 MB
Binary file not shown.

public/showcase-assets/atmos.webp

97.5 KB
Loading

0 commit comments

Comments
 (0)