render.jsx
9.7 kB · jsx · 341 lines
1import { Menu as Sitemap, Shell } from 'kit/ui/chrome.jsx';2import { configure } from 'kit/ui/config.js';3import site from '../site.json';4import { cdnUrl, grid, money, ogUrl, tileUrl } from './shop.ts';56configure(site);78const CATEGORIES = [9 ['accessories', 'Accessories'],10 ['bags', 'Bags'],11 ['kids', 'Kids'],12 ['men', 'Men'],13 ['unisex', 'Unisex'],14 ['women', 'Women'],15];1617/* SHELL */1819export function Page({ route, nav, controls, wide, children }) {20 return (21 <Shell route={route} tree={nav} controls={controls} wide={wide}>22 {children}23 </Shell>24 );25}2627/* PIECES */2829function Card({ product, width = 600, eager = false, pick = 0 }) {30 const image = product.images.length ? product.images[pick % product.images.length] : null;31 return (32 <a className="tile" href={`/products/${product.key}/`}>33 {image ? (34 <img35 src={grid(image.url, width)}36 alt={image.alt || product.title}37 width={width}38 height={width}39 loading={eager ? 'eager' : 'lazy'}40 fetchPriority={eager ? 'high' : undefined}41 decoding="async"42 />43 ) : (44 <img src={tileUrl(product.key, 3)} alt={product.title} width="88" height="88" loading="lazy" decoding="async" />45 )}46 <h2>{product.title}</h2>47 <p className="num">{money(product.price)}</p>48 </a>49 );50}5152function Gallery({ products, eager = 0 }) {53 return (54 <div className="gallery square">55 {products.map((product, i) => (56 <Card key={product.key} product={product} eager={i < eager} pick={i} />57 ))}58 </div>59 );60}6162export function Filter({ label, count, next }) {63 return (64 <>65 <a className="filter" href={next}>{`${label} (${count})`}</a>66 <ul className="tabs stack">67 <li><a href="/collections/all/">All</a></li>68 {CATEGORIES.map(([slug, name]) => (69 <li key={slug}><a href={`/collections/${slug}/`}>{name}</a></li>70 ))}71 </ul>72 </>73 );74}7576function Deck({ cards }) {77 return (78 <div className="cards">79 {cards.map((card) => (80 <a className="card" key={card.href} href={card.href}>81 {card.image ? (82 <img src={grid(card.image.url, 600)} alt={card.name} width="600" height="600" loading="lazy" decoding="async" />83 ) : card.key ? (84 <img src={tileUrl(card.key, 3)} alt={card.name} width="88" height="88" loading="lazy" decoding="async" />85 ) : null}86 <p className="code">87 {card.emoji ? <span role="img" aria-label={card.name}>{card.emoji}</span> : null} {card.name}88 </p>89 <p className="name">{`${card.count} design${card.count === 1 ? '' : 's'}`}</p>90 </a>91 ))}92 </div>93 );94}9596/* HOME */9798export function Home({ products, lead }) {99 return (100 <>101 <div className="lede">102 <h1>{site.name}</h1>103 <p className="lead">{lead}</p>104 </div>105 <Gallery products={products} eager={2} />106 </>107 );108}109110/* COLLECTIONS */111112export function Collections({ groups, lead }) {113 return (114 <>115 <div className="lede">116 <h1>Collections</h1>117 <p className="lead">{lead}</p>118 </div>119 {groups.map((group) => (120 <section key={group.name}>121 <h2>{group.name}</h2>122 <Deck cards={group.cards} />123 </section>124 ))}125 </>126 );127}128129export function Collection({ name, products }) {130 return (131 <>132 <div className="lede">133 <h1>{name}</h1>134 <p className="lead">{`${products.length} design${products.length === 1 ? '' : 's'}`}</p>135 </div>136 <Gallery products={products} eager={2} />137 </>138 );139}140141/* PRODUCT */142143export function Product({ product, siblings, collection, files, tiles }) {144 const images = product.images;145 return (146 <>147 <div className="lede">148 <h1>149 <a href={collection}>{product.title}</a>150 </h1>151 <p className="fine">Design {product.key}</p>152 </div>153 <div className="gallery square" data-mockups>154 {images.map((image, i) => (155 <a className="tile" key={image.style} href={grid(image.url, 2000)} data-style={image.style}>156 <img157 src={grid(image.url, 1000)}158 alt={image.alt}159 width="1000"160 height="1000"161 loading={i === 0 ? 'eager' : 'lazy'}162 fetchPriority={i === 0 ? 'high' : undefined}163 decoding="async"164 data-style={image.style}165 />166 </a>167 ))}168 </div>169 <div className="strip" data-strip>170 {tiles.map((n) => (171 <a key={n} href={tileUrl(product.key, n)} download data-tile={n}>172 <img src={tileUrl(product.key, n)} alt={`${product.key} ${n}x${n}`} width="88" height="88" loading="lazy" decoding="async" data-tile={n} />173 </a>174 ))}175 </div>176 <p className="fine" data-downloads>177 <span>Download </span>178 {files.map((name) => (179 <a key={name} href={cdnUrl(product.key, name)} download>{name}</a>180 ))}181 {tiles.map((n) => (182 <a key={`t${n}`} href={tileUrl(product.key, n)} download>{`${n}x${n}`}</a>183 ))}184 <a href={ogUrl(product.key)} download>og</a>185 </p>186 <h2 id="designs">Designs</h2>187 <div className="gallery square" data-siblings>188 {siblings.map((one) => (189 <a190 className="tile"191 key={one.key}192 href={`/products/${one.key}/`}193 aria-current={one.key === product.key ? 'page' : undefined}194 data-key={one.key}195 >196 <img src={tileUrl(one.key, 3)} alt={one.key} width="88" height="88" loading="lazy" decoding="async" />197 <p className="num">{money(one.price)}</p>198 </a>199 ))}200 </div>201 </>202 );203}204205export function Controls({ product, printful, sizes, index, total, buy, expires, live }) {206 return (207 <>208 <p className="num price" data-price>{money(product.price)}</p>209 {sizes.length > 1 && (210 <div className="tabs sizes">211 {sizes.map((size) => (212 <button213 type="button"214 key={size.id}215 data-variant={size.id}216 data-price={size.price}217 data-size={size.size}218 aria-pressed={size.id === product.variant ? 'true' : 'false'}219 >220 {size.size}221 </button>222 ))}223 </div>224 )}225 <button226 type="button"227 className="primary"228 data-add229 data-key={product.key}230 data-title={product.title}231 data-variant={product.variant}232 data-price={product.price}233 data-size={product.variants[0] ? product.variants[0].size : ''}234 disabled={!product.available}235 >236 Add to cart237 </button>238 <a className="buy" href={buy} data-buy rel="noopener">Buy now</a>239 <p className="count" data-expiry={expires} data-live={live} data-ttl></p>240 <div className="row pager">241 <button type="button" data-prev disabled={total < 2}>Prev</button>242 <span className="num" data-index>{`${index + 1} / ${total}`}</span>243 <button type="button" data-next disabled={total < 2}>Next</button>244 </div>245 <button type="button" data-share>Share</button>246 <a className="button" href={printful} rel="noopener">View on Printful</a>247 </>248 );249}250251/* CART */252253export function Cart() {254 return (255 <>256 <div className="lede">257 <h1>Cart</h1>258 </div>259 <div data-cart-lines></div>260 <div className="sum" data-cart-sum></div>261 <template id="line">262 <div className="item">263 <a className="shot" data-href>264 <img alt="" width="88" height="88" decoding="async" />265 </a>266 <div className="who">267 <a data-title></a>268 <p className="dim" data-size></p>269 <div className="qty">270 <button type="button" data-dec aria-label="One less">-</button>271 <b className="num" data-qty></b>272 <button type="button" data-inc aria-label="One more">+</button>273 </div>274 </div>275 <p className="num" data-total></p>276 <button type="button" data-remove>Remove</button>277 </div>278 </template>279 </>280 );281}282283/* PAGES */284285export function Menu({ tree }) {286 return (287 <>288 <div className="lede">289 <h1>Menu</h1>290 <p className="lead">Every page on {site.name}.</p>291 </div>292 <Sitemap tree={tree} />293 <section className="elsewhere">294 <h2>Elsewhere</h2>295 <ul>296 {site.socials.map((one) => (297 <li key={one.href}>298 <a href={one.href}>{one.name}</a>299 </li>300 ))}301 <li>302 <a href={`mailto:${site.contact}`}>{site.contact}</a>303 </li>304 </ul>305 </section>306 </>307 );308}309310export function Doc({ title, lead, body, hero, action }) {311 return (312 <>313 {hero && (314 <figure className="opener">315 <img className="dark" src={`/figures/${hero}-dark.png`} alt="" width="1024" height="1024" decoding="async" />316 <img className="light" src={`/figures/${hero}-light.png`} alt="" width="1024" height="1024" decoding="async" />317 </figure>318 )}319 <div className="lede">320 <h1>{title}</h1>321 {lead && <p className="lead">{lead}</p>}322 </div>323 {action && (324 <div className="prose">325 <p><a className="button primary" href={action.href}>{action.name}</a></p>326 </div>327 )}328 {body && <article className="prose" dangerouslySetInnerHTML={{ __html: body }}></article>}329 </>330 );331}332333export function NotFound() {334 return (335 <div className="lede">336 <h1>Not found</h1>337 <p className="lead">That page is gone, or the design expired.</p>338 <p><a href="/">Back to the shop</a></p>339 </div>340 );341}