Skip to content

Commit

Permalink
Added account pages from demo-store (#1410)
Browse files Browse the repository at this point in the history
* Added account pages from demo-store

* Autofocus email and password inputs
  • Loading branch information
davecyen authored Jun 2, 2022
1 parent 3440639 commit e53b974
Show file tree
Hide file tree
Showing 22 changed files with 13,610 additions and 15 deletions.
12,145 changes: 12,145 additions & 0 deletions templates/demo-store-neue/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion templates/demo-store-neue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
"dependencies": {
"@headlessui/react": "^1.6.3",
"@heroicons/react": "^1.0.6",
"@shopify/hydrogen": "^0.21.0",
"@shopify/hydrogen": "^0.22.1",
"clsx": "^1.1.1",
"graphql-tag": "^2.12.6",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-use": "^17.4.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {useMoney} from '@shopify/hydrogen';

/**
* A client component that defines the currency code, currency symbol, and amount of a product
*/
export default function MoneyPrice({money}) {
const {currencyCode, currencyNarrowSymbol, amount} = useMoney(money);
return (
<span className="text-black text-md">
{currencyCode}
{currencyNarrowSymbol}
{amount}
</span>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {useCallback} from 'react';

export default function LogoutButton(props) {
const logout = useCallback(() => {
fetch('/account/logout', {method: 'POST'}).then(
() => (window.location.href = '/'),
);
}, []);
return (
<button {...props} onClick={logout}>
Logout
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
Seo,
useShopQuery,
NoStore,
flattenConnection,
gql,
} from '@shopify/hydrogen';

import Layout from '../layouts/DefaultLayout.server';
import LogoutButton from '../elements/LogoutButton.client';
import MoneyPrice from '../blocks/MoneyPrice.client';

export default function AccountDetails({customerAccessToken}) {
const {data} = useShopQuery({
query: QUERY,
variables: {
customerAccessToken,
},
cache: NoStore(),
});

const customer = data && data.customer;

const orders =
customer?.orders?.edges.length > 0
? flattenConnection(customer.orders)
: [];

const pageHeader = customer
? `Welcome ${customer.firstName || customer.email}!`
: 'Account Details';

return (
<Layout>
<Seo type="noindex" data={{title: 'Account details'}} />
<h1>{pageHeader}</h1>
<div className="flex items-center justify-between">
<LogoutButton className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800">
Logout
</LogoutButton>
</div>
<div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mt-6 mb-4">
<h2>Order History</h2>
<table className="min-w-full table-fixed text-sm text-left mt-6">
<thead>
<tr>
<th>Order</th>
<th>Date</th>
<th>Payment Status</th>
<th>Fulfillment Status</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id}>
<td>#{order.orderNumber}</td>
<td>{new Date(order.processedAt).toDateString()}</td>
<td>{order.financialStatus}</td>
<td>{order.fulfillmentStatus}</td>
<td>
<MoneyPrice money={order.currentTotalPrice} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</Layout>
);
}

const QUERY = gql`
query CustomerDetails($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
firstName
email
orders(first: 250) {
edges {
node {
id
orderNumber
processedAt
financialStatus
fulfillmentStatus
currentTotalPrice {
amount
currencyCode
}
}
}
}
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React from 'react';
import {useNavigate} from '@shopify/hydrogen/client';

export default function AccountActivateForm({id, activationToken}) {
const navigate = useNavigate();

const [submitError, setSubmitError] = React.useState(null);

const [password, setPassword] = React.useState('');
const [passwordError, setPasswordError] = React.useState(null);

const [passwordConfirm, setPasswordConfirm] = React.useState('');
const [passwordConfirmError, setPasswordConfirmError] = React.useState(null);

function passwordValidation(form) {
setPasswordError(null);
setPasswordConfirmError(null);

let hasError = false;

if (!form.password.validity.valid) {
hasError = true;
setPasswordError(
form.password.validity.valueMissing
? 'Please enter a password'
: 'Passwords must be at least 6 characters',
);
}

if (!form.passwordConfirm.validity.valid) {
hasError = true;
setPasswordConfirmError(
form.password.validity.valueMissing
? 'Please re-enter a password'
: 'Passwords must be at least 6 characters',
);
}

if (password !== passwordConfirm) {
hasError = true;
setPasswordConfirmError('The two password entered did not match.');
}

return hasError;
}

async function onSubmit(event) {
event.preventDefault();

if (passwordValidation(event.target)) {
return;
}

const response = await callActivateApi({
id,
activationToken,
password,
});

if (response.error) {
setSubmitError(response.error);
return;
}

navigate('/account');
}

return (
<div className="flex justify-center">
<div className="max-w-md w-full">
<h1 className="text-4xl">Activate Account.</h1>
<p className="mt-4">Create your password to activate your account.</p>
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
{submitError && (
<div className="flex items-center justify-center mb-6 bg-zinc-500">
<p className="m-4 text-s text-white">{submitError}</p>
</div>
)}
<div className="mb-4">
<input
className={`mb-1 appearance-none border w-full py-2 px-3 text-gray-800 placeholder:text-gray-500 leading-tight focus:shadow-outline ${
passwordError ? ' border-red-500' : 'border-gray-900'
}`}
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="Password"
aria-label="Password"
value={password}
minLength={8}
required
onChange={(event) => {
setPassword(event.target.value);
}}
/>
<p
className={`text-red-500 text-xs ${
!passwordError ? 'invisible' : ''
}`}
>
{passwordError} &nbsp;
</p>
</div>
<div className="mb-4">
<input
className={`mb-1 appearance-none border w-full py-2 px-3 text-gray-800 placeholder:text-gray-500 leading-tight focus:shadow-outline ${
passwordConfirmError ? ' border-red-500' : 'border-gray-900'
}`}
id="passwordConfirm"
name="passwordConfirm"
type="password"
autoComplete="current-password"
placeholder="Re-enter password"
aria-label="Re-enter password"
value={passwordConfirm}
required
minLength={8}
onChange={(event) => {
setPasswordConfirm(event.target.value);
}}
/>
<p
className={`text-red-500 text-xs ${
!passwordConfirmError ? 'invisible' : ''
}`}
>
{passwordConfirmError} &nbsp;
</p>
</div>
<div className="flex items-center justify-between">
<button
className="bg-gray-900 text-white uppercase py-2 px-4 focus:shadow-outline block w-full"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
);
}

function callActivateApi({id, activationToken, password}) {
return fetch(`/account/activate`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({id, activationToken, password}),
})
.then((res) => {
if (res.ok) {
return {};
} else {
return res.json();
}
})
.catch((error) => {
return {
error: error.toString(),
};
});
}
Loading

0 comments on commit e53b974

Please sign in to comment.