Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
/node_modules
/.pnp
.pnp.js
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# testing
/coverage
Expand All @@ -28,3 +34,6 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local

# typescript
tsconfig.tsbuildinfo
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
79 changes: 79 additions & 0 deletions components/AppCard/AppCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.card {
overflow: hidden;
transition: background-color 0.1s ease-out, box-shadow 0.2s ease-out;
}

.card:hover {
background-color: #F7F7F7;
box-shadow: 0 16px 30px rgba(62, 38, 113, 0.3), 0 0 0 2px rgba(62, 38, 113, 0.12);
box-shadow: 0 13px 20px color-mix(in srgb, var(--card-color) 45%, transparent),
0 0 0 2px color-mix(in srgb, var(--card-color) 22%, transparent);
}

.description {
color: #000;
font-family: 'Inter', sans-serif !important;
font-size: 14.907px;
font-style: normal;
font-weight: 400;
line-height: 130% !important;
}

.byline {
margin-top: -4px;
}

.ribbon {
position: absolute;
z-index: 1;
top: 18px;
right: -48px;
width: 170px;
padding: 5px 0;
overflow: hidden;
background: #333;
color: #fff;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0;
line-height: 16px;
opacity: 0;
text-align: center;
transform: rotate(32deg) translate(18px, -8px);
transform-origin: center;
transition: opacity 180ms ease, transform 180ms ease;
}

.card:hover .ribbon {
opacity: 1;
transform: rotate(32deg) translate(0, 0);
}

.ribbonText {
display: block;
width: max-content;
transform: translateX(-36px);
}

.card:hover .ribbonText {
animation: rollComingSoon 2.4s linear infinite;
}

@keyframes rollComingSoon {
from {
transform: translateX(-36px);
}

to {
transform: translateX(-190px);
}
}

@media (prefers-reduced-motion: reduce) {
.ribbon,
.ribbonText {
transition: none;
animation: none;
}
}
148 changes: 148 additions & 0 deletions components/AppCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Card, Pane, Text, Paragraph } from 'evergreen-ui'
import Link from 'next/link'
import { CSSProperties, ElementType } from 'react'
import styles from './AppCard.module.css'

export interface AppCardTitle {
/** black portion of the title */
base: string;
/** portion of the title accented with the app's brand color */
highlight: string;
}

export interface AppCardByline extends AppCardTitle {
/** color for the highlighted portion of the byline */
color: string;
}

export interface AppCardProps {
/** app name, rendered as hoagie{name} unless title is set */
name: string;
/** short description shown under the app title */
description: string;
/** icon component rendered inside the colored block */
icon: ElementType;
/** icon size in pixels, defaults to 35 */
iconSize?: number;
/** brand color for the icon block and title accent */
color: string;
/** link target, defaults to the app subdomain */
href?: string;
/** overrides the default "hoagie{name}" title, e.g. for co-branded apps */
title?: AppCardTitle;
/** small credit line under the title, e.g. "by hoagieclub" */
byline?: AppCardByline;
/** shows an animated coming-soon ribbon on hover */
comingSoon?: boolean;
}

/** AppCard is a preview card for a single Hoagie app. */
const AppCard = ({
name,
description,
icon,
iconSize = 35,
color,
href = `https://${name}.hoagie.io/`,
title = { base: 'hoagie', highlight: name },
byline,
comingSoon = false,
}: AppCardProps) => {
const Icon = icon

const card = (
<Card
background="white"
display="flex"
width="100%"
maxWidth={370}
height={130}
minHeight={130}
borderRadius={10}
cursor={comingSoon ? 'default' : 'pointer'}
elevation={1}
position="relative"
className={styles.card}
style={{ '--card-color': color } as CSSProperties}
>
{comingSoon && (
<span className={styles.ribbon} aria-hidden="true">
<span className={styles.ribbonText}>
COMING SOON&nbsp;&middot;&nbsp;COMING SOON&nbsp;&middot;&nbsp;COMING SOON&nbsp;&middot;&nbsp;COMING SOON&nbsp;&middot;&nbsp;
</span>
</span>
)}
<Pane
background={color}
display="flex"
alignItems="center"
justifyContent="center"
width={90}
flexShrink={0}
borderTopLeftRadius={10}
borderBottomLeftRadius={10}
>
<Icon size={iconSize} color="white" />
</Pane>
<Pane
display="flex"
flexDirection="column"
justifyContent="center"
minWidth={0}
flex={1}
paddingX={20}
paddingY={20}
>
<Pane marginBottom={byline ? 0 : 8}>
<Text
display="inline-block"
className="hoagie"
color="gray900"
fontWeight={600}
fontSize={22}
>
{title.base}
</Text>
<Text
display="inline-block"
className="hoagie"
color={color}
fontWeight={600}
fontSize={22}
>
{title.highlight}
</Text>
</Pane>
{byline && (
<Pane className={styles.byline} marginBottom={4}>
<Text
display="inline-block"
color="gray900"
fontWeight={600}
fontSize={14}
>
{byline.base}
</Text>
<Text
display="inline-block"
color={byline.color}
fontWeight={600}
fontSize={14}
>
{byline.highlight}
</Text>
</Pane>
)}
<Paragraph
className={styles.description}
>
{description}
</Paragraph>
</Pane>
</Card>
)

return comingSoon ? card : <Link href={href}>{card}</Link>
}

export default AppCard
7 changes: 7 additions & 0 deletions components/AppGrid/AppGrid.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 24px;
}
19 changes: 19 additions & 0 deletions components/AppGrid/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Pane } from 'evergreen-ui'
import AppCard, { AppCardProps } from '../AppCard'
import styles from './AppGrid.module.css'

interface AppGridProps {
/** app card configs to display, in order */
apps: AppCardProps[];
}

/** AppGrid lays out AppCards in a centered, wrapping grid. */
const AppGrid = ({ apps }: AppGridProps) => (
<Pane className={styles.grid}>
{apps.map((app) => (
<AppCard key={app.name} {...app} />
))}
</Pane>
)

export default AppGrid
74 changes: 74 additions & 0 deletions components/AppPreviewModal/AppPreviewModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.popup {
position: relative;
width: min(760px, calc(100vw - 48px));
height: min(680px, calc(100vh - 96px));
padding: 10px;
border-radius: 28px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35);
animation: popIn 0.2s ease-out;
}

.inner {
position: relative;
width: 100%;
height: 100%;
background: white;
border-radius: 20px;
overflow: hidden;
}

@keyframes popIn {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}

.image {
display: block;
width: 100%;
height: 62%;
object-fit: cover;
object-position: top center;
}

.fade {
position: absolute;
left: 0;
right: 0;
top: 30%;
height: 32%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 92%);
}

.badge {
position: absolute;
top: 48%;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 16px;
padding: 14px 28px 14px 14px;
background: white;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
}

.badgeText {
white-space: nowrap;
}

.badgeIcon {
display: flex;
align-items: center;
justify-content: center;
width: 72px;
height: 72px;
border-radius: 10px;
flex-shrink: 0;
}
Loading
Loading