Compare commits

..

1 commit

Author SHA1 Message Date
Ben Wolfram ab6c666b5d
Merge pull request #102 from Shopify/404-style-updates
[Template] Update 404 Styles
2021-11-05 13:30:14 -05:00
327 changed files with 2406 additions and 3481 deletions

View file

@ -2,7 +2,6 @@
const {defineConfig} = require('eslint-define-config');
module.exports = defineConfig({
ignorePatterns: ['**/graphql/types/types.ts'],
root: true,
plugins: ['eslint-plugin-tsdoc'],
extends: [

View file

@ -10,11 +10,7 @@ assignees: ''
A clear and concise description of what the bug is.
**To Reproduce**
- Provide a link to a [StackBlitz sandbox](https://hydrogen.new) which reproduces the issue
- OR: provide a link to a repository that reproduces the issue
If you cannot do one of the above, list steps to reproduce the behaviour below:
Steps to reproduce the behaviour:
1. Go to '...'
2. Click on '....'

View file

@ -15,4 +15,4 @@
- [ ] Add your change under the `Unreleased` heading in the package's `CHANGELOG.md`
- [ ] Read the [Contributing Guidelines](https://github.com/shopify/hydrogen/blob/main/docs/contributing.md)
- [ ] Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`)
- [ ] Update docs in this repository for your change, if needed
- [ ] (Shopifolk only) Open a PR in the Shopify Dev Docs with updates to the Hydrogen documentation, if needed

View file

@ -1,21 +0,0 @@
name: Publish to Stackblitz
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
name: Publish latest release to Stackblitz
steps:
- name: Checkout the code
uses: actions/checkout@v2
- name: Rename gitignore
run: |
mv packages/dev/_gitignore packages/dev/.gitignore
- name: Push to stackblitz branch
uses: EndBug/add-and-commit@v7.4.0
with:
push: 'origin stackblitz --force'

View file

@ -1,21 +1,11 @@
<p align="center">
<a href="https://hydrogen.shopify.dev">
<img src="./docs/images/HydrogenLogo.png"/>
</a>
</p>
![Hydrogen logo](/docs/images/HydrogenLogo.png)
<div align="center">
📚 [Docs](https://shopify.dev/custom-storefronts/hydrogen) | 🗣 [Discord](https://discord.gg/Hefq6w5c5d) | 💬 [Discussions](https://github.com/Shopify/hydrogen/discussions) | 📝 [Changelog](./packages/hydrogen/CHANGELOG.md)
</div>
📚 [Docs](https://shopify.dev/custom-storefronts/hydrogen) | 🗣 [Discord](https://discord.gg/Hefq6w5c5d) | 💬 [Discussions](https://github.com/Shopify/hydrogen/discussions) | 📝 [Changelog](./packages/hydrogen/CHANGELOG.md)
Hydrogen is a **React-based framework** for building dynamic, **Shopify-powered** custom storefronts.
Spin up a Hydrogen app in your browser with our [playground](https://hydrogen.new/) or set up your local environment with the instructions below ⬇️
**This is a developer preview of Hydrogen**. The documentation will be updated as Shopify [introduces new features and refines existing functionality](https://github.com/Shopify/hydrogen/releases). Production launches of Hydrogen custom storefronts aren't yet supported as Shopify is evolving the Hydrogen framework.
## Getting Started
**Requirements:**
@ -73,3 +63,4 @@ Learn more about [getting started with Hydrogen](https://shopify.dev/custom-stor
- [`@shopify/hydrogen-plugin-sanity`](https://www.npmjs.com/package/@shopify/hydrogen-plugin-sanity)
- [`create-hydrogen-app`](https://www.npmjs.com/package/create-hydrogen-app)
- [`eslint-plugin-hydrogen`](https://www.npmjs.com/package/eslint-plugin-hydrogen)
- [`@shopify/create-hydrogen` (deprecated)](https://www.npmjs.com/package/@shopify/create-hydrogen)

View file

@ -182,16 +182,3 @@ When finished, push up your changes.
Next, visit the Shipit page for Hydrogen and click **Deploy**.
After Shipit has released your version, visit the [releases page on GitHub](https://github.com/Shopify/hydrogen/releases), click on the version number you just released, and select "Create release from tag." Then, select "Auto-generate release notes." At this point, edit the release notes as you see fit (e.g. call out any breaking changes or upgrade guides). Finally, click "Publish release."
## Testing changes in another project
From the root of the repo, run:
```bash
yarn tophat ../PATH/TO/PROJECT --packages [...PACKAGES_LIST]
# example
yarn tophat ../cartogram/hydrogen-shop --packages cli hydrogen eslint-plugin
```

View file

@ -3,7 +3,7 @@
"packages": [
"packages/*"
],
"version": "0.6.4",
"version": "0.5.8",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {}

View file

@ -12,7 +12,7 @@
},
"scripts": {
"dev-lib": "yarn workspace @shopify/hydrogen dev",
"dev-server": "LOCAL_DEV=true VITE_INSPECT=1 yarn workspace dev dev",
"dev-server": "VITE_INSPECT=1 yarn workspace dev dev",
"build": "run-s build-lib build-dev build-cli",
"build-lib": "yarn workspace @shopify/hydrogen build",
"build-cli": "yarn workspace @shopify/hydrogen-cli build",
@ -30,8 +30,7 @@
"h2": "./packages/cli/bin/hydrogen",
"update-docs-on-version": "ts-node --project ./tsconfig.json ./scripts/update-docs-on-version.ts",
"generate-docs": "ts-node --project ./tsconfig.json ./scripts/generate-docs.ts",
"generate-docs:debug": "node --inspect-brk ./node_modules/.bin/ts-node --project ./tsconfig.json ./scripts/generate-docs.ts",
"tophat": "node ./scripts/tophat"
"generate-docs:debug": "node --inspect-brk ./node_modules/.bin/ts-node --project ./tsconfig.json ./scripts/generate-docs.ts"
},
"devDependencies": {
"@shopify/prettier-config": "^1.1.2",
@ -65,10 +64,8 @@
"ts-jest": "^26.5.4",
"ts-node": "^10.2.1",
"typescript": "^4.2.3",
"vite": "^2.6.14",
"yorkie": "^2.0.0",
"glob": "^7.2.0",
"shelljs": "^0.8.4"
"vite": "^2.6.0",
"yorkie": "^2.0.0"
},
"gitHooks": {
"pre-commit": "lint-staged",
@ -93,4 +90,4 @@
"unified": "9.2.2"
},
"version": "0.0.0"
}
}

View file

@ -5,19 +5,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
<!-- ## Unreleased -->
## Unreleased
## 0.6.4 - 2021-11-11
- No updates. Transitive dependency bump.
## 0.6.2 - 2021-11-10
- Add ability to explicitly set the root for the running command using the `--root` flag
## 0.6.0 - 2021-11-05
- Add create page command [#810](https://github.com/Shopify/hydrogen/pull/810)
- Add create component command [#806](https://github.com/Shopify/hydrogen/pull/806)
- Add init command [#791](https://github.com/Shopify/hydrogen/pull/791)

View file

@ -1,4 +1,4 @@
<!-- This file is generated from source code in the Shopify/hydrogen repo. Edit the files in /packages/cli and run 'yarn generate-docs' at the root of this repo. For more information, refer to https://github.com/Shopify/shopify-dev/blob/master/content/internal/operations/hydrogen-reference-docs.md. -->
<!-- This file is generated from the source code. Edit the files in /packages/cli and run 'yarn generate-docs' at the root of this repo. -->
## `@shopify/hydrogen-cli`

View file

@ -1,6 +1,6 @@
{
"name": "@shopify/hydrogen-cli",
"version": "0.6.4",
"version": "0.5.8",
"description": "Command line interface for hydrogen",
"license": "MIT",
"engines": {
@ -52,7 +52,7 @@
"playwright-chromium": "^1.13.0",
"sirv": "^1.0.14",
"typescript": "^4.2.3",
"vite": "^2.6.14"
"vite": "^2.6.0"
},
"files": [
"dist",

View file

@ -99,7 +99,7 @@ export async function app(env: Env<{name: string}>) {
workspace.install('react-dom', {version: '18.0.0-alpha-e6be2d531'});
workspace.install('react-router-dom', {version: '^5.2.0'});
workspace.install('@shopify/hydrogen');
workspace.install('vite', {dev: true, version: '^2.6.14'});
workspace.install('vite', {dev: true, version: '^2.6.0'});
workspace.install('@vitejs/plugin-react-refresh', {
dev: true,
version: '^1.3.2',

View file

@ -7,7 +7,7 @@ export default function ({ifFeature}: TemplateOptions) {
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
${ifFeature(

View file

@ -1,12 +1,19 @@
import {Env, ComponentType} from '../../../types';
import {componentName, validComponentName} from '../../../utilities';
import {pascalCase} from 'change-case';
import {Env} from '../../../types';
export enum ComponentType {
Client = 'React client component',
Shared = 'React shared component',
Server = 'React server component',
}
/**
* Scaffold a new React component.
*/
export async function component(env: Env) {
const {ui, fs, workspace} = env;
const name = await ui.ask('What do you want to name this component?', {
validate: validComponentName,
validate: validateComponentName,
default: 'ProductCard',
name: 'name',
});
@ -34,3 +41,29 @@ export async function component(env: Env) {
})
);
}
function validateComponentName(name: string) {
const suggested = pascalCase(name);
if (name === suggested) {
return true;
}
return `Invalid component name. Try ${suggested} instead.`;
}
function getReactComponentTypeSuffix(component: ComponentType) {
switch (component) {
case ComponentType.Client:
return 'client';
case ComponentType.Server:
return 'server';
default:
return null;
}
}
function componentName(name: string, type: ComponentType, extension: string) {
return [name, getReactComponentTypeSuffix(type), extension]
.filter((fp) => fp)
.join('.');
}

View file

@ -1 +0,0 @@
export {page as default} from './page';

View file

@ -1,44 +0,0 @@
import {Env, ComponentType} from '../../../types';
import {componentName, validComponentName} from '../../../utilities';
const PAGES_DIRECTORY = 'src/pages';
/**
* Scaffold a new Hydrogen page.
*/
export async function page(env: Env) {
const {ui, fs, workspace} = env;
const name = await ui.ask('What do you want to name this page?', {
validate: validComponentName,
default: 'Products',
name: 'name',
});
const url = await ui.ask('What is the url path to this page?', {
default: '/products',
name: 'name',
});
const urlSegments = url.split('/');
const lastUrlSegment = urlSegments.pop() || '';
const extension = (await workspace.isTypeScript) ? 'tsx' : 'jsx';
const path = fs.join(
workspace.root(),
PAGES_DIRECTORY,
...urlSegments,
componentName(lastUrlSegment, ComponentType.Server, extension)
);
fs.write(
path,
(await import('./templates/page-jsx')).default({
name,
path: fs.join(
PAGES_DIRECTORY,
componentName(url, ComponentType.Server, extension)
),
})
);
}

View file

@ -1,9 +0,0 @@
export default function ({name, path}: {name: string; path: string}) {
return `
export default function ${name}({request, response, ...serverState}) {
return (
<div>${name} component at \`${path}\`</div>
);
}
`;
}

View file

@ -1,59 +0,0 @@
import {withCli} from '../../../../testing';
describe('page', () => {
it('scaffolds a basic JSX page with a name', async () => {
await withCli(async ({run, fs}) => {
await run('create page', {
name: 'Products',
url: 'products',
});
expect(await fs.read('src/pages/products.server.jsx')).toBe(
`export default function Products({request, response, ...serverState}) {
return (
<div>Products component at \`src/pages/products.server.jsx\`</div>
);
}
`
);
});
});
it('scaffolds a basic TSX page with a name when a tsconfig exists', async () => {
await withCli(async ({run, fs}) => {
await fs.write('tsconfig.json', JSON.stringify({}, null, 2));
await run('create page', {
name: 'Collections',
url: 'collections',
});
expect(await fs.read('src/pages/collections.server.tsx')).toBe(
`export default function Collections({request, response, ...serverState}) {
return (
<div>Collections component at \`src/pages/collections.server.tsx\`</div>
);
}
`
);
});
});
it('supports nested and dynamic components', async () => {
await withCli(async ({run, fs}) => {
await fs.write('tsconfig.json', JSON.stringify({}, null, 2));
await run('create page', {
name: 'ProductDetails',
url: 'products/[handle]',
});
expect(await fs.read('src/pages/products/[handle].server.tsx')).toBe(
`export default function ProductDetails({request, response, ...serverState}) {
return (
<div>ProductDetails component at \`src/pages/products/[handle].server.tsx\`</div>
);
}
`
);
});
});
});

View file

@ -13,7 +13,7 @@ export enum Template {
* Create a new `@shopify/hydrogen` app.
*/
export async function init(env: Env) {
const {ui, fs, workspace, ...passThroughEnv} = env;
const {ui, fs, workspace} = env;
const name = await ui.ask('What do you want to name this app?', {
validate: validateProjectName,
@ -43,8 +43,8 @@ export async function init(env: Env) {
);
if (template === Template.None) {
const context = {name, ...passThroughEnv.context};
await app({...passThroughEnv, ui, fs, workspace, context});
const context = {name};
await app({ui, fs, workspace, context});
}
if (template === Template.Default) {

View file

@ -4,14 +4,9 @@ import {Cli} from './ui';
import {Workspace} from './workspace';
import {Fs} from './fs';
import {loadConfig} from './config';
import {Env} from './types';
const logger = debug('hydrogen');
interface ModuleType {
default: (env: Env) => Promise<void>;
}
(async () => {
const rawInputs = process.argv.slice(2);
const {command, root} = parseCliArguments(rawInputs);
@ -27,23 +22,10 @@ interface ModuleType {
throw new InputError();
}
async function runModule(module: ModuleType) {
logger('Run start');
await module.default({
ui,
fs,
workspace,
logger: debug(`hydrogen/${command}`),
});
logger('Run end');
}
try {
const module = await import(`./commands/${command}`);
await runModule(module);
await module.default({ui, fs, workspace});
} catch (error) {
logger(error);
@ -54,9 +36,7 @@ interface ModuleType {
ui.printFile(file);
}
if (command === 'init') {
await workspace.commit();
}
await workspace.commit();
})().catch((error) => {
logger(error);
process.exitCode = 1;

View file

@ -24,7 +24,7 @@ import getPort from 'get-port';
const INPUT_TIMEOUT = 500;
const execPromise = promisify(exec);
type Command = 'create' | 'create component' | 'create page';
type Command = 'create' | 'create component';
type Input = Record<string, string | boolean | null>;
interface App {

View file

@ -1,4 +1,3 @@
import debug from 'debug';
import {Workspace} from './workspace';
import {Ui} from './ui';
import {Fs} from './fs';
@ -10,13 +9,6 @@ export interface Env<Context = {}> {
workspace: Workspace;
fs: Fs;
context?: Context;
logger: debug.Debugger;
}
export enum ComponentType {
Client = 'React client component',
Shared = 'React shared component',
Server = 'React server component',
}
export interface TemplateOptions {

View file

@ -1,11 +1,10 @@
import {resolve} from 'path';
import {join, resolve} from 'path';
import minimist from 'minimist';
export * from './error';
export * from './feature';
export {merge} from './merge';
export {componentName, validComponentName} from './react';
const DEFAULT_SUBCOMMANDS = {
create: 'app',
@ -15,8 +14,10 @@ const DEFAULT_SUBCOMMANDS = {
export function parseCliArguments(rawInputs?: string[]) {
const inputs = minimist(rawInputs || []);
const command = inputs._[0];
const root = inputs.root || process.cwd();
const root =
command === 'create' && inputs._[2]
? join(process.cwd(), inputs._[2])
: process.cwd();
const subcommand =
inputs._[1] || DEFAULT_SUBCOMMANDS[command as 'create' | 'version'];
const {debug} = inputs;

View file

@ -1,32 +0,0 @@
import {pascalCase} from 'change-case';
import {ComponentType} from '../types';
function getReactComponentTypeSuffix(component: ComponentType) {
switch (component) {
case ComponentType.Client:
return 'client';
case ComponentType.Server:
return 'server';
default:
return null;
}
}
export function componentName(
name: string,
type: ComponentType,
extension: string
) {
return [name, getReactComponentTypeSuffix(type), extension]
.filter((fp) => fp)
.join('.');
}
export function validComponentName(name: string) {
const suggested = pascalCase(name);
if (name === suggested) {
return true;
}
return `Invalid component name. Try ${suggested} instead.`;
}

View file

@ -7,30 +7,6 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
<!-- ## Unreleased -->
## 0.6.4 - 2021-11-11
- No updates. Transitive dependency bump.
## 0.6.3 - 2021-11-10
- No updates. Transitive dependency bump.
## 0.6.2 - 2021-11-10
- Wrap instances of `<Money>` in client components due to render prop usage
- Eliminate use of `Link` client re-exports
- fix: gallery safari grid layout
- fix: Move `LocalizationProvider` to `Layout.server` to prevent issues with React Router & Suspense
## 0.6.1 - 2021-11-08
- Wrap LocalizationProvider in a proper Suspense boundary
- Optimize `@headlessui/react` to prevent dev server slowness while we investigate a long term solution
## 0.6.0 - 2021-11-05
- No updates. Transitive dependency bump.
## 0.5.8 - 2021-11-04
- No updates. Transitive dependency bump.

View file

@ -4,7 +4,7 @@
"access": "public",
"@shopify:registry": "https://registry.npmjs.org"
},
"version": "0.6.4",
"version": "0.5.8",
"main": "index.js",
"license": "MIT",
"bin": {

View file

@ -12,7 +12,7 @@ const {copy} = require('./utils');
const devPath = path.resolve(__dirname, '..', '..', 'dev');
const templatePath = path.resolve(__dirname, '..', './template-hydrogen');
const skipFiles = ['node_modules', 'dist', '.stackblitzrc'];
const skipFiles = ['node_modules', 'dist'];
// Remove the symlink and replace it with a folder
fs.unlinkSync(templatePath);

View file

@ -1,4 +0,0 @@
{
"installDependencies": true,
"startCommand": "npm run dev"
}

View file

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hydrogen App</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />

View file

@ -1,11 +1,11 @@
{
"name": "dev",
"description": "This is a dev environment for Hydrogen",
"version": "0.6.4",
"version": "0.5.8",
"license": "MIT",
"private": true,
"scripts": {
"dev": "vite",
"dev": "LOCAL_DEV=true vite",
"lint": "npm-run-all lint:*",
"lint:js": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx src",
"lint:css": "stylelint ./src/**/*.{css,sass,scss}",
@ -28,11 +28,11 @@
"prettier": "^2.3.2",
"stylelint": "^13.13.0",
"tailwindcss": "^2.2.4",
"vite": "^2.6.14"
"vite": "^2.6.0"
},
"dependencies": {
"@headlessui/react": "^1.4.1",
"@shopify/hydrogen": "^0.6.4",
"@shopify/hydrogen": "^0.5.8",
"compression": "^1.7.4",
"express": "^4.17.1",
"graphql-tag": "^2.12.4",

View file

@ -1,4 +1,8 @@
import {ShopifyServerProvider, DefaultRoutes} from '@shopify/hydrogen';
import {
ShopifyServerProvider,
DefaultRoutes,
LocalizationProvider,
} from '@shopify/hydrogen';
import {Switch} from 'react-router-dom';
import {Suspense} from 'react';
@ -13,19 +17,21 @@ export default function App({...serverState}) {
const pages = import.meta.globEager('./pages/**/*.server.[jt]sx');
return (
<Suspense fallback={<LoadingFallback />}>
<ShopifyServerProvider shopifyConfig={shopifyConfig} {...serverState}>
<ShopifyServerProvider shopifyConfig={shopifyConfig} {...serverState}>
<LocalizationProvider>
<CartProvider>
<DefaultSeo />
<Switch>
<DefaultRoutes
pages={pages}
serverState={serverState}
fallback={<NotFound />}
/>
</Switch>
<Suspense fallback={<LoadingFallback />}>
<DefaultSeo />
<Switch>
<DefaultRoutes
pages={pages}
serverState={serverState}
fallback={<NotFound />}
/>
</Switch>
</Suspense>
</CartProvider>
</ShopifyServerProvider>
</Suspense>
</LocalizationProvider>
</ShopifyServerProvider>
);
}

View file

@ -1,7 +1,7 @@
import {Link} from '@shopify/hydrogen/client';
const DEFAULT_CLASSES =
'block m-0 w-full items-center justify-center uppercase font-medium text-center px-6 py-4 rounded disabled:border-gray-300 disabled:bg-gray-300 disabled:cursor-not-allowed';
'block m-0 w-full items-center justify-center font-medium text-center px-6 py-4 rounded disabled:border-gray-300 disabled:bg-gray-300 disabled:cursor-not-allowed';
const VARIANT_CLASSES = {
primary: 'text-white bg-gray-900 hover:bg-gray-800 active:bg-gray-700',

View file

@ -11,7 +11,7 @@ import {Dialog} from '@headlessui/react';
import {useCartUI} from './CartUIProvider.client';
import CartIconWithItems from './CartIconWithItems.client';
import {BUTTON_PRIMARY_CLASSES} from './Button.client';
import Button, {BUTTON_PRIMARY_CLASSES} from './Button.client';
export default function Cart() {
const {isCartOpen, closeCart} = useCartUI();
@ -19,9 +19,9 @@ export default function Cart() {
return (
<Dialog open={isCartOpen} onClose={closeCart}>
<Dialog.Overlay className="fixed z-20 inset-0 bg-gray-50 opacity-75" />
<Dialog.Overlay className="fixed z-20 inset-0 bg-black opacity-75" />
<div
className={`absolute flex flex-col md:block z-20 top-0 left-0 right-0 bottom-0 md:top-7 h-full md:left-auto md:right-7 md:bottom-auto md:h-auto md:max-h-[calc(100vh-56px)] bg-gray-50 w-full md:w-[470px] rounded-b-lg shadow-2xl ${
className={`absolute flex flex-col md:block z-20 top-0 left-0 right-0 bottom-0 md:top-7 h-full md:left-auto md:right-7 md:bottom-auto md:h-auto md:max-h-[calc(100vh-56px)] bg-white w-full md:w-[470px] ${
itemCount === 0 ? 'overflow-hidden' : 'overflow-y-scroll'
}`}
>
@ -42,14 +42,11 @@ export default function Cart() {
function CartHeader() {
const {closeCart} = useCartUI();
return (
<header className="border-b border-gray-300 bg-white py-3 px-6 flex justify-between items-center sticky top-0">
<header className="border-b-2 border-black py-3 px-6 flex justify-between items-center bg-white sticky top-0">
<button type="button" onClick={closeCart}>
<ArrowIcon />
<span className="sr-only">Close cart</span>
</button>
<span className="text-xs text-gray-500">
Free shipping on orders over $50
</span>
<span className="text-xs">Free shipping on orders over $50</span>
<CartIconWithItems />
</header>
);
@ -67,12 +64,12 @@ function CartItems() {
{({merchandise}) => (
<div
role="row"
className="flex py-7 border-b last:border-b-0 border-gray-300 text-gray-900"
className="flex py-7 border-b-2 last:border-b-0 border-black"
>
<div role="cell" className="flex-shrink-0 mr-7">
<Link to={`products/${merchandise.product.handle}`}>
<CartLine.Image
className="bg-white border border-black border-opacity-5 rounded-xl "
className="bg-white border-2 border-black"
options={{width: 98, height: 98, crop: 'center'}}
/>
</Link>
@ -110,7 +107,6 @@ function CartItems() {
<CartLine.QuantityAdjustButton
adjust="remove"
aria-label="Remove from cart"
className="disabled:pointer-events-all disabled:cursor-wait"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -136,15 +132,15 @@ function CartItems() {
function CartItemQuantity() {
return (
<div className="flex border rounded border-gray-300 items-center overflow-auto mt-2">
<div className="flex border border-gray-300 items-center overflow-auto mt-2">
<CartLine.QuantityAdjustButton
adjust="decrease"
className="p-2"
aria-label="Decrease quantity"
className="p-2 disabled:pointer-events-all disabled:cursor-wait"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-gray-400"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
@ -161,8 +157,8 @@ function CartItemQuantity() {
/>
<CartLine.QuantityAdjustButton
adjust="increase"
className="p-2"
aria-label="Increase quantity"
className="p-2 text-gray-400 disabled:pointer-events-all disabled:cursor-wait"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -183,29 +179,27 @@ function CartItemQuantity() {
function CartFooter() {
return (
<footer className="bottom-0 sticky pb-8 border-t border-black border-opacity-5">
<div className="relative h-60 bg-white text-gray-900 p-7">
<footer className="bottom-0 sticky">
<div className="relative h-60 bg-white p-7 border-t-2 border-black">
<div role="table" aria-label="Cost summary">
<div role="row" className="flex justify-between">
<span className="font-semibold" role="rowheader">
Subtotal
</span>
<div role="row" className="flex justify-between font-medium text-lg">
<span role="rowheader">Subtotal</span>
<CartEstimatedCost
amountType="subtotal"
role="cell"
className="text-right "
className="text-right"
/>
</div>
<div role="row" className="flex justify-between mt-2">
<span className="font-semibold" role="rowheader">
Shipping
</span>
<div role="row" className="flex justify-between text-xs mt-2">
<span role="rowheader">Shipping</span>
<span role="cell" className="uppercase">
Free
</span>
</div>
</div>
<CartShopPayButton className="flex my-4 justify-center w-full bg-[#5a31f4] py-2 rounded-md" />
<CartShopPayButton
className={`${BUTTON_PRIMARY_CLASSES} flex py-1 mt-6 mb-2 justify-center bg-[#5a31f4] hover:bg-[#5a31f4]`}
/>
<CartCheckoutButton className={BUTTON_PRIMARY_CLASSES}>
Checkout
</CartCheckoutButton>
@ -218,16 +212,8 @@ function CartEmpty() {
const {closeCart} = useCartUI();
return (
<div className="p-7 flex flex-col">
<p className="mb-4 text-lg text-gray-500 text-center">
Your cart is empty
</p>
<button
type="button"
onClick={closeCart}
className={BUTTON_PRIMARY_CLASSES}
>
Continue Shopping
</button>
<p className="mb-4 text-lg">Your cart is empty</p>
<Button handleClick={closeCart} label="Continue Shopping" />
</div>
);
}

View file

@ -1,7 +1,6 @@
export default function CartIcon() {
return (
<svg
aria-hidden="true"
width="40"
height="40"
viewBox="0 0 40 40"

View file

@ -11,10 +11,9 @@ export default function CartIconWithItems() {
<CartIcon />
<div
className={`bg-blue-700 text-xs rounded-full leading-none text-white absolute bottom-3 right-1 flex items-center justify-center transform translate-y-1/2 transition-all ${
className={`bg-blue-600 text-xs rounded-full leading-none text-white absolute bottom-3 right-1 flex items-center justify-center transform translate-y-1/2 transition-all ${
itemCount > 0 ? 'h-4 w-4' : 'h-0 w-0 overflow-hidden'
}`}
aria-hidden
>
{itemCount > 0 ? itemCount : null}
</div>

View file

@ -21,7 +21,6 @@ export default function CartToggle({handleClick}) {
}}
>
<CartIconWithItems />
<span className="sr-only">Open cart</span>
</button>
);
}

View file

@ -0,0 +1,50 @@
import {Image} from '@shopify/hydrogen/client';
import Button from './Button.client';
export default function CollectionHero({collection}) {
return collection ? (
<section className="relative w-full border-b-2 border-black mb-14">
<div className="absolute h-full w-full grid grid-cols-1 lg:grid-cols-2">
<div className="w-full h-full md:min-h-96 lg:order-first lg:h-auto">
<div className="w-full h-full bg-gradient-to-bl from-purple-600 via-blue-500 to-green-300" />
</div>
<div className="order-first overflow-hidden border-b-2 lg:border-b-0 lg:border-l-2 border-black">
<Image
className="object-cover bg-center h-full w-full"
image={collection.image}
options={{
height: collection.image.height,
width: collection.image.width,
}}
/>
</div>
</div>
<div className="h-full mx-auto max-w-7xl">
<div className="h-full px-4 pt-80 md:px-8 pb-16 md:pt-96 lg:pt-28 lg:pb-32">
<div className="flex justify-center flex-col h-full md:pt-48 lg:pt-0">
<div className="w-full lg:max-w-[808px] border-2 border-black bg-white p-7 md:p-14 drop-shadow-lg">
<p className="uppercase font-mono font-bold text-sm pb-4">
The Latest
</p>
<h2 className="font-black text-4xl md:text-5xl pb-4">
New Year.
<span className="text-purple-600 ml-4">New Drop.</span>
</h2>
<p className="text-xl md:text-2xl">
Get the freshest styles from this season&apos;s collection. Our
most chill collection yet.
</p>
</div>
<Button
className="md:self-start mt-4 md:mt-8 bg-opacity-50 hover:bg-opacity-50 active:bg-opacity-50"
label={`Shop ${collection.title.toLowerCase()}`}
url={`collections/${collection.handle}`}
variant="secondary"
/>
</div>
</div>
</div>
</section>
) : null;
}

View file

@ -18,89 +18,57 @@ export default function CurrencySelector() {
return (
<div className="hidden lg:block">
<Listbox onChange={setCountry}>
{({open}) => (
<>
<Listbox.Button className="font-medium text-sm h-8 p-2 flex items-center">
<span className="mr-4">{selectedCountry.currency.isoCode}</span>
<ArrowIcon isOpen={open} />
</Listbox.Button>
<Listbox.Button className="font-medium text-sm h-8 p-2 w-16">
{selectedCountry.currency.isoCode}
</Listbox.Button>
<Listbox.Options className="absolute z-10 mt-2">
<div className="bg-white p-4 rounded-lg drop-shadow-2xl">
<Listbox.Option
disabled
className="p-2 text-md text-left font-medium uppercase"
>
Currency
</Listbox.Option>
{countries.map((country) => {
const isSelected =
country.isoCode === selectedCountry.isoCode;
return (
<Listbox.Option
key={country.isoCode}
value={country.isoCode}
<Listbox.Options className="absolute z-10 mt-2 border-t-4 border-blue-600">
<div className="bg-white w-28 border-2 border-black">
<Listbox.Option
disabled
className="p-2 text-sm text-left font-bold"
>
Currency
</Listbox.Option>
{countries.map((country) => {
const isSelected = country.isoCode === selectedCountry.isoCode;
return (
<Listbox.Option key={country.isoCode} value={country.isoCode}>
{({active}) => (
<div
className={`p-2 flex justify-between items-center text-left w-full cursor-pointer ${
isSelected ? 'font-medium' : null
} ${active ? 'bg-gray-200' : null}`}
>
{({active}) => (
<div
className={`w-36 py-2 px-3 flex justify-between items-center text-left cursor-pointer rounded
${active ? 'bg-gray-200' : null}`}
>
{country.currency.isoCode}
{isSelected ? <CheckIcon /> : null}
</div>
)}
</Listbox.Option>
);
})}
</div>
</Listbox.Options>
</>
)}
{country.currency.isoCode}
{isSelected ? <CheckIcon /> : null}
</div>
)}
</Listbox.Option>
);
})}
</div>
</Listbox.Options>
</Listbox>
</div>
);
}
export function CheckIcon() {
function CheckIcon() {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M7 10L9 12L13 8M19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10Z"
stroke="#354CF6"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function ArrowIcon({isOpen}) {
return (
<svg
className={`transition-transform duration-300 ${
isOpen ? 'rotate-180' : null
}`}
aria-hidden="true"
width="10"
height="6"
viewBox="0 0 10 6"
className="mr-2"
width="14"
height="10"
viewBox="0 0 14 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.292893 0.292893C0.683416 -0.097631 1.31658 -0.097631 1.7071 0.292893L4.99999 3.58579L8.29288 0.292893C8.6834 -0.0976311 9.31657 -0.0976311 9.70709 0.292893C10.0976 0.683417 10.0976 1.31658 9.70709 1.70711L5.7071 5.70711C5.31657 6.09763 4.68341 6.09763 4.29289 5.70711L0.292893 1.70711C-0.0976309 1.31658 -0.0976309 0.683417 0.292893 0.292893Z"
fill="#374151"
d="M13.7071 0.292893C14.0976 0.683417 14.0976 1.31658 13.7071 1.70711L5.70711 9.70711C5.31658 10.0976 4.68342 10.0976 4.29289 9.70711L0.292893 5.70711C-0.0976311 5.31658 -0.0976311 4.68342 0.292893 4.29289C0.683417 3.90237 1.31658 3.90237 1.70711 4.29289L5 7.58579L12.2929 0.292893C12.6834 -0.0976311 13.3166 -0.0976311 13.7071 0.292893Z"
fill="#354CF6"
/>
</svg>
);

View file

@ -1,22 +1,30 @@
import {Image, Link} from '@shopify/hydrogen/client';
import {Image, RawHtml} from '@shopify/hydrogen/client';
import StyledLink from './StyledLink';
export default function FeaturedCollection({collection}) {
return collection ? (
<div className="shadow-xl rounded-xl grid grid-cols-1 lg:grid-cols-2 items-center bg-white overflow-hidden">
{collection.image ? (
<Image width="622" height="465" image={collection.image} />
) : null}
<div className="px-10 py-10 lg:py-0">
<h2 className="text-gray-700 text-3xl font-bold mb-5">
<div className="mb-14 grid grid-cols-1 lg:grid-cols-2">
<Image
width="1024"
image={collection.image}
className="border-2 border-black"
/>
<div className="py-14 px-0 lg:pl-8 lg:pr-14">
<span className="text-small text-black uppercase tracking-wider font-bold font-mono">
Featured Collection
</span>
<h2 className="text-4xl md:text-5xl text-black font-black my-4">
{collection.title}
</h2>
<p className="text-xl text-gray-500 mb-6">{collection.description}</p>
<Link
to={`/collections/${collection.handle}`}
className="inline-block bg-gray-900 text-white text-lg font-medium rounded-md py-4 px-16 uppercase"
>
Shop Collection
</Link>
<RawHtml
className="text-xl md:text-2xl text-black leading-relaxed mb-4"
string={collection.descriptionHtml}
/>
<StyledLink
url={`/collections/${collection.handle}`}
value="Shop Collection"
/>
</div>
</div>
) : null;

View file

@ -3,11 +3,13 @@ import {Link} from '@shopify/hydrogen';
export default function Footer({collection, product}) {
return (
<footer role="contentinfo">
<div className="relative bg-white border-t border-b border-black border-opacity-5">
<div className="border-t-2 border-b-2 border-black">
<div className="mx-auto max-w-7xl px-4 py-14 md:px-8 grid grid-cols-1 md:grid-cols-3 gap-12">
{/* eslint-disable-next-line @shopify/jsx-prefer-fragment-wrappers */}
<div>
<h2 className="text-md font-medium uppercase mb-4">Community</h2>
<h5 className="text-black text-sm font-mono font-bold uppercase">
Community
</h5>
<ul className="mt-8 space-y-4">
<li className="text-sm font-medium text-gray-600 hover:text-gray-900">
<a
@ -59,7 +61,9 @@ export default function Footer({collection, product}) {
</div>
{/* eslint-disable-next-line @shopify/jsx-prefer-fragment-wrappers */}
<div>
<h2 className="text-md font-medium uppercase mb-4">Templates</h2>
<h5 className="text-black text-sm font-mono font-bold uppercase">
Templates
</h5>
<ul className="mt-8 space-y-4">
<li className="flex items-center text-sm font-medium text-gray-600 hover:text-gray-900">
<Link to="/home">Home</Link>
@ -68,15 +72,15 @@ export default function Footer({collection, product}) {
<Link to={`/products/${product?.handle}`}>Product</Link>
</li>
<li className="flex items-center text-sm font-medium text-gray-600 hover:text-gray-900">
<Link to={`/collections/${collection?.handle}`}>
Collection
</Link>
<Link to={`/collection/${collection?.handle}`}>Collection</Link>
</li>
</ul>
</div>
{/* eslint-disable-next-line @shopify/jsx-prefer-fragment-wrappers */}
<div>
<h2 className="text-md font-medium uppercase mb-4">Docs</h2>
<h5 className="text-black text-sm font-mono font-bold uppercase">
Docs
</h5>
<ul className="mt-8 space-y-4">
<li className="flex items-center text-sm font-medium text-gray-600 hover:text-gray-900">
<a href="https://shopify.dev/custom-storefronts/hydrogen">
@ -92,7 +96,7 @@ export default function Footer({collection, product}) {
</div>
</div>
</div>
<div className="py-6 px-4 md:px-8 bg-gray-50">
<div className="py-6 px-4 md:px-8">
<p className="text-gray-600">© 2021 Shopify</p>
</div>
</footer>

View file

@ -10,7 +10,7 @@ export default function Gallery() {
const featuredMedia = selectedVariant.image || media[0].image;
const featuredMediaSrc = featuredMedia.url.split('?')[0];
const galleryMedia = media.filter((med) => {
if (med.mediaContentType === MODEL_3D_TYPE) {
if (med.mediaContentType === 'MODEL_3D') {
return true;
}
@ -26,14 +26,8 @@ export default function Gallery() {
className="gap-4 flex md:grid md:grid-cols-2 overflow-x-scroll no-scrollbar scroll-snap-x scroll-smooth h-[485px] md:h-auto place-content-start"
tabIndex="-1"
>
<SelectedVariantImage className="w-[80vw] md:w-full h-full md:h-auto object-cover object-center flex-shrink-0 md:flex-shrink-none snap-start md:col-span-2 border border-gray-200 rounded-lg" />
<SelectedVariantImage className="w-[80vw] md:w-full h-full object-cover object-center flex-shrink-0 md:flex-shrink-none snap-start md:col-span-2 border border-gray-200 rounded-lg" />
{galleryMedia.map((med) => {
let extraProps = {};
if (med.mediaContentType === MODEL_3D_TYPE) {
extraProps = MODEL_3D_PROPS;
}
return (
<MediaFile
tabIndex="0"
@ -44,15 +38,10 @@ export default function Gallery() {
height: '485',
crop: 'center',
}}
{...extraProps}
interactionPromptThreshold="0"
/>
);
})}
</div>
);
}
const MODEL_3D_TYPE = 'MODEL_3D';
const MODEL_3D_PROPS = {
interactionPromptThreshold: '0',
};

View file

@ -10,12 +10,8 @@ export default function Header({collections, storeName}) {
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
return (
<header className="h-20 lg:h-32" role="banner">
<div
className={`fixed z-20 h-20 lg:h-32 w-full border-b border-gray-200 px-6 md:px-8 md:py-6 lg:pt-8 lg:pb-0 mx-auto bg-white ${
isMobileNavOpen ? '' : 'bg-opacity-95'
}`}
>
<header className="h-20 lg:h-32">
<div className="fixed z-10 h-20 lg:h-32 w-full bg-white border-b-2 border-black px-6 md:px-8 md:py-6 lg:pt-8 lg:pb-0 mx-auto">
<div className="h-full flex lg:flex-col place-content-between">
<div className="text-center w-full flex justify-between items-center">
<CurrencySelector />
@ -25,7 +21,7 @@ export default function Header({collections, storeName}) {
setIsOpen={setIsMobileNavOpen}
/>
<Link
className="font-black uppercase text-3xl tracking-widest"
className="font-black uppercase text-2xl tracking-widest"
to="/"
>
{storeName}

View file

@ -1,9 +1,4 @@
import {
Image,
useShopQuery,
flattenConnection,
LocalizationProvider,
} from '@shopify/hydrogen';
import {useShopQuery, flattenConnection, Image} from '@shopify/hydrogen';
import gql from 'graphql-tag';
import Header from './Header.client';
@ -28,7 +23,7 @@ export default function Layout({children, hero}) {
const storeName = data ? data.shop.name : '';
return (
<LocalizationProvider>
<>
<div className="absolute top-0 left-0">
<a
href="#mainContent"
@ -37,7 +32,7 @@ export default function Layout({children, hero}) {
Skip to content
</a>
</div>
<div className="min-h-screen max-w-screen text-gray-700 font-sans">
<div className="min-h-screen max-w-screen text-gray-700">
<Header collections={collections} storeName={storeName} />
{/* eslint-disable-next-line @shopify/jsx-prefer-fragment-wrappers */}
<div>
@ -49,7 +44,7 @@ export default function Layout({children, hero}) {
/>
<Cart />
</div>
<main role="main" id="mainContent" className="relative bg-gray-50">
<main id="mainContent" className="relative bg-gray-50">
{hero}
<div className="mx-auto max-w-7xl p-4 md:py-5 md:px-8">
{children}
@ -57,7 +52,7 @@ export default function Layout({children, hero}) {
</main>
<Footer collection={collections[0]} product={products[0]} />
</div>
</LocalizationProvider>
</>
);
}

View file

@ -4,15 +4,15 @@ import OpenIcon from './OpenIcon';
export default function LoadingFallback() {
return (
<header className="h-20 lg:h-32 max-w-screen text-gray-700">
<div className="fixed z-10 h-20 lg:h-32 w-full bg-white/90 border-b border-black border-opacity-5 px-6 md:px-8 md:py-6 lg:pt-8 lg:pb-0 mx-auto">
<div className="fixed z-10 h-20 lg:h-32 w-full bg-white border-b-2 border-black px-6 md:px-8 md:py-6 lg:pt-8 lg:pb-0 mx-auto">
<div className="h-full flex lg:flex-col place-content-between">
<div className="text-center w-full flex justify-between items-center">
<div className="hidden lg:block w-16" />
<div className="lg:hidden flex justify-center items-center w-7 h-full">
<OpenIcon />
</div>
<p className="font-black uppercase text-3xl tracking-widest">
Snowdevil
<p className="font-black uppercase text-2xl tracking-widest">
Snowdevil Snowboards
</p>
<CartIcon />
</div>

View file

@ -0,0 +1,21 @@
export default function MediaPlaceholder({text}) {
return (
<div className="text-gray-500 h-full w-full p-4 bg-gray-200 flex flex-col items-center justify-center space-y-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-10 w-10"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{text && <p className="text-sm max-w-xs text-center">{text}</p>}
</div>
);
}

View file

@ -2,8 +2,6 @@ import {useCallback} from 'react';
import {useAvailableCountries, useCountry} from '@shopify/hydrogen/client';
import {Listbox} from '@headlessui/react';
import {ArrowIcon, CheckIcon} from './CurrencySelector.client';
export default function MobileCurrencySelector() {
const countries = useAvailableCountries();
const [selectedCountry, setSelectedCountry] = useCountry();
@ -18,18 +16,18 @@ export default function MobileCurrencySelector() {
);
return (
<div className="mt-8 rounded border border-gray-200 w-full">
<div className="mt-8 border-2 border-black w-full">
<Listbox onChange={setCountry}>
{({open}) => (
<>
<Listbox.Button className="w-full flex justify-between text-sm items-center py-5 px-7">
<Listbox.Button className="w-full flex justify-between items-center py-5 px-7">
{selectedCountry.currency.isoCode}
<ArrowIcon isOpen={open} />
{open ? <MinusIcon /> : <PlusIcon />}
</Listbox.Button>
<Listbox.Options className="w-full px-3 pb-2 text-lg">
<Listbox.Options className="w-full px-7 pb-2">
<Listbox.Option
disabled
className="font-medium px-4 pb-4 w-full text-left uppercase"
className="font-bold p-2 w-full text-left border-t-4 border-blue-600"
>
Currency
</Listbox.Option>
@ -39,9 +37,9 @@ export default function MobileCurrencySelector() {
<Listbox.Option key={country.isoCode} value={country.isoCode}>
{({active}) => (
<div
className={`py-2 px-4 rounded flex justify-between items-center text-left w-full cursor-pointer ${
active ? 'bg-gray-100' : null
}`}
className={`p-2 flex justify-between items-center text-left w-full cursor-pointer border-t border-gray-200 ${
isSelected ? 'font-medium' : null
} ${active ? 'bg-gray-100' : null}`}
>
{country.currency.isoCode}
{isSelected ? <CheckIcon /> : null}
@ -57,3 +55,64 @@ export default function MobileCurrencySelector() {
</div>
);
}
function PlusIcon() {
return (
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5 0C5.55228 0 6 0.447715 6 1V4L9 4C9.55228 4 10 4.44772 10 5C10 5.55228 9.55228 6 9 6H6V9C6 9.55229 5.55228 10 5 10C4.44771 10 4 9.55229 4 9V6H1C0.447715 6 0 5.55228 0 5C5.96046e-08 4.44771 0.447715 4 1 4L4 4V1C4 0.447715 4.44771 0 5 0Z"
fill="#111827"
/>
</svg>
);
}
function MinusIcon() {
return (
<svg
width="13"
height="2"
viewBox="0 0 13 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="1"
y1="1"
x2="12"
y2="1"
stroke="black"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
function CheckIcon() {
return (
<svg
className="mr-2"
width="14"
height="10"
viewBox="0 0 14 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.7071 0.292893C14.0976 0.683417 14.0976 1.31658 13.7071 1.70711L5.70711 9.70711C5.31658 10.0976 4.68342 10.0976 4.29289 9.70711L0.292893 5.70711C-0.0976311 5.31658 -0.0976311 4.68342 0.292893 4.29289C0.683417 3.90237 1.31658 3.90237 1.70711 4.29289L5 7.58579L12.2929 0.292893C12.6834 -0.0976311 13.3166 -0.0976311 13.7071 0.292893Z"
fill="#354CF6"
/>
</svg>
);
}

View file

@ -19,12 +19,12 @@ export default function MobileNavigation({collections, isOpen, setIsOpen}) {
{isOpen ? <CloseIcon /> : <OpenIcon />}
</button>
{isOpen ? (
<div className="absolute -left-0 top-20 w-full h-screen z-10 bg-gray-50 px-4 md:px-12 py-7">
<ul>
<div className="absolute -left-0 top-20 w-full h-screen z-10 bg-white px-4 md:px-12 py-7">
<ul className="font-medium">
{collections.map((collection) => (
<li className="border-b border-gray-200" key={collection.id}>
<li className="border-b-2 border-black" key={collection.id}>
<Link
className="group py-5 text-gray-700 flex items-center justify-between"
className="group p-5 flex items-center justify-between"
to={`/collections/${collection.handle}`}
onClick={() => setIsOpen(false)}
>

View file

@ -1,14 +0,0 @@
import {Money} from '@shopify/hydrogen/client';
export default function MoneyCompareAtPrice({money}) {
return (
<Money money={money}>
{({amount, currencyNarrowSymbol}) => (
<span className="line-through text-lg mr-2.5 text-gray-500">
{currencyNarrowSymbol}
{amount}
</span>
)}
</Money>
);
}

View file

@ -1,15 +0,0 @@
import {Money} from '@shopify/hydrogen/client';
export default function MoneyPrice({money}) {
return (
<Money className="text-black text-md" money={money}>
{({amount, currencyNarrowSymbol, currencyCode}) => (
<>
{currencyCode}
{currencyNarrowSymbol}
{amount}
</>
)}
</Money>
);
}

View file

@ -3,12 +3,12 @@ import {Link} from '@shopify/hydrogen/client';
export default function Navigation({collections}) {
return (
<nav className="hidden lg:block text-center">
<ul className="md:flex items-center justify-center">
<ul className="md:flex items-center justify-center space-x-6 font-medium">
{collections.map((collection) => (
<li key={collection.id}>
<Link
to={`/collections/${collection.handle}`}
className="block p-4 hover:opacity-80"
className="block py-4 hover:opacity-80"
>
{collection.title}
</Link>

View file

@ -1,7 +1,4 @@
import {Image, Link} from '@shopify/hydrogen';
import MoneyCompareAtPrice from './MoneyCompareAtPrice.client';
import MoneyPrice from './MoneyPrice.client';
import {Link, Image, Money} from '@shopify/hydrogen';
export default function ProductCard({product}) {
const selectedVariant = product.variants.edges[0].node;
@ -11,37 +8,47 @@ export default function ProductCard({product}) {
}
return (
<div className="text-md mb-4 relative">
<div className="text-lg mb-4 relative">
<Link to={`/products/${product.handle}`}>
<div className="rounded-lg border-2 border-gray-200 mb-2 relative flex items-center justify-center overflow-hidden object-cover h-96">
{selectedVariant.image ? (
<Image
className="bg-white absolute w-full h-full transition-all duration-500 ease-in-out transform bg-center bg-cover object-center object-contain hover:scale-110"
image={selectedVariant.image}
/>
) : null}
{!selectedVariant?.availableForSale && (
<div className="absolute top-3 left-3 rounded-3xl text-xs bg-black text-white py-3 px-4">
Out of stock
</div>
)}
{!selectedVariant?.availableForSale && (
<div className="absolute text-xs bg-black text-white p-3 z-50">
Out of stock
</div>
)}
<div className="border-2 border-black mb-2 relative flex items-center justify-center overflow-hidden object-cover h-96">
<Image
className="bg-white absolute w-full h-full transition-all duration-500 ease-in-out transform bg-center bg-cover object-center object-contain hover:scale-110"
image={selectedVariant.image}
/>
</div>
<span className="text-black font-semibold mb-0.5">{product.title}</span>
{product.vendor && (
<p className="text-gray-900 font-medium text-sm mb-0.5">
{product.vendor}
</p>
)}
<span className="text-black font-medium">{product.title}</span>
<div className="flex ">
{selectedVariant.compareAtPriceV2 && (
<MoneyCompareAtPrice money={selectedVariant.compareAtPriceV2} />
<Money money={selectedVariant.compareAtPriceV2}>
{({amount, currencyNarrowSymbol}) => (
<span className="line-through mr-2.5 text-gray-500">
{currencyNarrowSymbol}
{amount}
</span>
)}
</Money>
)}
<MoneyPrice money={selectedVariant.priceV2} />
<Money className="text-black" money={selectedVariant.priceV2}>
{({amount, currencyNarrowSymbol, currencyCode}) => (
<>
{currencyCode}
{currencyNarrowSymbol}
{amount}
</>
)}
</Money>
</div>
</Link>
{!selectedVariant.availableForSale && (
<p className="text-xs mt-2">Avaliable in 2-3 weeks</p>
)}
</div>
);
}

View file

@ -0,0 +1,72 @@
import {
Link,
SelectedVariantImage,
SelectedVariantPrice,
ProductProvider,
useProduct,
ProductTitle,
} from '@shopify/hydrogen/client';
import ProductVariantImageSelector from './ProductVariantImageSelector.client';
export default function ProductCardAdvanced({product}) {
return (
<ProductProvider
product={product}
initialVariantId={product.variants.edges[0].node.id}
>
<ProductCard />
</ProductProvider>
);
}
function ProductCard() {
const {handle, vendor, variants, selectedVariant} = useProduct();
return (
<div className="text-lg mb-4 relative">
<Link to={`/products/${handle}`}>
<div className="border-2 border-black mb-2 relative flex items-center justify-center overflow-hidden object-cover aspect-w-1 aspect-h-1">
<SelectedVariantImage className="bg-white absolute w-full h-full transition-all duration-500 ease-in-out transform bg-center bg-cover object-center object-contain hover:scale-110" />
</div>
{!selectedVariant.availableForSale && (
<div className="absolute top-0 left-0 text-xs bg-black text-white p-3">
Out of stock
</div>
)}
<ProductTitle as="span" className="text-black font-medium" />
{vendor && (
<p className="text-gray-900 text-sm font-medium">{vendor}</p>
)}
<div className="flex text-gray-500">
<SelectedVariantPrice priceType="compareAt">
{({amount, currencyNarrowSymbol}) => (
<span className="line-through mr-2.5">
{currencyNarrowSymbol}
{amount}
</span>
)}
</SelectedVariantPrice>
<SelectedVariantPrice
className={
selectedVariant.compareAtPriceV2 ? 'text-black' : 'text-gray-500'
}
>
{({amount, currencyNarrowSymbol, currencyCode}) => (
<>
{currencyCode}
{currencyNarrowSymbol}
{amount}
</>
)}
</SelectedVariantPrice>
</div>
</Link>
{variants.length > 1 && <ProductVariantImageSelector />}
{!selectedVariant.availableForSale && (
<p className="text-xs mt-2">Avaliable in 2-3 weeks</p>
)}
</div>
);
}

View file

@ -49,7 +49,7 @@ function AddToCartMarkup() {
<Product.SelectedVariant.BuyNowButton
className={BUTTON_SECONDARY_CLASSES}
>
Buy it now
Buy It Now
</Product.SelectedVariant.BuyNowButton>
)}
</div>
@ -219,7 +219,7 @@ export default function ProductDetails({product}) {
</div>
</div>
{/* Product Description */}
<Product.Description className="prose border-t border-gray-200 pt-6 text-black text-md" />
<Product.Description className="prose border-t border-gray-200 pt-6 text-black text-lg" />
<Product.Metafield namespace="my_fields" keyName="size_chart">
{({value}) => {
return value ? (

View file

@ -0,0 +1,70 @@
import {Image, useProduct, Link} from '@shopify/hydrogen/client';
export default function ProductVariantImageSelector() {
const {variants, setSelectedVariant, selectedVariant, handle} = useProduct();
const uniqueVariants = findUniqueImages(variants);
return (
uniqueVariants.length > 1 && (
<ul className="grid grid-cols-4 gap-2 mt-2">
{uniqueVariants.map((variant) => {
return (
<div key={variant.id}>
<label
className="block aspect-w-1 aspect-h-1 object-cover cursor-pointer"
htmlFor={variant.id}
>
<Image
image={variant.image}
className={`bg-white w-full h-full object-center object-contain border-2
${
variant.id === selectedVariant.id
? 'border-black'
: 'border-transparent'
}`}
/>
<input
type="radio"
className="sr-only"
id={variant.id}
value={variant.title}
name={variant.id}
checked={variant.id === selectedVariant.id}
onChange={() => {
setSelectedVariant(variant);
}}
/>
</label>
</div>
);
})}
<li className="flex place-items-center text-sm text-gray-900">
<Link to={`/products/${handle}`}>
{`+ ${variants.length - uniqueVariants.length}`}
</Link>
</li>
</ul>
)
);
}
/*
This filters any variants with a unique image for the thumbnail.
By default, variants without images will fall back to the default product image.
They are excluded in the thumbnails by comparing their URLs.
*/
function findUniqueImages(variants) {
const uniqueVariants = variants.reduce((acc, variant) => {
const image = variant.image.url;
if (acc.length >= 3) {
return acc;
} else if (acc.find((element) => element.image.url === image)) {
return acc;
} else {
return [...acc, variant];
}
}, []);
return uniqueVariants;
}

View file

@ -0,0 +1,10 @@
import {Link} from '@shopify/hydrogen/client';
export default function StyledLink({url, value}) {
return (
<Link className="text-lg font-bold text-blue-600 font-mono" to={url}>
<span className="underline">{value}</span>
<span>{' -->'}</span>
</Link>
);
}

View file

@ -1,177 +0,0 @@
import {useShopQuery, flattenConnection, Link} from '@shopify/hydrogen';
import gql from 'graphql-tag';
function ExternalIcon() {
return (
<svg
className="ml-3"
width="15"
height="14"
viewBox="0 0 15 14"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<path d="M8.11963 0.000976562C7.56734 0.000976562 7.11963 0.448692 7.11963 1.00098C7.11963 1.55326 7.56734 2.00098 8.11963 2.00098H10.7054L4.41252 8.29387C4.022 8.68439 4.022 9.31756 4.41252 9.70808C4.80305 10.0986 5.43621 10.0986 5.82674 9.70808L12.1196 3.41519V6.00098C12.1196 6.55326 12.5673 7.00098 13.1196 7.00098C13.6719 7.00098 14.1196 6.55326 14.1196 6.00098V1.00098C14.1196 0.448692 13.6719 0.000976562 13.1196 0.000976562H8.11963Z" />
<path d="M2.11963 2.00098C1.01506 2.00098 0.119629 2.89641 0.119629 4.00098V12.001C0.119629 13.1055 1.01506 14.001 2.11963 14.001H10.1196C11.2242 14.001 12.1196 13.1055 12.1196 12.001V9.00098C12.1196 8.44869 11.6719 8.00098 11.1196 8.00098C10.5673 8.00098 10.1196 8.44869 10.1196 9.00098V12.001H2.11963V4.00098L5.11963 4.00098C5.67191 4.00098 6.11963 3.55326 6.11963 3.00098C6.11963 2.44869 5.67191 2.00098 5.11963 2.00098H2.11963Z" />
</svg>
);
}
function DocsButton({url, label}) {
return (
<a
href={url}
target="_blank"
className="bg-white shadow py-2 px-5 rounded-full inline-flex items-center hover:opacity-80"
rel="noreferrer"
>
{label}
<ExternalIcon />
</a>
);
}
function StorefrontInfo({shopName, totalProducts, totalCollections}) {
const pluralize = (count, noun, suffix = 's') =>
`${count} ${noun}${count === 1 ? '' : suffix}`;
return (
<div className="bg-white p-12 shadow-xl rounded-xl text-gray-900">
<p className="text-md font-medium uppercase mb-4">Connected Storefront</p>
<h2 className="text-2xl font-bold mb-4">{shopName}</h2>
<p className="text-md">
{pluralize(totalProducts, 'Product')}
{', '}
{pluralize(totalCollections, 'Collection')}
</p>
{totalProducts === 0 && totalCollections === 0 && (
<div className="py-2 px-3 bg-red-100 text-md">
Use the{' '}
<a
href="https://shopify.dev/apps/tools/cli/getting-started"
className="text-primary font-mono font-bold underline"
target="_blank"
rel="noreferrer"
>
Shopify CLI
</a>{' '}
to populate sample products and collections.
</div>
)}
<hr className="my-4" />
<a
href="https://shopify.dev/custom-storefronts/hydrogen/getting-started#update-information-about-your-shopify-storefront"
className="text-md inline-flex items-center text-blue-700 font-medium hover:underline"
target="_blank"
rel="noreferrer"
>
Change your storefront access token
<ExternalIcon />
</a>
</div>
);
}
function TemplateLinks({firstProductPath, firstCollectionPath}) {
return (
<div className="bg-white p-12 md:p-12 shadow-xl rounded-xl text-gray-900">
<p className="text-md font-medium uppercase mb-4">
Explore the templates
</p>
<ul>
<li className="mb-4">
<Link
to={`/collections/${firstCollectionPath}`}
className="text-md font-medium text-blue-700 hover:underline"
>
Collection template
</Link>
</li>
<li className="mb-4">
<Link
to={`/products/${firstProductPath}`}
className="text-md font-medium text-blue-700 hover:underline"
>
Product template
</Link>
</li>
<li>
<Link
to="/error-page"
className="text-md font-medium text-blue-700 hover:underline"
>
404 template
</Link>
</li>
</ul>
</div>
);
}
export default function Welcome() {
const {data} = useShopQuery({query: QUERY});
const shopName = data ? data.shop.name : '';
const products = data && flattenConnection(data.products);
const collections = data && flattenConnection(data.collections);
const firstProduct = products ? products[0].handle : '';
const totalProducts = products && products.length;
const firstCollection = collections[0] ? collections[0].handle : '';
const totalCollections = collections && collections.length;
return (
<div className="text-gray-900 pt-16 rounded-[40px] my-16 px-4 xl:px-12 bg-gradient-to-b from-white -mx-4 xl:-mx-12">
<div className="text-center mb-16">
<h1 className="font-extrabold mb-4 text-5xl md:text-7xl">
Hello, Hydrogen
</h1>
<p className="text-lg mb-8">
Welcome to your custom storefront. Let&rsquo;s get building.
</p>
<div className="flex flex-col lg:flex-row justify-center items-center gap-8 text-gray-700">
<DocsButton
url="https://shopify.dev/custom-storefronts/hydrogen"
label="Browse Hydrogen documentation"
/>
<DocsButton url="/graphql" label="Open the GraphiQL explorer" />
<DocsButton
url="https://github.com/Shopify/hydrogen-examples"
label="Explore Hydrogen examples"
/>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
<StorefrontInfo
shopName={shopName}
totalProducts={totalProducts}
totalCollections={totalCollections}
/>
<TemplateLinks
firstProductPath={firstProduct}
firstCollectionPath={firstCollection}
/>
</div>
</div>
);
}
const QUERY = gql`
query welcomeContent {
shop {
name
}
products(first: 250) {
edges {
node {
handle
}
}
}
collections(first: 250) {
edges {
node {
handle
}
}
}
}
`;

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,168 +1,371 @@
import {
useShopQuery,
flattenConnection,
ProductProviderFragment,
Image,
Link,
} from '@shopify/hydrogen';
import {useShopQuery, flattenConnection} from '@shopify/hydrogen';
import gql from 'graphql-tag';
import Layout from '../components/Layout.server';
import FeaturedCollection from '../components/FeaturedCollection.server';
import ProductCard from '../components/ProductCard.server';
import Welcome from '../components/Welcome.server';
import Button from '../components/Button.client';
function GradientBackground() {
function Header() {
return (
<div className="fixed top-0 w-full h-3/5 overflow-hidden">
<div className="absolute w-full h-full bg-gradient-to-t from-gray-50 z-10" />
<div className="bg-black text-white py-5 px-6 flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<svg
viewBox="0 0 960 743"
aria-hidden="true"
width="171"
height="34"
viewBox="0 0 171 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
className="filter blur-[30px]"
aria-hidden="true"
>
<path
d="M15.8778 33.1462.735582 25.0518 5.9143 22.3054l5.8305 3.1152 4.8922-2.5949-5.8305-3.1153 5.1787-2.753 15.1422 8.0944-5.1787 2.7464-5.5082-2.944-4.8994 2.6016 5.5154 2.944-5.1787 2.7464Z"
fill="#fff"
/>
<path
d="M15.8779 17.0447.735722 8.9503 5.91444 6.20387 11.745 9.31913l4.8922-2.59495-5.8306-3.11526L15.9854.855896 31.1276 8.9503l-5.1787 2.7464-5.5083-2.94398-4.8993 2.60158 5.5153 2.944-5.1787 2.7464Z"
fill="url(#a)"
/>
<path
d="M44.8282 11.3157v11.8448h4.2353v-4.5466h5.1524v4.5466h4.2686V11.3157h-4.2686v4.3118h-5.1357v-4.3118h-4.252ZM64.8954 11.3157h-4.9189l5.4025 8.3216v3.5232h4.1853v-3.5232l5.3358-8.3216h-4.8689l-1.8342 3.322c-.2501.52-.5003 1.0569-.717 1.577-.1001-.2013-.1834-.4026-.2835-.604-.1167-.2516-.2334-.4865-.3501-.7214l-1.951-3.5736ZM76.4508 11.3157v11.8448h7.4701c1.1839-.0335 1.6175-.0839 2.2177-.2349.717-.1845 1.2506-.4697 1.5007-.6039.7004-.4027 1.1005-.8389 1.3173-1.0738 1.1839-1.4261 1.2506-3.3387 1.2506-3.8755 0-.6376-.0334-2.1475-1.0338-3.5904-.7003-1.0234-1.9009-2.181-4.6355-2.4159-.4669-.0336-.9338-.0504-1.4007-.0504h-6.6864Zm4.1186 2.8186h2.0176c.2835 0 1.534-.0335 2.4178.6376 1.0338.8053 1.0838 2.1307 1.0838 2.4998 0 .3188-.0167.9563-.3835 1.7113-.2835.604-.8004 1.2583-2.251 1.4428-.2502.0336-.5003.0504-.7337.0504h-2.151v-6.3419ZM92.3581 11.3157v11.8448h4.2353v-4.3117h2.2343l2.4343 4.3117h4.652l-2.934-4.7312c.6-.151.817-.2349 1.1-.4026 1.484-.8221 1.601-2.3992 1.601-2.9528 0-1.0738-.367-1.7784-.6-2.1308-.934-1.5099-2.468-1.6106-4.052-1.6274h-8.6709Zm4.2353 2.6844h3.5016c.45.0168.917.0671 1.201.4866.166.2348.183.5033.183.6039 0 .1343-.017.4027-.233.6711-.267.3188-.684.4027-1.4845.4195h-3.1681v-2.1811ZM114.752 10.8795c-2.635.0336-4.069.6376-5.069 1.3254-.967.6544-2.551 2.1475-2.551 5.1339 0 .755.05 2.2985 1.3 3.8252.584.7047 1.167 1.0906 1.484 1.2751.951.5704 2.468 1.1241 4.969 1.1241 1.668 0 2.701-.2013 3.385-.4027.7-.2013 1.301-.4865 1.918-.9059 2.384-1.6274 2.601-4.0098 2.601-5.0668 0-1.7952-.684-3.0199-1.067-3.5903-.817-1.1745-2.268-2.3992-5.386-2.6676-.534-.0336-1.051-.0504-1.584-.0504Zm.25 2.6341c.634 0 .95.0839 1.25.1845.567.2013.951.5201 1.134.6711.417.4027 1.068 1.3422 1.068 2.8522 0 1.3421-.534 2.8521-1.851 3.4393-.634.2852-1.384.2852-1.584.2852-.717 0-2.618-.151-3.319-2.2649-.216-.6208-.233-1.2248-.233-1.4429 0-1.4428.634-2.8689 2.018-3.4561.516-.2181.967-.2684 1.517-.2684ZM139.43 14.0337s-.167-.4027-.3-.604c-.2-.3356-.484-.6543-.784-.9228-.2-.2013-.584-.5368-1.217-.8556-1.718-.8389-4.085-.8724-4.519-.8724-3.568 0-5.486 1.1912-6.586 2.3823-1.468 1.6107-1.568 3.4394-1.568 4.2279 0 1.6945.584 2.8857.951 3.4226.166.2684.533.755 1.184 1.2583 1.784 1.3925 3.835 1.4429 4.585 1.4429 2.001 0 3.152-.4698 3.919-.8892.617-.3356.8-.5201 1.067-.7718v1.3086h3.651v-7.0968h-7.02v2.6844h3.069c-.034.1342-.067.2852-.084.3188-.3.6711-1.434 1.7448-3.335 1.7448-1.05 0-2.184-.3691-2.868-1.1912-.75-.8724-.783-1.9797-.783-2.3488 0-1.7784.983-2.7515 1.517-3.1374.934-.6711 2.001-.6878 2.368-.6878 1.45 0 2.184.6039 2.584 1.0402l4.169-.453ZM154.737 11.3157h-12.422v11.8448h12.572v-2.6843h-8.354v-1.8959h7.587v-2.6676h-7.587v-1.9461h8.204v-2.6509ZM156.905 11.3157v11.8448h4.168v-2.5166c-.016-1.3254-.033-2.6676-.066-4.0097.417.5368.867 1.0905 1.334 1.6274l4.318 4.8989h3.502V11.3157h-4.152v4.3454c.017.7885.05 1.4596.083 2.1307-.3-.4027-.617-.8053-.95-1.1912-.35-.4362-.75-.8556-1.134-1.2919l-3.618-3.993h-3.485Z"
fill="#fff"
/>
<defs>
<path fill="#fff" d="M0 0h960v540H0z" id="reuse-0" />
</defs>
<g clipPath="url(#a)">
<use xlinkHref="#reuse-0" />
<path d="M960 0H0v743h960V0Z" fill="#7CFBEE" />
<path
d="M831 380c200.48 0 363-162.521 363-363s-162.52-363-363-363c-200.479 0-363 162.521-363 363s162.521 363 363 363Z"
fill="#4F98D0"
/>
<path
d="M579 759c200.479 0 363-162.521 363-363S779.479 33 579 33 216 195.521 216 396s162.521 363 363 363Z"
fill="#7CFBEE"
/>
<path
d="M178 691c200.479 0 363-162.521 363-363S378.479-35 178-35c-200.4794 0-363 162.521-363 363s162.5206 363 363 363Z"
fill="#4F98D0"
/>
<path
d="M490 414c200.479 0 363-162.521 363-363S690.479-312 490-312 127-149.479 127 51s162.521 363 363 363Z"
fill="#4F98D0"
/>
<path
d="M354 569c200.479 0 363-162.521 363-363 0-200.47937-162.521-363-363-363S-9 5.52063-9 206c0 200.479 162.521 363 363 363Z"
fill="#7CFBEE"
/>
<path
d="M630 532c200.479 0 363-162.521 363-363 0-200.4794-162.521-363-363-363S267-31.4794 267 169c0 200.479 162.521 363 363 363Z"
fill="#4F98D0"
/>
</g>
<path fill="#fff" d="M0 540h960v203H0z" />
<defs>
<clipPath id="a">
<use xlinkHref="#reuse-0" />
</clipPath>
<linearGradient
id="a"
x1="30.5576"
y1="9.57961"
x2="8.17565"
y2="21.5672"
gradientUnits="userSpaceOnUse"
>
<stop offset=".00224366" stopColor="#430470" />
<stop offset=".385468" stopColor="#8E01F0" />
<stop offset=".635473" stopColor="#354CF6" />
<stop offset="1" stopColor="#01FFFF" />
</linearGradient>
</defs>
</svg>
<div className="flex items-center center space-x-10">
<a
href="https://github.com/Shopify/hydrogen/discussions"
className="text-sm font-bold font-mono underline flex items-center"
target="_blank"
rel="noreferrer"
>
Give Feedback
<svg
aria-hidden="true"
className="fill-current text-white ml-2"
width="15"
height="14"
viewBox="0 0 15 14"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8.11963 0.000976562C7.56734 0.000976562 7.11963 0.448692 7.11963 1.00098C7.11963 1.55326 7.56734 2.00098 8.11963 2.00098H10.7054L4.41252 8.29387C4.022 8.68439 4.022 9.31756 4.41252 9.70808C4.80305 10.0986 5.43621 10.0986 5.82674 9.70808L12.1196 3.41519V6.00098C12.1196 6.55326 12.5673 7.00098 13.1196 7.00098C13.6719 7.00098 14.1196 6.55326 14.1196 6.00098V1.00098C14.1196 0.448692 13.6719 0.000976562 13.1196 0.000976562H8.11963Z" />
<path d="M2.11963 2.00098C1.01506 2.00098 0.119629 2.89641 0.119629 4.00098V12.001C0.119629 13.1055 1.01506 14.001 2.11963 14.001H10.1196C11.2242 14.001 12.1196 13.1055 12.1196 12.001V9.00098C12.1196 8.44869 11.6719 8.00098 11.1196 8.00098C10.5673 8.00098 10.1196 8.44869 10.1196 9.00098V12.001H2.11963V4.00098L5.11963 4.00098C5.67191 4.00098 6.11963 3.55326 6.11963 3.00098C6.11963 2.44869 5.67191 2.00098 5.11963 2.00098H2.11963Z" />
</svg>
</a>
<div className="flex items-center space-x-4">
<a
href="https://discord.gg/ppSbThrFaS"
target="_blank"
rel="noreferrer"
>
<span className="sr-only">Discord</span>
<svg
aria-hidden="true"
className="fill-current text-white"
width="26"
height="20"
viewBox="0 0 26 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M21.3103 1.67597C19.691 0.893476 17.9595 0.324791 16.1494 0.000976562C15.9271 0.416095 15.6673 0.974439 15.4883 1.4186C13.564 1.11972 11.6574 1.11972 9.76855 1.4186C9.58952 0.974439 9.3239 0.416095 9.0996 0.000976562C7.28746 0.324791 5.55403 0.895566 3.93472 1.68012C0.668559 6.77767 -0.216844 11.7486 0.225859 16.649C2.39215 18.3198 4.49155 19.3348 6.55551 19.9989C7.06512 19.2745 7.51962 18.5045 7.91116 17.693C7.16546 17.4003 6.45123 17.0392 5.77638 16.6199C5.95541 16.4829 6.13054 16.3397 6.29973 16.1923C10.4159 18.1807 14.8882 18.1807 18.9551 16.1923C19.1263 16.3397 19.3014 16.4829 19.4785 16.6199C18.8016 17.0412 18.0855 17.4024 17.3398 17.6951C17.7313 18.5045 18.1838 19.2766 18.6954 20.001C20.7614 19.3368 22.8627 18.3219 25.029 16.649C25.5484 10.9682 24.1416 6.04292 21.3103 1.67597ZM8.47192 13.6353C7.2363 13.6353 6.22299 12.4439 6.22299 10.9931C6.22299 9.5423 7.21466 8.34886 8.47192 8.34886C9.72922 8.34886 10.7425 9.54021 10.7209 10.9931C10.7228 12.4439 9.72922 13.6353 8.47192 13.6353ZM16.7829 13.6353C15.5473 13.6353 14.534 12.4439 14.534 10.9931C14.534 9.5423 15.5256 8.34886 16.7829 8.34886C18.0402 8.34886 19.0535 9.54021 19.0319 10.9931C19.0319 12.4439 18.0402 13.6353 16.7829 13.6353Z" />
</svg>
</a>
<a
href="https://github.com/Shopify/hydrogen"
target="_blank"
rel="noreferrer"
>
<span className="sr-only">Github</span>
<svg
aria-hidden="true"
className="fill-current text-white"
width="21"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.1319 0.000976562C4.60874 0.000976562 0.135254 4.58917 0.135254 10.2539C0.135254 14.7908 2.99679 18.6229 6.97045 19.9814C7.47028 20.0711 7.65772 19.7635 7.65772 19.4944C7.65772 19.2509 7.64522 18.4434 7.64522 17.5848C5.13357 18.059 4.48379 16.9568 4.28385 16.38C4.17139 16.0853 3.68406 15.1753 3.2592 14.9318C2.90932 14.7396 2.40949 14.2654 3.2467 14.2526C4.03394 14.2397 4.59625 14.9959 4.78369 15.3035C5.68338 16.8542 7.1204 16.4185 7.6952 16.1494C7.78267 15.4829 8.04508 15.0343 8.33249 14.778C6.10824 14.5217 3.78402 13.6374 3.78402 9.71564C3.78402 8.60063 4.17139 7.67786 4.80868 6.96016C4.70871 6.70383 4.35883 5.65291 4.90864 4.24313C4.90864 4.24313 5.74586 3.97399 7.65772 5.29406C8.45745 5.06336 9.30716 4.94802 10.1569 4.94802C11.0066 4.94802 11.8563 5.06336 12.656 5.29406C14.5679 3.96117 15.4051 4.24313 15.4051 4.24313C15.9549 5.65291 15.605 6.70383 15.5051 6.96016C16.1424 7.67786 16.5297 8.58781 16.5297 9.71564C16.5297 13.6502 14.193 14.5217 11.9688 14.778C12.3311 15.0984 12.6435 15.7136 12.6435 16.6748C12.6435 18.0461 12.631 19.1483 12.631 19.4944C12.631 19.7635 12.8185 20.0839 13.3183 19.9814C15.3028 19.2943 17.0273 17.9861 18.2489 16.2411C19.4706 14.4962 20.128 12.4022 20.1285 10.2539C20.1285 4.58917 15.655 0.000976562 10.1319 0.000976562Z"
/>
</svg>
</a>
<a
href="https://twitter.com/shopifydevs"
target="_blank"
rel="noreferrer"
>
<span className="sr-only">Twitter</span>
<svg
aria-hidden="true"
className="fill-current text-white"
width="23"
height="20"
viewBox="0 0 23 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.6585 4.97993C20.6724 5.19691 20.6724 5.4139 20.6724 5.63288C20.6724 12.3055 15.9522 20.001 7.32122 20.001V19.997C4.77158 20.001 2.27491 19.215 0.12854 17.7331C0.499277 17.7811 0.871873 17.8051 1.2454 17.8061C3.35832 17.8081 5.41085 17.0452 7.07313 15.6403C5.0652 15.5993 3.30443 14.1903 2.68932 12.1335C3.3927 12.2795 4.11745 12.2495 4.80782 12.0465C2.61871 11.5705 1.04377 9.50064 1.04377 7.09679C1.04377 7.07479 1.04377 7.05379 1.04377 7.0328C1.69604 7.42377 2.42637 7.64076 3.17342 7.66476C1.1116 6.18185 0.476048 3.23004 1.72113 0.922181C4.10351 4.07698 7.61855 5.99486 11.3919 6.19785C11.0137 4.44396 11.5303 2.60607 12.7494 1.37315C14.6393 -0.538727 17.6117 -0.440733 19.3883 1.59214C20.4392 1.36915 21.4464 0.954179 22.3681 0.366216C22.0178 1.53514 21.2847 2.52808 20.3054 3.15904C21.2355 3.04105 22.1442 2.77306 23 2.36409C22.37 3.38003 21.5765 4.26497 20.6585 4.97993Z" />
</svg>
</a>
</div>
</div>
</div>
);
}
export default function Index({country = {isoCode: 'US'}}) {
const {data} = useShopQuery({
query: QUERY,
variables: {
country: country.isoCode,
},
});
function WelcomeMessage({firstProductPath, firstCollectionPath}) {
return (
<div className="grid grid-cols-1 lg:grid-cols-[3fr,1fr] col-span-2 gap-8 my-16 bg-white p-8 md:p-16 border-2 border-black">
<div className="flex flex-col">
<h1 className="text-black text-4xl md:text-5xl font-black mb-4">
Hello, Hydrogen.
</h1>
<p className="text-2xl mb-8">Welcome to your custom storefront.</p>
<div className="flex flex-col lg:flex-row space-x-0 lg:space-x-8 space-y-8 lg:space-y-0 mb-8">
<Button
className="md:px-5 py-4 flex-1 lg:flex-none"
url="https://shopify.dev/custom-storefronts/hydrogen"
label="Browse documentation"
target="_blank"
/>
<Button
className="md:px-5 py-4 flex-1 lg:flex-none"
url="/graphql"
label="Open GraphiQL explorer"
target="_blank"
/>
</div>
</div>
<div className="max-w-max">
<p className="text-black text-sm font-bold font-mono mb-3 uppercase">
Explore the templates
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 gap-y-3 gap-x-8">
<a href="/home" className="text-lg font-mono font-bold text-blue-600">
Home template
</a>
<a
href={`/collections/${firstCollectionPath}`}
className="text-lg font-mono font-bold text-blue-600"
>
Collection template
</a>
<a
href={`/products/${firstProductPath}`}
className="text-lg font-mono font-bold text-blue-600"
>
Product template
</a>
<a
href="/error-page"
className="text-lg font-mono font-bold text-blue-600"
>
404 template
</a>
</div>
</div>
</div>
);
}
const collections = data ? flattenConnection(data.collections) : [];
const featuredProductsCollection = collections[0];
const featuredProducts = featuredProductsCollection
? flattenConnection(featuredProductsCollection.products)
: null;
const featuredCollection =
collections && collections.length > 1 ? collections[1] : collections[0];
function StorefrontInfo({shopName, totalProducts, totalCollections}) {
const pluralize = (count, noun, suffix = 's') =>
// eslint-disable-next-line no-negated-condition
`${count} ${noun}${count !== 1 ? suffix : ''}`;
return (
<div className="bg-white p-8 md:p-16 border-2 border-black text-black">
<p className="text-sm font-bold font-mono tracking-wider uppercase mb-4">
Connected Storefront
</p>
<h2 className="text-2xl font-bold mb-4">{shopName}</h2>
<p className="text-lg">
{pluralize(totalProducts, 'Product')}
{', '}
{pluralize(totalCollections, 'Collection')}
</p>
{totalProducts === 0 && totalCollections === 0 && (
<div className="py-2 px-3 bg-red-100 text-lg">
Use the{' '}
<a
href="https://shopify.dev/apps/tools/cli/getting-started"
className="text-blue-600 font-mono font-bold underline"
target="_blank"
rel="noreferrer"
>
Shopify CLI
</a>{' '}
to populate sample products and collections.
</div>
)}
<hr className="my-8" />
<a
href="https://shopify.dev/custom-storefronts/hydrogen/getting-started#update-information-about-your-shopify-storefront"
className="inline-flex items-center text-blue-600 font-mono font-bold underline"
target="_blank"
rel="noreferrer"
>
Changing your storefront access token
<svg
aria-hidden="true"
className="fill-current ml-3"
width="15"
height="14"
viewBox="0 0 15 14"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8.11963 0.000976562C7.56734 0.000976562 7.11963 0.448692 7.11963 1.00098C7.11963 1.55326 7.56734 2.00098 8.11963 2.00098H10.7054L4.41252 8.29387C4.022 8.68439 4.022 9.31756 4.41252 9.70808C4.80305 10.0986 5.43621 10.0986 5.82674 9.70808L12.1196 3.41519V6.00098C12.1196 6.55326 12.5673 7.00098 13.1196 7.00098C13.6719 7.00098 14.1196 6.55326 14.1196 6.00098V1.00098C14.1196 0.448692 13.6719 0.000976562 13.1196 0.000976562H8.11963Z" />
<path d="M2.11963 2.00098C1.01506 2.00098 0.119629 2.89641 0.119629 4.00098V12.001C0.119629 13.1055 1.01506 14.001 2.11963 14.001H10.1196C11.2242 14.001 12.1196 13.1055 12.1196 12.001V9.00098C12.1196 8.44869 11.6719 8.00098 11.1196 8.00098C10.5673 8.00098 10.1196 8.44869 10.1196 9.00098V12.001H2.11963V4.00098L5.11963 4.00098C5.67191 4.00098 6.11963 3.55326 6.11963 3.00098C6.11963 2.44869 5.67191 2.00098 5.11963 2.00098H2.11963Z" />
</svg>
</a>
</div>
);
}
function NextSteps() {
return (
<div className="bg-white p-8 md:p-14 border-2 border-black text-black">
<p className="text-sm font-bold font-mono tracking-wider uppercase mb-4">
Update Your Index
</p>
<p className="text-lg mb-6 md:mb-4 leading-10">
1. Remove or rename{' '}
<span className="py-1 px-3 bg-gray-100 font-mono font-bold">
pages/index.jsx
</span>
</p>
<p className="text-lg mb-6 md:mb-4 leading-10">
2. Rename{' '}
<code className="py-1 px-3 bg-gray-100 font-mono font-bold">
pages/home.jsx
</code>{' '}
to{' '}
<code className="py-1 px-3 bg-gray-100 font-mono font-bold">
pages/index.jsx
</code>
</p>
<p className="text-lg mb-6 md:mb-4 leading-10">
3. Happy Coding!{' '}
<svg
className="inline"
width="27"
height="27"
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-hidden="true"
>
<path
d="M84 39.3684v-8.1052h-4v-8.1053h-4v-4.0526h-8v-4.0527h-8V11H40v4.0526h-8v4.0527h-8v4.0526h-4v8.1053h-4v8.1052h-4V59.6316h4v8.1052h4v8.1053h4v4.0526h8v4.0527h8V88h20v-4.0526h8v-4.0527h8v-4.0526h4v-8.1053h4v-8.1052h4V39.3684h-4Z"
fill="#FFFF01"
/>
<path
fill="#000"
d="M36 29.2363h4v4.05263h-4zM32 33.2891h4v4.05263h-4zM60 33.2891h-4v4.05263h4zM32 37.3418h4v4.05263h-4zM40 33.2891h4v4.05263h-4zM60 37.3418h-4v4.05263h4zM40 37.3418h4v4.05263h-4zM59.3335 29.2363h4v4.05263h-4z"
/>
<path
fill="#000"
d="M55.3335 33.2891h4v4.05263h-4zM63.3335 33.2891h4v4.05263h-4zM55.3335 37.3418h4v4.05263h-4zM63.3335 37.3418h4v4.05263h-4zM36 15.0527h4v4.05263h-4zM32 15.0527h4v4.05263h-4zM28 19.1055h4v4.05263h-4zM24 19.1055h4v4.05263h-4zM20 23.1582h4v4.05263h-4zM36 83.9473h4v-4.05264h-4zM32 83.9473h4v-4.05264h-4zM28 79.8945h4v-4.05263h-4zM20 27.2109h4v4.05263h-4zM24 79.8945h4v-4.05263h-4zM20 75.8438h4v-4.05263h-4zM40 11h4v4.05263h-4zM40 83.9473h4v4.05263h-4zM64 15.0527h-4v4.05263h4zM20 71.7891h4v-4.05263h-4zM68 15.0527h-4v4.05263h4zM16 31.2637h4v4.05263h-4zM72 19.1055h-4v4.05263h4zM76 19.1055h-4v4.05263h4zM80 23.1582h-4v4.05263h4zM64 83.9473h-4v-4.05264h4zM68 83.9473h-4v-4.05264h4zM72 79.8945h-4v-4.05263h4zM16 67.7363h4v-4.05263h-4zM12 39.3691h4v4.05263h-4zM80 27.2109h-4v4.05263h4zM76 79.8945h-4v-4.05263h4zM80 75.8438h-4v-4.05263h4z"
/>
<path
fill="#000"
d="M16 35.3145h4v4.05263h-4zM36 33.2891h4v4.05263h-4zM22.6665 46.123h4v4.05263h-4zM78 46.123h-4.00001v4.05263H78zM80 71.7891h-4v-4.05263h4zM36 37.3418h4v4.05263h-4zM84 31.2637h-4v4.05263h4zM36 41.3945h4v4.05263h-4zM22.6665 50.1758h4v4.05263h-4z"
/>
<path
fill="#000"
d="M22.6665 54.2266h4v4.05263h-4zM12 43.4219h4v4.05263h-4z"
/>
<path
fill="#000"
d="M12 47.4727h4v4.05263h-4zM12 51.5254h4v4.05263h-4zM26.6665 58.2812h4v4.05263h-4zM30.6665 62.334h4v4.05263h-4zM12 55.5781h4v4.05263h-4zM34.6665 66.3848h4v4.05263h-4zM84 67.7363h-4v-4.05263h4zM88 39.3691h-4v4.05263h4zM78 50.1758h-4.00001v4.05263H78z"
/>
<path
fill="#000"
d="M78 54.2266h-4.00001v4.05263H78zM74 58.2812h-4v4.05263h4zM84 35.3145h-4v4.05263h4zM70 62.334h-4v4.05263h4zM66 66.3848h-4v4.05263h4zM38.6665 70.4375h4v4.05264h-4zM42.6665 70.4375h4v4.05264h-4zM46.6665 70.4375h4v4.05264h-4zM50.6665 70.4375h4v4.05264h-4zM54.667 70.4375h4v4.05264h-4zM16 63.6836h4v-4.05264h-4zM80 63.6836h4v-4.05264h-4z"
/>
<path
fill="#000"
d="M58.6665 70.4375h4v4.05264h-4zM88 43.4219h-4v4.05263h4z"
/>
<path
fill="#000"
d="M88 47.4727h-4v4.05263h4zM88 51.5254h-4v4.05263h4zM44 11h4v4.05263h-4zM48 11h4v4.05263h-4zM88 55.5781h-4v4.05263h4zM52 11h4v4.05263h-4zM56 11h4v4.05263h-4zM44 83.9473h4v4.05263h-4zM48 83.9473h4v4.05263h-4zM59.3335 33.2891h4v4.05263h-4zM52 83.9473h4v4.05263h-4zM56 83.9473h4v4.05263h-4zM59.3335 37.3418h4v4.05263h-4zM59.3335 41.3945h4v4.05263h-4z"
/>
</svg>
</p>
</div>
);
}
export default function Index() {
const {data} = useShopQuery({query: QUERY});
const shopName = data ? data.shop.name : '';
const products = data && flattenConnection(data.products);
const collections = data && flattenConnection(data.collections);
const firstProduct = products ? products[0].handle : '';
const totalProducts = products && products.length;
const firstCollection = collections ? collections[0].handle : '';
const totalCollections = collections && collections.length;
return (
<Layout hero={<GradientBackground />}>
<div className="relative mb-12">
<Welcome />
<div className="bg-white p-12 shadow-xl rounded-xl mb-10">
{featuredProductsCollection ? (
<>
<div className="flex justify-between items-center mb-8 text-md font-medium">
<span className="text-black uppercase">
{featuredProductsCollection.title}
</span>
<span className="hidden md:inline-flex">
<Link
to={`/collections/${featuredProductsCollection.handle}`}
className="text-blue-600 hover:underline"
>
Shop all
</Link>
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-8">
{featuredProducts.map((product) => (
<div key={product.id}>
<ProductCard product={product} />
</div>
))}
</div>
<div className="md:hidden text-center">
<Link
to={`/collections/${featuredCollection.handle}`}
className="text-blue-600"
>
Shop all
</Link>
</div>
</>
) : null}
<div
className="min-h-screen background-repeat pb-16"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='216' height='202' viewBox='0 0 216 202' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23a)' stroke='%23000' stroke-opacity='.5'%3E%3Cpath d='M43.5 1v200M86.5 1v200M129.5 1v200M172.5 1v201M215.5 2e-8V207M2e-8 165H215M2e-8 122H215M2e-8 79H215M2e-8 36H215M2e-8 .5 215 .500009'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath fill='%23fff' d='M0 0h216v202H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E")`,
}}
>
<Header />
<div className="mx-auto max-w-7xl px-4 md:px-10">
<WelcomeMessage
firstProductPath={firstProduct}
firstCollectionPath={firstCollection}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 mt-8">
<StorefrontInfo
shopName={shopName}
totalProducts={totalProducts}
totalCollections={totalCollections}
/>
<NextSteps />
</div>
<FeaturedCollection collection={featuredCollection} />
</div>
</Layout>
</div>
);
}
const QUERY = gql`
query indexContent(
$country: CountryCode
$numCollections: Int = 2
$numProducts: Int = 3
$numProductMetafields: Int = 0
$numProductVariants: Int = 250
$numProductMedia: Int = 1
$numProductVariantMetafields: Int = 10
$numProductVariantSellingPlanAllocations: Int = 0
$numProductSellingPlanGroups: Int = 0
$numProductSellingPlans: Int = 0
) @inContext(country: $country) {
collections(first: $numCollections) {
query welcomeContent {
shop {
name
}
products(first: 250) {
edges {
node {
handle
}
}
}
collections(first: 250) {
edges {
node {
descriptionHtml
description
handle
id
title
image {
...ImageFragment
}
products(first: $numProducts) {
edges {
node {
...ProductProviderFragment
}
}
}
}
}
}
}
${ProductProviderFragment}
${Image.Fragment}
`;

View file

@ -37,11 +37,11 @@ export default function Collection({
return (
<Layout>
<h1 className="font-bold text-4xl md:text-5xl text-gray-900 mb-6 mt-6">
<h1 className="font-black text-4xl md:text-5xl text-black mb-6 mt-6">
{collection.title}
</h1>
<RawHtml string={collection.descriptionHtml} className="text-2xl" />
<p className="text-sm text-gray-500 mt-5 mb-5">
<p className="text-sm text-gray-900 mt-5 mb-5">
{products.length} {products.length > 1 ? 'products' : 'product'}
</p>

View file

@ -0,0 +1,99 @@
import {
useShopQuery,
flattenConnection,
ProductProviderFragment,
Image,
} from '@shopify/hydrogen';
import gql from 'graphql-tag';
import Layout from '../components/Layout.server';
import FeaturedCollection from '../components/FeaturedCollection.server';
import CollectionHero from '../components/CollectionHero.client';
import ProductCard from '../components/ProductCard.server';
import StyledLink from '../components/StyledLink';
export default function Home({country = {isoCode: 'US'}}) {
const {data} = useShopQuery({
query: QUERY,
variables: {
country: country.isoCode,
},
});
const collections = data ? flattenConnection(data.collections) : [];
const heroCollection = collections && collections[0];
const featuredCollection =
collections && collections.length > 1 ? collections[1] : collections[0];
const featuredProducts = flattenConnection(featuredCollection.products);
return (
<Layout hero={<CollectionHero collection={heroCollection} />}>
<div className="flex justify-between items-center mb-8">
<span className="text-small text-black uppercase tracking-wider font-bold font-mono">
{featuredCollection.title}
</span>
<span className="hidden md:inline-flex">
<StyledLink
url={`/collections/${featuredCollection.handle}`}
value="Shop All"
/>
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
{featuredProducts.map((product) => (
<div key={product.id}>
<ProductCard product={product} />
</div>
))}
</div>
<div className="md:hidden mb-16 text-center">
<StyledLink
url={`/collections/${featuredCollection.handle}`}
value="Shop All"
/>
</div>
<FeaturedCollection collection={featuredCollection} />
</Layout>
);
}
const QUERY = gql`
query indexContent(
$country: CountryCode
$numCollections: Int = 2
$numProducts: Int = 3
$numProductMetafields: Int = 0
$numProductVariants: Int = 250
$numProductMedia: Int = 1
$numProductVariantMetafields: Int = 10
$numProductVariantSellingPlanAllocations: Int = 0
$numProductSellingPlanGroups: Int = 0
$numProductSellingPlans: Int = 0
) @inContext(country: $country) {
collections(first: $numCollections) {
edges {
node {
descriptionHtml
handle
id
title
image {
...ImageFragment
}
products(first: $numProducts) {
edges {
node {
...ProductProviderFragment
}
}
}
}
}
}
}
${ProductProviderFragment}
${Image.Fragment}
`;

View file

@ -3,16 +3,11 @@ import {useShopQuery, RawHtml} from '@shopify/hydrogen';
import gql from 'graphql-tag';
import Layout from '../../components/Layout.server';
import NotFound from '../../components/NotFound.server';
export default function Page() {
const {handle} = useParams();
const {data} = useShopQuery({query: QUERY, variables: {handle}});
if (!data.pageByHandle) {
return <NotFound />;
}
const page = data.pageByHandle;
return (

View file

@ -0,0 +1,107 @@
import {useEffect, useMemo, useState} from 'react';
import {useHistory, useLocation} from 'react-router-dom';
import {useShopQuery, MediaFileFragment} from '@shopify/hydrogen';
import gql from 'graphql-tag';
import Layout from '../components/Layout.server';
import ProductCard from '../components/ProductCard.server';
/**
* TODO: Refactor to a true server component.
*/
export default function Search() {
const {search} = useLocation();
const originalQuery = useMemo(
() => new URLSearchParams(search).get('query'),
[search],
);
const history = useHistory();
const [query, setQuery] = useState(originalQuery);
const [newQuery, setNewQuery] = useState(query);
useEffect(() => {
if (originalQuery) {
setQuery(originalQuery);
}
}, [originalQuery]);
return (
<Layout>
<h1 className="text-2xl font-bold">Search</h1>
<form
onSubmit={(event) => {
event.preventDefault();
setQuery(newQuery);
history.push(`/search?query=${newQuery}`);
}}
className="mt-4 space-x-2"
>
<label htmlFor="search">Search Products:</label>
<input
autocomplete="off"
name="search"
id="search"
type="search"
value={newQuery}
onChange={(event) => setNewQuery(event.target.value)}
className="p-1"
/>
<button type="submit" className="bg-black text-white font-bold p-1">
Search
</button>
</form>
{query && <SearchResults query={query} />}
</Layout>
);
}
function SearchResults({query}) {
const {data, fetching} = useShopQuery({query: QUERY, variables: {query}});
if (fetching) return <p>Loading...</p>;
return (
<>
<h2 className="text-xl font-medium mt-8">Search results for: {query}</h2>
{data.products.edges.length ? (
<ul className="grid lg:grid-cols-3 gap-6 mt-4">
{data.products.edges.map((edge) => (
<li key={edge.node.id}>
<ProductCard product={edge.node} />
</li>
))}
</ul>
) : (
<p>No results found.</p>
)}
</>
);
}
const QUERY = gql`
fragment SearchProductDetails on Product {
id
title
handle
media(first: 1) {
edges {
node {
...MediaFileFragment
}
}
}
}
query ProductSearch($query: String!) {
products(query: $query, first: 10) {
edges {
node {
...SearchProductDetails
}
}
}
}
${MediaFileFragment}
`;

View file

@ -6,5 +6,4 @@ import shopifyConfig from './shopify.config';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [hydrogen(shopifyConfig)],
optimizeDeps: {include: ['@headlessui/react']},
});

View file

@ -1,30 +0,0 @@
# Contributing to `eslint-plugin-hydrogen`
## Thanks for your interest in being a contributor ❤️
If this is your first time contributing to an Open Source Project on GitHub, you can learn how from [this free course](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
All changes should have tests and documentation.
## Project setup
1. Fork and clone the repo
2. `$ yarn install` to install dependencies
3. `$ yarn test` to run all tests
## Test changes in the starter template
Before submitting a PR you can test your changes in the starter template by replacing the contents of `packages/dev/.eslintrc.js` with:
```js
module.exports = {
extends: ['plugin:hydrogen/recommended'],
};
```
And then modifying the `lint:js` task in `packages/dev/package.json` to be:
```json
"lint:js": "eslint --rulesdir '../eslint-plugin/dist/' --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx src",
```
Finally, run `yarn lint:js` from the root of that package.

View file

@ -7,16 +7,6 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
<!-- ## Unreleased -->
## 0.6.2 - 2021-11-10
- Added new `hydrogen/typescript` config
- Added `env#node: true` and `parserOptions#sourceType: 'module'` to core config
- Fixed issue requiring typescript to be install on non-typescript projects
## 0.6.0 - 2021-11-05
- No updates. Transitive dependency bump.
## 0.5.0 - 2021-11-01
- No updates. Transitive dependency bump.

View file

@ -1,12 +1,27 @@
<!-- This file is generated from the source code. Edit the files in /packages/eslint-plugin and run 'yarn generate-docs' at the root of this repo. -->
# `eslint-plugin-hydrogen`
Hydrogen provides an ESLint plugin to enforce Shopifys javascript best practices and catch common issues when using React Server components in Hydrogen apps.
## Configurations
| Rule | Description | Configurations | Fixable |
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------- | ------- |
| [hydrogen/no-state-in-server-components](./src/rules/no-state-in-server-components) | Prevents `useState` and `useReducer` in React Server Components | `recommended` | |
| [hydrogen/prefer-image-component](./src/rules/prefer-image-component) | Prefer using @shopify/hydrogen `Image` component in place of HTML `img` tags' | `recommended` | ✅ |
### Recommended
## Installation
You'll first need to install [ESLint](http://eslint.org) in addition to this module.
```bash
yarn add --dev eslint eslint-plugin-hydrogen
```
## Usage
Hydrogens ESLint configurations come bundled in this package. In order to use them, you simply extend the relevant configuration in your projects `.eslintrc.js`.
### Configurations
#### Recommended
This plugin exports a recommended configuration.
@ -19,31 +34,15 @@ config file:
}
```
### Hydrogen
#### Recommended for Typescript projects
This plugin exports a `hydrogen` configuration. Unlike the [recommended](#recommended) configuration, this excludes suggested third-party plugins and configurations. Instead, it enables only the `hydrogen` rules with their suggested defaults.
This plugin exports a recommended configuration for Typescript projects.
To enable this configuration use the `extends` property in your `.eslintrc.js`
config file:
```ts
{
extends: 'plugin:hydrogen/hydrogen',
extends: 'plugin:hydrogen/recommended-typescript',
}
```
## Contributing
If you've come here to help contribute Thank you ❤️ Take a look at the [contributing docs](./.github/contributing.md) to get getting started.
## Installation
You'll first need to install [ESLint](http://eslint.org) in addition to this module.
```bash
yarn add --dev eslint eslint-plugin-hydrogen
```
## Usage
Hydrogens ESLint configurations come bundled in this package. To use them you must extend the relevant configuration in your projects `.eslintrc.js`.

View file

@ -1,27 +0,0 @@
## Configurations
### Recommended
This plugin exports a recommended configuration.
To enable this configuration use the `extends` property in your `.eslintrc.js`
config file:
```ts
{
extends: 'plugin:hydrogen/recommended',
}
```
### Hydrogen
This plugin exports a `hydrogen` configuration. Unlike the [recommended](#recommended) configuration, this excludes suggested third-party plugins and configurations. Instead, it enables only the `hydrogen` rules with their suggested defaults.
To enable this configuration use the `extends` property in your `.eslintrc.js`
config file:
```ts
{
extends: 'plugin:hydrogen/hydrogen',
}
```

View file

@ -1,3 +0,0 @@
## Contributing
If you've come here to help contribute Thank you ❤️ Take a look at the [contributing docs](./.github/contributing.md) to get getting started.

View file

@ -1,7 +0,0 @@
## Installation
You'll first need to install [ESLint](http://eslint.org) in addition to this module.
```bash
yarn add --dev eslint eslint-plugin-hydrogen
```

View file

@ -1,3 +0,0 @@
# `eslint-plugin-hydrogen`
Hydrogen provides an ESLint plugin to enforce Shopifys javascript best practices and catch common issues when using React Server components in Hydrogen apps.

View file

@ -1,3 +0,0 @@
## Usage
Hydrogens ESLint configurations come bundled in this package. To use them you must extend the relevant configuration in your projects `.eslintrc.js`.

View file

@ -1,6 +1,6 @@
{
"name": "eslint-plugin-hydrogen",
"version": "0.6.2",
"version": "0.5.0",
"description": "Eslint rules for hydrogen",
"license": "MIT",
"main": "./dist/index.js",
@ -12,8 +12,7 @@
"test": "jest",
"dev": "yarn build -w",
"build": "tsc -p .",
"prepack": "yarn build",
"tophat": "node ./scripts/tophat"
"prepack": "yarn build"
},
"repository": {
"type": "git",
@ -21,28 +20,27 @@
"directory": "packages/eslint-plugin"
},
"dependencies": {
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/experimental-utils": "^4.0.1",
"@typescript-eslint/parser": "^4.26.0",
"@typescript-eslint/types": "^4.20.0",
"eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^24.3.0",
"eslint-plugin-jsx-a11y": "^6.4.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.0.0"
"@shopify/eslint-plugin": "^40.4.0",
"@typescript-eslint/experimental-utils": "^4.0.1"
},
"devDependencies": {
"@types/dedent": "^0.7.0",
"@types/jest": "^26.0.22",
"@typescript-eslint/parser": "^4.20.0",
"@typescript-eslint/types": "^4.20.0",
"dedent": "^0.7.0",
"eslint": "^7.23.0",
"jest": "^26.6.3",
"ts-jest": "^26.5.4"
},
"peerDependencies": {
"eslint": ">=5"
"@typescript-eslint/eslint-plugin": ">= 4",
"eslint": ">=5",
"prettier": ">= 2"
},
"peerDependenciesMeta": {
"@typescript-eslint/eslint-plugin": {
"optional": true
}
}
}

View file

@ -0,0 +1,12 @@
import rules from './rules';
export default {
plugins: ['hydrogen'],
extends: [
'plugin:@shopify/typescript',
'plugin:@shopify/react',
'plugin:@shopify/node',
'plugin:@shopify/prettier',
],
rules,
};

View file

@ -0,0 +1,12 @@
import rules from './rules';
export default {
plugins: ['hydrogen'],
extends: [
'plugin:@shopify/esnext',
'plugin:@shopify/react',
'plugin:@shopify/node',
'plugin:@shopify/prettier',
],
rules,
};

View file

@ -0,0 +1,4 @@
export default {
'hydrogen/no-state-in-server-components': 'error',
'hydrogen/prefer-image-component': 'error',
};

View file

@ -0,0 +1,5 @@
import {deepMerge} from '@typescript-eslint/experimental-utils/dist/eslint-utils';
import overrides from './overrides';
import hydrogen from './hydrogen';
export default deepMerge(hydrogen, overrides);

View file

@ -0,0 +1,45 @@
export default {
// TODO: We may want to change the configuration for the following 3 rules
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'off',
'react/jsx-filename-extension': ['error', {extensions: ['.tsx', '.jsx']}],
/**
* Rules below this line are intentionally disabled for different reasons
*/
// The following 2 rules are very strict and disabled here to lessen developer frustration
'@shopify/jsx-no-hardcoded-content': 'off',
'@shopify/jsx-no-complex-expressions': 'off',
/**
* eslint overrides
*/
// We often define GraphQL queries at the bottom of the file which would be a violation for this rule
'no-use-before-define': 'off',
// We agree that warning comments acceptable
'no-warning-comments': 'off',
/**
* react overrides
*/
// Following Next.js and likely this will be the default in most frameworks
'react/react-in-jsx-scope': 'off',
// We default to typescript for typing, non typescript projects can override this rule if they want
'react/prop-types': 'off',
/**
* import overrides
*/
// TODO: We should turn these on, at least partially
// 'import/no-unresolved': ['off'],
// 'import/extensions': ['off'],
// These two rules result in a significant number of false positives so we
// need to keep them disabled.
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/control-has-associated-label': 'off',
};

View file

@ -1,54 +0,0 @@
export default {
extends: [
'eslint:recommended',
'plugin:eslint-comments/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:prettier/recommended',
],
plugins: ['eslint-comments', 'prettier', 'react', 'react-hooks', 'jsx-a11y'],
env: {
es2021: true,
browser: true,
node: true,
},
settings: {
react: {
version: 'detect',
},
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
sourceType: 'module',
},
rules: {
'no-console': 'warn',
'eslint-comments/no-unused-disable': 'error',
'object-shorthand': ['error', 'always', {avoidQuotes: true}],
'react/display-name': 'off',
'react/prop-types': 'off',
'react/no-array-index-key': 'warn',
'react/react-in-jsx-scope': 'off',
'@shopify/jsx-no-hardcoded-content': 'off',
'@shopify/jsx-no-complex-expressions': 'off',
'no-use-before-define': 'off',
'no-warning-comments': 'off',
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/control-has-associated-label': 'off',
},
ignorePatterns: ['node_modules/', 'build/', '*.graphql.d.ts', '*.graphql.ts'],
overrides: [
{
files: ['*.test.*'],
plugins: ['jest'],
extends: ['plugin:jest/recommended'],
env: {
node: true,
jest: true,
},
},
],
};

View file

@ -1,7 +0,0 @@
export default {
plugins: ['hydrogen'],
rules: {
'hydrogen/no-state-in-server-components': 'error',
'hydrogen/prefer-image-component': 'error',
},
};

View file

@ -1,8 +0,0 @@
import {merge} from '../utilities';
import core from './core';
import hydrogen from './hydrogen';
const recommended = merge(core, hydrogen);
export default recommended;

View file

@ -1,52 +0,0 @@
export default {
overrides: [
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
extends: ['plugin:@typescript-eslint/recommended'],
rules: {
'react/prop-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/ban-types': [
'error',
{
types: {
String: {message: 'Use string instead', fixWith: 'string'},
Boolean: {message: 'Use boolean instead', fixWith: 'boolean'},
Number: {message: 'Use number instead', fixWith: 'number'},
Object: {message: 'Use object instead', fixWith: 'object'},
Array: {message: 'Provide a more specific type'},
ReadonlyArray: {message: 'Provide a more specific type'},
},
},
],
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble',
},
{
selector: 'typeLike',
format: ['PascalCase'],
},
{
selector: 'typeParameter',
format: ['PascalCase'],
leadingUnderscore: 'allow',
},
{
selector: 'interface',
format: ['PascalCase'],
},
],
},
},
],
};

View file

@ -1,11 +1,9 @@
import recommended from './configs/recommended';
import hydrogen from './configs/hydrogen';
import typescript from './configs/typescript';
import recommended from './config/recommended';
import recommendedTypescript from './config/recommended-typescript';
export {rules} from './rules';
export const configs = {
recommended,
hydrogen,
typescript,
'recommended-typescript': recommendedTypescript,
};

View file

@ -1,7 +1,7 @@
import {noStateInServerComponents} from './no-state-in-server-components';
import {preferImageComponent} from './prefer-image-component';
export const rules: {[key: string]: any} = {
export const rules = {
'no-state-in-server-components': noStateInServerComponents,
'prefer-image-component': preferImageComponent,
};

View file

@ -1,3 +0,0 @@
## Rule Details
This rule prevents using these hooks in files that do not end with the `.client` suffix that denotes a React Component that does not run on the server.

View file

@ -1,3 +0,0 @@
# Prevents `useState` and `useReducer` in React Server Components (`hydrogen/no-state-in-server-components`)
The `useState` and `useReducer` state handling hooks do not function as expected in React Server Components because they execute once per request on the server.

View file

@ -1,9 +0,0 @@
// Examples of **incorrect** code for this rule:
// MyComponent.jsx or MyComponent.server.jsx
import {useState} from 'react';
function MyNonClientComponent() {
const [state, setState] = useState();
return null;
}

View file

@ -1,9 +0,0 @@
// Examples of **correct** code for this rule:
// MyClientComponent.client.jsx
import {useState} from 'react';
function MyClientComponent() {
const [state, setState] = useState();
return null;
}

View file

@ -1,3 +0,0 @@
## Rule Details
This rule prevents using the `img` tag directly and suggests using the `Image` component from `@shopify/hydrogen`.

View file

@ -1,3 +0,0 @@
# Prefer using @shopify/hydrogen `Image` component in place of HTML `img` tags(`hydrogen/prefer-image-component`)
Images can cause layout shifts if they load after the surrounding page has already rendered. This can lead to [Cumulative Layout Shift](https://web.dev/cls/), a [Core Web Vital](https://web.dev/vitals/) that [Google uses in search ranking](https://developers.google.com/search/blog/2020/05/evaluating-page-experience).

View file

@ -1,7 +0,0 @@
// Examples of **incorrect** code for this rule:
function MyComponent() {
return (
<img src="/image.png" alt="My product image" width={300} height={300} />
);
}

View file

@ -1,9 +0,0 @@
// Examples of **correct** code for this rule:
import {Image} from '@shopify/hydrogen';
function MyComponent() {
return (
<Image src="/image.png" alt="My product image" width={300} height={300} />
);
}

View file

@ -16,59 +16,3 @@ function getRepoFromPackageJson(pkg: any) {
return join(repoPathParts.dir, repoPathParts.name, pkg.repository.directory);
}
export const deepCopy = <T>(obj: T): T => {
if (typeof obj === 'object') {
const copyArray = (arr: any[]): any => arr.map((val) => deepCopy(val));
if (obj instanceof Array) return copyArray(obj);
const newObj = {} as T;
for (const key in obj) {
const val = obj[key];
if (val instanceof Array) {
newObj[key] = copyArray(val);
} else if (typeof val === 'object') {
newObj[key] = deepCopy(val);
} else {
newObj[key] = val;
}
}
return newObj;
}
return obj;
};
/**
* Does a shallow merge of object `from` to object `to`.
* Traverses each of the keys in Object `from`, allowing for:
*
* * If the value of a key is an array, it will be concatenated
* onto `to`.
* * If the value of a key is an object it will extend `to` the
* key/values of that object.
*/
export function merge<
F extends object,
T extends object,
R extends F & T = F & T
>(from: F, to: T): R {
const mergedInto = deepCopy(to) as R;
for (const key in from) {
const curKey = key as unknown as keyof R;
const hasKey = mergedInto.hasOwnProperty(key);
const fromVal = from[key];
if (Array.isArray(fromVal)) {
if (!hasKey || !(mergedInto[curKey] instanceof Array))
mergedInto[curKey] = [] as unknown as R[typeof curKey];
(mergedInto[curKey] as unknown as Array<any>).push(...fromVal);
} else if (typeof fromVal === 'object') {
if (!hasKey || !(typeof mergedInto[curKey] === 'object'))
mergedInto[curKey] = {} as unknown as R[typeof curKey];
Object.assign(mergedInto[curKey], fromVal);
} else {
mergedInto[curKey] = fromVal as unknown as R[typeof curKey];
}
}
return mergedInto;
}

View file

@ -1,6 +1,6 @@
{
"name": "@shopify/generate-docs",
"version": "0.6.2",
"version": "0.5.1",
"description": "Command line interface for generating docs from code comments",
"license": "MIT",
"private": true,

View file

@ -142,11 +142,10 @@ export class DocsGen {
await componentResult.docs(componentEntryBase);
await componentResult.writeReadme(componentEntryBase);
if (this.writeDocs) {
componentResult.writeDevDoc(componentPaths.output);
}
await Promise.all([
componentResult.writeReadme(componentEntryBase),
componentResult.writeDevDoc(componentPaths.output),
]);
listItems.push({
name: `<a href="${componentUrl}">${name}</a>`,

View file

@ -24,9 +24,8 @@ export class FileResult {
public async writeDevDoc(path: string) {
const result = inPageAnchors(this.staged.join(''));
const docPath = "https://github.com/Shopify/shopify-dev/blob/master/content/internal/operations/hydrogen-reference-docs.md"
const comment = `<!-- This file is generated from source code in the Shopify/hydrogen repo. Any changes you make here will be overwritten. For more information, refer to ${docPath}. -->`;
const comment = `<!-- This file is generated from the source code and any changes you make here will be overwritten. -->`;
await this.write(path, [this.frontMatter, comment, result].join('\n\n'));
}
@ -38,9 +37,8 @@ export class FileResult {
}
const localPath = path.replace(resolve('.'), '');
const docPath = "https://github.com/Shopify/shopify-dev/blob/master/content/internal/operations/hydrogen-reference-docs.md"
const finalPath = resolve(path, 'README.md');
const comment = `<!-- This file is generated from source code in the Shopify/hydrogen repo. Edit the files in ${localPath} and run 'yarn generate-docs' at the root of this repo. For more information, refer to ${docPath}. -->`;
const comment = `<!-- This file is generated from the source code. Edit the files in ${localPath} and run 'yarn generate-docs' at the root of this repo. -->`;
await this.write(
finalPath,
[comment, ...this.staged].join('\n\n').trim()

View file

@ -7,26 +7,6 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
<!-- ## Unreleased -->
## 0.6.4 - 2021-11-11
- No updates. Transitive dependency bump.
## 0.6.3 - 2021-11-10
- No updates. Transitive dependency bump.
## 0.6.2 - 2021-11-10
- No updates. Transitive dependency bump.
## 0.6.1 - 2021-11-08
- No updates. Transitive dependency bump.
## 0.6.0 - 2021-11-05
- No updates. Transitive dependency bump.
## 0.5.8 - 2021-11-04
- No updates. Transitive dependency bump.

View file

@ -1,6 +1,6 @@
{
"name": "@shopify/hydrogen-plugin-sanity",
"version": "0.6.4",
"version": "0.5.8",
"description": "Hydrogen plugins for integrating with Sanity",
"main": "dist/esnext/index.js",
"publishConfig": {
@ -17,7 +17,7 @@
"prepack": "yarn build"
},
"devDependencies": {
"@shopify/hydrogen": "^0.6.4"
"@shopify/hydrogen": "^0.5.8"
},
"peerDependencies": {
"@shopify/hydrogen": "^0.4.0"

View file

@ -5,44 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
<!-- ## Unreleased -->
## Unreleased
## 0.6.4 - 2021-11-11
- fix: let Vite handle public assets in development
- fix: new lines in hydration request break JSON.parse
- fix(#201): normalize POSIX separators to support windows
- fix: scroll to top on app first load
## 0.6.3 - 2021-11-10
- fix: add trailing slash to user components glob
## 0.6.2 - 2021-11-10
- fix: remove CartProvider from BuyNowButton
- fix: reading property of null for component props
- fix: transform deeply-imported client components
- fix: duplicated files and contexts in browser
## 0.6.1 - 2021-11-08
- No updates. Transitive dependency bump.
- fix: do not set headers after they are sent to client
## 0.6.0 - 2021-11-05
- feat: disable the quantity adjust button when the cart is not idle
- feat: use country server state in cart for the inContext directive
- fix: update interaction prompt and interaction promp style attributes for Model3d
- fix: make sure all errors show an error dialog when hydrogen is in dev mode
- feat: use Image url field instead of deprecated originalSrc field
- feat: switch to unstable API
- fix: MediaFile component warning on non-Model3D types
- fix: remove console logs for caching
- fix: lowercased SVG tags in RSC
- fix: make the URL search property available via hooks
## 0.5.8 - 2021-11-04

View file

@ -1 +1 @@
export * from './dist/esnext/framework/ClientMarker';
export {ClientMarker} from './dist/esnext/framework/ClientMarker';

View file

@ -4,7 +4,7 @@
"access": "public",
"@shopify:registry": "https://registry.npmjs.org"
},
"version": "0.6.4",
"version": "0.5.8",
"description": "Modern custom Shopify storefronts",
"license": "MIT",
"main": "dist/esnext/index.js",
@ -78,18 +78,16 @@
"react": "^18",
"react-dom": "^18",
"react-router-dom": "^5.2.0",
"vite": "^2.6.14"
"vite": "^2.6.0"
},
"dependencies": {
"@vitejs/plugin-react": "^1.0.8",
"@vitejs/plugin-react": "^1.0.0",
"connect": "^3.7.0",
"es-module-lexer": "^0.9.0",
"fast-glob": "^3.2.5",
"graphql": "^15.5.0",
"html-dom-parser": "^1.0.1",
"html-react-parser": "^1.2.6",
"isomorphic-dompurify": "^0.13.0",
"magic-string": "^0.25.7",
"node-fetch": "^2.6.1",
"react-error-boundary": "^3.1.3",
"react-helmet-async": "^1.0.9",

Some files were not shown because too many files have changed in this diff Show more