Thing Time 📦
This commit is contained in:
parent
a9f32048f0
commit
e8e7321062
4
app/.eslintignore
Normal file
4
app/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
build
|
||||
node_modules
|
||||
bin
|
||||
*.d.ts
|
6
app/.eslintrc.js
Normal file
6
app/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @type {import("@types/eslint").Linter.BaseConfig}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['plugin:hydrogen/recommended', 'plugin:hydrogen/typescript'],
|
||||
};
|
8
app/.gitignore
vendored
Normal file
8
app/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
|
||||
/.cache
|
||||
/build
|
||||
/dist
|
||||
/public/build
|
||||
/.mf
|
||||
.env
|
1
app/.graphqlrc.yml
Normal file
1
app/.graphqlrc.yml
Normal file
@ -0,0 +1 @@
|
||||
schema: node_modules/@shopify/hydrogen-react/storefront.schema.json
|
2
app/.npmrc
Normal file
2
app/.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
@shopify:registry=https://registry.npmjs.com
|
||||
progress=false
|
42
app/README.md
Normal file
42
app/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Hydrogen template: Hello World
|
||||
|
||||
Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
|
||||
|
||||
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
|
||||
[Get familiar with Remix](https://remix.run/docs/en/v1)
|
||||
|
||||
## What's included
|
||||
|
||||
- Remix
|
||||
- Hydrogen
|
||||
- Oxygen
|
||||
- Shopify CLI
|
||||
- ESLint
|
||||
- Prettier
|
||||
- GraphQL generator
|
||||
- TypeScript and JavaScript flavors
|
||||
- Minimal setup of components and routes
|
||||
|
||||
## Getting started
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Node.js version 16.14.0 or higher
|
||||
|
||||
```bash
|
||||
npm create @shopify/hydrogen@latest --template hello-world
|
||||
```
|
||||
|
||||
Remember to update `.env` with your shop's domain and Storefront API token!
|
||||
|
||||
## Building for production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
26292
app/package-lock.json
generated
Normal file
26292
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
app/package.json
Normal file
41
app/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"build": "shopify hydrogen build",
|
||||
"dev": "shopify hydrogen dev",
|
||||
"preview": "npm run build && shopify hydrogen preview",
|
||||
"lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"g": "shopify hydrogen generate"
|
||||
},
|
||||
"prettier": "@shopify/prettier-config",
|
||||
"dependencies": {
|
||||
"@remix-run/react": "1.12.0",
|
||||
"@shopify/cli": "3.29.0",
|
||||
"@shopify/cli-hydrogen": "^4.0.8",
|
||||
"@shopify/hydrogen": "^2023.1.5",
|
||||
"@shopify/remix-oxygen": "^1.0.3",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "1.12.0",
|
||||
"@shopify/oxygen-workers-types": "^3.17.2",
|
||||
"@shopify/prettier-config": "^1.1.2",
|
||||
"@types/eslint": "^8.4.10",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-plugin-hydrogen": "0.12.2",
|
||||
"prettier": "^2.8.4",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
}
|
||||
}
|
28
app/public/favicon.svg
Normal file
28
app/public/favicon.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
||||
<style>
|
||||
.stroke {
|
||||
stroke: #000;
|
||||
}
|
||||
.fill {
|
||||
fill: #000;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.stroke {
|
||||
stroke: #fff;
|
||||
}
|
||||
.fill {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
class="stroke"
|
||||
fill-rule="evenodd"
|
||||
d="M16.1 16.04 1 8.02 6.16 5.3l5.82 3.09 4.88-2.57-5.82-3.1L16.21 0l15.1 8.02-5.17 2.72-5.5-2.91-4.88 2.57 5.5 2.92-5.16 2.72Z"
|
||||
/>
|
||||
<path
|
||||
class="fill"
|
||||
fill-rule="evenodd"
|
||||
d="M16.1 32 1 23.98l5.16-2.72 5.82 3.08 4.88-2.57-5.82-3.08 5.17-2.73 15.1 8.02-5.17 2.72-5.5-2.92-4.88 2.58 5.5 2.92L16.1 32Z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 690 B |
19
app/remix.config.js
Normal file
19
app/remix.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
appDirectory: 'src',
|
||||
ignoredRouteFiles: ['**/.*'],
|
||||
watchPaths: ['./public'],
|
||||
server: './server.ts',
|
||||
/**
|
||||
* The following settings are required to deploy Hydrogen apps to Oxygen:
|
||||
*/
|
||||
publicPath: (process.env.HYDROGEN_ASSET_BASE_URL ?? '/') + 'build/',
|
||||
assetsBuildDirectory: 'dist/client/build',
|
||||
serverBuildPath: 'dist/worker/index.js',
|
||||
serverMainFields: ['browser', 'module', 'main'],
|
||||
serverConditions: ['worker', process.env.NODE_ENV],
|
||||
serverDependenciesToBundle: 'all',
|
||||
serverModuleFormat: 'esm',
|
||||
serverPlatform: 'neutral',
|
||||
serverMinify: process.env.NODE_ENV === 'production',
|
||||
};
|
36
app/remix.env.d.ts
vendored
Normal file
36
app/remix.env.d.ts
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@shopify/remix-oxygen" />
|
||||
/// <reference types="@shopify/oxygen-workers-types" />
|
||||
|
||||
import type {Storefront} from '@shopify/hydrogen';
|
||||
import type {HydrogenSession} from '../server';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* A global `process` object is only available during build to access NODE_ENV.
|
||||
*/
|
||||
const process: {env: {NODE_ENV: 'production' | 'development'}};
|
||||
|
||||
/**
|
||||
* Declare expected Env parameter in fetch handler.
|
||||
*/
|
||||
interface Env {
|
||||
SESSION_SECRET: string;
|
||||
PUBLIC_STOREFRONT_API_TOKEN: string;
|
||||
PRIVATE_STOREFRONT_API_TOKEN: string;
|
||||
PUBLIC_STOREFRONT_API_VERSION: string;
|
||||
PUBLIC_STORE_DOMAIN: string;
|
||||
PUBLIC_STOREFRONT_ID: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare local additions to `AppLoadContext` to include the session utilities we injected in `server.ts`.
|
||||
*/
|
||||
declare module '@shopify/remix-oxygen' {
|
||||
export interface AppLoadContext {
|
||||
session: HydrogenSession;
|
||||
storefront: Storefront;
|
||||
env: Env;
|
||||
}
|
||||
}
|
131
app/server.ts
Normal file
131
app/server.ts
Normal file
@ -0,0 +1,131 @@
|
||||
// Virtual entry point for the app
|
||||
import * as remixBuild from '@remix-run/dev/server-build';
|
||||
import {createStorefrontClient, storefrontRedirect} from '@shopify/hydrogen';
|
||||
import {
|
||||
createRequestHandler,
|
||||
getBuyerIp,
|
||||
createCookieSessionStorage,
|
||||
type SessionStorage,
|
||||
type Session,
|
||||
} from '@shopify/remix-oxygen';
|
||||
|
||||
/**
|
||||
* Export a fetch handler in module format.
|
||||
*/
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
executionContext: ExecutionContext,
|
||||
): Promise<Response> {
|
||||
try {
|
||||
/**
|
||||
* Open a cache instance in the worker and a custom session instance.
|
||||
*/
|
||||
if (!env?.SESSION_SECRET) {
|
||||
throw new Error('SESSION_SECRET environment variable is not set');
|
||||
}
|
||||
|
||||
const waitUntil = (p: Promise<any>) => executionContext.waitUntil(p);
|
||||
const [cache, session] = await Promise.all([
|
||||
caches.open('hydrogen'),
|
||||
HydrogenSession.init(request, [env.SESSION_SECRET]),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Create Hydrogen's Storefront client.
|
||||
*/
|
||||
const {storefront} = createStorefrontClient({
|
||||
cache,
|
||||
waitUntil,
|
||||
buyerIp: getBuyerIp(request),
|
||||
i18n: {language: 'EN', country: 'US'},
|
||||
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
|
||||
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
|
||||
storeDomain: `https://${env.PUBLIC_STORE_DOMAIN}`,
|
||||
storefrontApiVersion: env.PUBLIC_STOREFRONT_API_VERSION || '2023-01',
|
||||
storefrontId: env.PUBLIC_STOREFRONT_ID,
|
||||
requestGroupId: request.headers.get('request-id'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a Remix request handler and pass
|
||||
* Hydrogen's Storefront client to the loader context.
|
||||
*/
|
||||
const handleRequest = createRequestHandler({
|
||||
build: remixBuild,
|
||||
mode: process.env.NODE_ENV,
|
||||
getLoadContext: () => ({session, storefront, env}),
|
||||
});
|
||||
|
||||
const response = await handleRequest(request);
|
||||
|
||||
if (response.status === 404) {
|
||||
/**
|
||||
* Check for redirects only when there's a 404 from the app.
|
||||
* If the redirect doesn't exist, then `storefrontRedirect`
|
||||
* will pass through the 404 response.
|
||||
*/
|
||||
return storefrontRedirect({request, response, storefront});
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
return new Response('An unexpected error occurred', {status: 500});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a custom session implementation for your Hydrogen shop.
|
||||
* Feel free to customize it to your needs, add helper methods, or
|
||||
* swap out the cookie-based implementation with something else!
|
||||
*/
|
||||
class HydrogenSession {
|
||||
constructor(
|
||||
private sessionStorage: SessionStorage,
|
||||
private session: Session,
|
||||
) {}
|
||||
|
||||
static async init(request: Request, secrets: string[]) {
|
||||
const storage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: 'session',
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
secrets,
|
||||
},
|
||||
});
|
||||
|
||||
const session = await storage.getSession(request.headers.get('Cookie'));
|
||||
|
||||
return new this(storage, session);
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this.session.get(key);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
return this.sessionStorage.destroySession(this.session);
|
||||
}
|
||||
|
||||
flash(key: string, value: any) {
|
||||
this.session.flash(key, value);
|
||||
}
|
||||
|
||||
unset(key: string) {
|
||||
this.session.unset(key);
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.session.set(key, value);
|
||||
}
|
||||
|
||||
commit() {
|
||||
return this.sessionStorage.commitSession(this.session);
|
||||
}
|
||||
}
|
4
app/src/entry.client.tsx
Normal file
4
app/src/entry.client.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import {RemixBrowser} from '@remix-run/react';
|
||||
import {hydrateRoot} from 'react-dom/client';
|
||||
|
||||
hydrateRoot(document, <RemixBrowser />);
|
21
app/src/entry.server.tsx
Normal file
21
app/src/entry.server.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import type {EntryContext} from '@shopify/remix-oxygen';
|
||||
import {RemixServer} from '@remix-run/react';
|
||||
import {renderToReadableStream} from 'react-dom/server';
|
||||
|
||||
export default async function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
) {
|
||||
const body = await renderToReadableStream(
|
||||
<RemixServer context={remixContext} url={request.url} />,
|
||||
);
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
|
||||
return new Response(body, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
72
app/src/root.tsx
Normal file
72
app/src/root.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import {
|
||||
type LinksFunction,
|
||||
type MetaFunction,
|
||||
type LoaderArgs,
|
||||
} from '@shopify/remix-oxygen';
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useLoaderData,
|
||||
} from '@remix-run/react';
|
||||
import type {Shop} from '@shopify/hydrogen/storefront-api-types';
|
||||
import styles from './styles/app.css';
|
||||
import favicon from '../public/favicon.svg';
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [
|
||||
{rel: 'stylesheet', href: styles},
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://cdn.shopify.com',
|
||||
},
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://shop.app',
|
||||
},
|
||||
{rel: 'icon', type: 'image/svg+xml', href: favicon},
|
||||
];
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => ({
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width,initial-scale=1',
|
||||
});
|
||||
|
||||
export async function loader({context}: LoaderArgs) {
|
||||
const layout = await context.storefront.query<{shop: Shop}>(LAYOUT_QUERY);
|
||||
return {layout};
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const {name} = data.layout.shop;
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello, {name}</h1>
|
||||
<p>This is a custom storefront powered by Hydrogen</p>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
const LAYOUT_QUERY = `#graphql
|
||||
query layout {
|
||||
shop {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
31
app/src/styles/app.css
Normal file
31
app/src/styles/app.css
Normal file
@ -0,0 +1,31 @@
|
||||
body {
|
||||
margin: 0;
|
||||
background: #FFFFFF;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
}
|
25
app/tsconfig.json
Normal file
25
app/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2022",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["@shopify/oxygen-workers-types"],
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
},
|
||||
|
||||
// Remix takes care of building everything in `./app` with `remix build`.
|
||||
// Wrangler takes care of building everything in `./worker` with `wrangler start` / `wrangler publish`.
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
9
ecosystem.config.js
Normal file
9
ecosystem.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
script: 'npm run app',
|
||||
name: "thingtime-app",
|
||||
namespace: "thingtime"
|
||||
}
|
||||
],
|
||||
};
|
Loading…
Reference in New Issue
Block a user