Skip to content
This repository was archived by the owner on Jan 21, 2024. It is now read-only.

Commit 5bd497e

Browse files
author
builtbyvys
committed
⬆ - Push local files
1 parent 955a24d commit 5bd497e

38 files changed

+6169
-0
lines changed

.env

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Swell Store
2+
NEXT_PUBLIC_SWELL_STORE_ID='thrifthaven'
3+
NEXT_PUBLIC_SWELL_PUBLIC_KEY='pk_test_iVudBxa1qlMC7MxNaQw5czc7dmbJWiAg'
4+
5+
# Clerk
6+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_dXByaWdodC1jYXJkaW5hbC0zMy5jbGVyay5hY2NvdW50cy5kZXYk
7+
CLERK_SECRET_KEY=sk_test_f80Me6dpSa5cP7rW9YQLbXoREiT40G5RVtsSomTErD

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

.gitignore

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel

.prettierrc

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"arrowParens": "avoid",
3+
"singleQuote": true,
4+
"jsxSingleQuote": true,
5+
"tabWidth": 2,
6+
"trailingComma": "none",
7+
"semi": false,
8+
"proseWrap": "always",
9+
"printWidth": 80,
10+
"plugins": ["prettier-plugin-tailwindcss"]
11+
}

.vscode/settings.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"typescript.tsdk": "node_modules\\.pnpm\\[email protected]\\node_modules\\typescript\\lib",
3+
"typescript.enablePromptUseWorkspaceTsdk": true
4+
}

app/api/hello/route.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NextResponse } from 'next/server'
2+
import { currentUser } from '@clerk/nextjs/app-beta'
3+
4+
export async function GET() {
5+
const user = await currentUser()
6+
7+
if (!user) {
8+
return NextResponse.json({ message: 'You are not logged in.' })
9+
}
10+
11+
return NextResponse.json({ name: user.firstName })
12+
}

app/components/cart-slider.jsx

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
'use client'
2+
3+
import { Fragment, useState, useTransition } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
6+
import Image from 'next/image'
7+
import Link from 'next/link'
8+
9+
import { Blinker } from '@/components/ui/loading'
10+
import { formatCurrency } from '@/lib/utils'
11+
12+
import { Dialog, Transition } from '@headlessui/react'
13+
import { XMarkIcon } from '@heroicons/react/24/outline'
14+
import { removeFromCart } from '@/lib/swell/cart'
15+
import { useSWRConfig } from 'swr'
16+
17+
const CartSlider = ({ cart, cartIsLoading, open, setCartSliderIsOpen }) => {
18+
const router = useRouter()
19+
const { mutate } = useSWRConfig()
20+
const [isPending, startTransition] = useTransition()
21+
const [loading, setLoading] = useState(false)
22+
const isMutating = loading || isPending
23+
24+
const removeItem = async itemId => {
25+
setLoading(true)
26+
await removeFromCart(itemId)
27+
setLoading(false)
28+
mutate('cart')
29+
startTransition(() => {
30+
router.refresh()
31+
})
32+
}
33+
34+
return (
35+
<Transition.Root show={open} as={Fragment}>
36+
<Dialog as='div' className='relative z-10' onClose={setCartSliderIsOpen}>
37+
<Transition.Child
38+
as={Fragment}
39+
enter='ease-in-out duration-500'
40+
enterFrom='opacity-0'
41+
enterTo='opacity-100'
42+
leave='ease-in-out duration-500'
43+
leaveFrom='opacity-100'
44+
leaveTo='opacity-0'
45+
>
46+
<div className='fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity' />
47+
</Transition.Child>
48+
49+
<div className='fixed inset-0 overflow-hidden'>
50+
<div className='absolute inset-0 overflow-hidden'>
51+
<div className='pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10'>
52+
<Transition.Child
53+
as={Fragment}
54+
enter='transform transition ease-in-out duration-500 sm:duration-700'
55+
enterFrom='translate-x-full'
56+
enterTo='translate-x-0'
57+
leave='transform transition ease-in-out duration-500 sm:duration-700'
58+
leaveFrom='translate-x-0'
59+
leaveTo='translate-x-full'
60+
>
61+
<Dialog.Panel className='pointer-events-auto w-screen max-w-md'>
62+
<div className='flex h-full flex-col overflow-y-scroll bg-white shadow-xl'>
63+
<div className='flex-1 overflow-y-auto py-6 px-4 sm:px-6'>
64+
<div className='flex items-start justify-between'>
65+
<Dialog.Title className='text-lg font-medium text-gray-900'>
66+
{' '}
67+
Shopping cart{' '}
68+
</Dialog.Title>
69+
<div className='ml-3 flex h-7 items-center'>
70+
<button
71+
type='button'
72+
className='-m-2 p-2 text-gray-400 hover:text-gray-500'
73+
onClick={() => setCartSliderIsOpen(false)}
74+
>
75+
<span className='sr-only'>Close panel</span>
76+
<XMarkIcon className='h-6 w-6' aria-hidden='true' />
77+
</button>
78+
</div>
79+
</div>
80+
81+
<div className='mt-8'>
82+
<div className='flow-root'>
83+
<ul
84+
role='list'
85+
className='-my-6 divide-y divide-gray-200'
86+
>
87+
{cart?.items?.length > 0 &&
88+
cart.items.map(item => (
89+
<li key={item.id} className='flex py-6'>
90+
<div className='relative h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200'>
91+
<Image
92+
src={item.product.images[0].file.url}
93+
alt={item.product.name}
94+
className='h-full w-full object-cover object-center'
95+
layout='fill'
96+
/>
97+
</div>
98+
99+
<div className='ml-4 flex flex-1 flex-col'>
100+
<div>
101+
<div className='flex justify-between text-base font-medium text-gray-900'>
102+
<h3>
103+
<a
104+
href={`/products/${item.product.slug}`}
105+
>
106+
{' '}
107+
{item.product.name}{' '}
108+
</a>
109+
</h3>
110+
<p className='ml-4'>
111+
{formatCurrency({
112+
amount: item.price_total
113+
})}
114+
</p>
115+
</div>
116+
<p className='mt-1 text-sm text-gray-500'>
117+
{item.product.name}
118+
</p>
119+
</div>
120+
<div className='flex flex-1 items-end justify-between text-sm'>
121+
<p className='text-gray-500'>
122+
Qty {item.quantity}
123+
</p>
124+
125+
<div className='flex'>
126+
<button
127+
type='button'
128+
disabled={isMutating}
129+
onClick={() => removeItem(item.id)}
130+
className='font-medium text-pink-600 hover:text-pink-500 disabled:cursor-not-allowed disabled:opacity-50'
131+
>
132+
Remove
133+
</button>
134+
</div>
135+
</div>
136+
</div>
137+
</li>
138+
))}
139+
</ul>
140+
</div>
141+
</div>
142+
</div>
143+
144+
<div className='border-t border-gray-200 py-6 px-4 sm:px-6'>
145+
<div className='flex justify-between text-base font-medium text-gray-900'>
146+
<p>Subtotal</p>
147+
<p>
148+
{formatCurrency({ amount: cart?.sub_total || 0 })}
149+
</p>
150+
</div>
151+
<p className='mt-0.5 text-sm text-gray-500'>
152+
Shipping and taxes calculated at checkout.
153+
</p>
154+
155+
{cart?.checkout_url && (
156+
<div className='mt-6'>
157+
<Link href={cart.checkout_url}>
158+
<button
159+
disabled={cartIsLoading}
160+
className='flex h-12 w-full items-center justify-center rounded-md border border-transparent bg-cyan-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-cyan-700 disabled:cursor-not-allowed disabled:opacity-75'
161+
>
162+
{cartIsLoading ? <Blinker /> : 'Checkout'}
163+
</button>
164+
</Link>
165+
</div>
166+
)}
167+
168+
<div className='mt-6 flex justify-center text-center text-sm text-gray-500'>
169+
<p>
170+
or{' '}
171+
<button
172+
type='button'
173+
className='font-medium text-cyan-600 hover:text-cyan-500'
174+
onClick={() => setCartSliderIsOpen(false)}
175+
>
176+
Continue Shopping
177+
<span aria-hidden='true'> &rarr;</span>
178+
</button>
179+
</p>
180+
</div>
181+
</div>
182+
</div>
183+
</Dialog.Panel>
184+
</Transition.Child>
185+
</div>
186+
</div>
187+
</div>
188+
</Dialog>
189+
</Transition.Root>
190+
)
191+
}
192+
193+
export default CartSlider

app/components/layout/footer.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const Footer = () => {
2+
return (
3+
<footer className='z-10 py-10 text-stone-400'>
4+
<div className='container'>
5+
<p className='mt-4 text-sm text-stone-500'>
6+
&copy; {new Date().getFullYear()} Thrifthaven
7+
</p>
8+
<div className='text-sm text-stone-400'>
9+
Developed by{' '}
10+
<a
11+
className='text-sky-600'
12+
href='https://github.com/builtbyvys'
13+
rel='noreferrer'
14+
target='_blank'
15+
>
16+
Yamil
17+
</a>{' '}
18+
using{' '}
19+
<a
20+
className='text-sky-600'
21+
href='https://www.swell.is/'
22+
rel='noreferrer'
23+
target='_blank'
24+
>
25+
Swell
26+
</a>
27+
.
28+
</div>
29+
</div>
30+
</footer>
31+
)
32+
}
33+
34+
export default Footer

app/components/layout/header.tsx

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
5+
import Link from 'next/link'
6+
import useSWR from 'swr'
7+
import CartSlider from '@/components/cart-slider'
8+
9+
import { getCart } from '@/lib/swell/cart'
10+
import { ShoppingCartIcon } from '@heroicons/react/24/outline'
11+
12+
import { SignInButton, UserButton } from '@clerk/nextjs'
13+
import { SignedIn, SignedOut } from '@clerk/nextjs/app-beta/client'
14+
15+
const Header = () => {
16+
const { data: cart, isLoading } = useSWR('cart', getCart)
17+
const [cartSliderIsOpen, setCartSliderIsOpen] = useState(false)
18+
19+
return (
20+
<>
21+
<header className='z-10 py-10 text-stone-400'>
22+
<nav className='container flex items-center justify-between'>
23+
{/* Logo */}
24+
<div>
25+
<Link
26+
href='/'
27+
className='text-2xl font-bold uppercase tracking-widest'
28+
>
29+
SHOP
30+
</Link>
31+
</div>
32+
33+
{/* Nav links */}
34+
<ul className='flex items-center gap-10'>
35+
<li className='text-sm font-medium uppercase tracking-wider'>
36+
<Link href='/products'>Products</Link>
37+
</li>
38+
<SignedIn>
39+
<li className='text-sm font-medium uppercase tracking-wider'>
40+
<Link href='/dashboard'>Dashboard</Link>
41+
</li>
42+
</SignedIn>
43+
</ul>
44+
45+
{/* Shopping cart */}
46+
<div className='flex items-center justify-between gap-6'>
47+
<button
48+
className='flex items-center gap-x-2 pl-4'
49+
onClick={() => setCartSliderIsOpen(open => !open)}
50+
>
51+
<ShoppingCartIcon className='h-7 w-7' />
52+
53+
{cart?.item_quantity ? (
54+
<span className='flex h-5 w-5 items-center justify-center rounded bg-sky-600 text-xs font-medium text-white'>
55+
{cart?.item_quantity}
56+
</span>
57+
) : null}
58+
</button>
59+
<SignedIn>
60+
<UserButton />
61+
</SignedIn>
62+
<SignedOut>
63+
<SignInButton mode='modal'>
64+
<button className='rounded border border-gray-400 px-3 py-0.5'>
65+
Sign in
66+
</button>
67+
</SignInButton>
68+
</SignedOut>
69+
</div>
70+
</nav>
71+
</header>
72+
<CartSlider
73+
cart={cart}
74+
cartIsLoading={isLoading}
75+
open={cartSliderIsOpen}
76+
setCartSliderIsOpen={setCartSliderIsOpen}
77+
/>
78+
</>
79+
)
80+
}
81+
82+
export default Header

0 commit comments

Comments
 (0)