Skip to content

Commit

Permalink
V2
Browse files Browse the repository at this point in the history
  • Loading branch information
zackha committed Aug 14, 2024
1 parent 508c4d3 commit dc1eead
Show file tree
Hide file tree
Showing 34 changed files with 9,956 additions and 0 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GQL_HOST="https://example.com/graphql"
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Release

on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- uses: actions/setup-node@v3
with:
node-version: 18.x

- run: npx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<p align="center">
<img width="64" align="center" src="https://github.com/zackha/nuxtcommerce/assets/79358543/ac0ae5da-b077-4c5d-9e56-23a6b16498ed">
</p>
<h1 align="center">
NuxtCommerce
</h1>
<p align="center">
An open-source, dynamic e-commerce solution powered by Nuxt 3 and GraphQL, headless storefront replacement for Woocommerce. Featuring a user interface in the style of Pinterest and fully customizable (Vue, Nuxt3).
</p>

### [🚀 Live Demo](https://nuxtcommerce.netlify.app/)

## Introduction

NuxtCommerce is a dynamic and lively e-commerce platform developed with Nuxt 3. Developed for WooCommerce, NuxtCommerce optimizes data flow with [WPGraphQL](https://github.com/wp-graphql/wp-graphql), offering an efficient shopping experience. It stands out with its Pinterest-style user-friendly interface and fashion-oriented structure. With its dark mode feature and open-source nature, it offers flexibility and continuous development opportunities.

If your product stocks and prices are not changeable, and you are not continuously uploading new products, it could be beneficial for you to opt for [Woonuxt](https://github.com/scottyzen/woonuxt#readme). This project, developed by [scottyzen](https://github.com/scottyzen), is static, thus providing a faster solution.

## Stack

- Nuxt3 / Vue
- Headless Storefront
- GraphQL with Apollo Client
- Tailwind CSS
- Pinterest Interface
- Developed for WooCommerce
- Dynamic
- Open Source
- Suitable for Fashion Category
- Dark Mode

## Contributing

Contributions of any kind are welcome! You can open an issue for requests, bug reports, or general feedback, or you can directly create a pull request(PR).

## Contact

Don't hesitate to get in touch if you have any questions or suggestions:

Email: zckhtln@icloud.com</br>
Twitter: [@ZHatlen](https://twitter.com/ZHatlen)

![Nuxtcommerce](https://github.com/zackha/nuxtcommerce/assets/79358543/0fbd5415-f449-4a7f-9031-33f8be0e447c)
5 changes: 5 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineAppConfig({
ui: {
primary: 'red',
},
});
28 changes: 28 additions & 0 deletions app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<AppHeader />
<div class="pt-20 min-h-[calc(100vh-72px)]">
<NuxtPage />
</div>
<AppFooter />
</template>

<style lang="postcss">
.dark {
@apply bg-black text-neutral-100;
color-scheme: dark;
}
.dropdown-enter-active {
@apply transition duration-200 ease-out;
}
.dropdown-enter-from,
.dropdown-leave-to {
@apply translate-y-5 opacity-0;
}
.dropdown-enter-to,
.dropdown-leave-from {
@apply transform opacity-100;
}
.dropdown-leave-active {
@apply transition duration-150 ease-in;
}
</style>
28 changes: 28 additions & 0 deletions components/AppFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup>
const config = useRuntimeConfig();
const colorMode = useColorMode();
const toggleDark = () => {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';
};
const colorModeIcon = computed(() => (colorMode.preference === 'dark' ? 'i-iconamoon-mode-dark-fill' : 'i-iconamoon-mode-light-fill'));
</script>

<template>
<div class="my-5 flex items-center justify-between px-5 text-[13px] font-semibold text-secondary-text dark:text-secondary-text-d">
<div>
<a class="hover:text-black hover:dark:text-neutral-100" href="https://github.com/zackha/nuxtcommerce" target="_blank">NuxtCommerce v{{ config.public.version }}</a>
- by
<a class="hover:text-black hover:dark:text-neutral-100" href="https://zackha.com" target="_blank">Sefa Bulak</a>
</div>
<div>
<button
@click="toggleDark"
class="box-border flex h-8 items-center gap-1.5 rounded-lg p-2 transition-all bg-neutral-800/5 hover:bg-neutral-800/10 hover:text-black active:scale-95 dark:bg-white/10 hover:dark:bg-white/20 hover:dark:text-neutral-100">
<div class="flex"><UIcon :name="colorModeIcon" size="16" /></div>
<div class="capitalize leading-3">{{ colorMode.preference }}</div>
</button>
</div>
</div>
</template>
191 changes: 191 additions & 0 deletions components/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<script setup>
const router = useRouter();
const route = useRoute();
const searchQuery = ref((route.query.q || '').toString());
const searchResults = ref([]);
const isLoading = ref(false);
const suggestionMenu = ref(false);
const suggestionMenuRef = ref(null);
const search = () => {
router.push({ path: '/', query: { ...route.query, q: searchQuery.value || undefined } });
suggestionMenu.value = false;
};
async function fetch() {
try {
const response = await searchProducts(searchQuery.value);
searchResults.value = response.products.nodes;
} finally {
isLoading.value = false;
}
}
onMounted(fetch);
const throttledFetch = useDebounceFn(async () => {
await fetch();
}, 300);
watch(
() => searchQuery.value,
() => {
isLoading.value = true;
throttledFetch();
}
);
const clearSearch = () => {
suggestionMenu.value = false;
searchQuery.value = '';
router.push({ query: { ...route.query, q: undefined } });
};
onClickOutside(suggestionMenuRef, event => {
suggestionMenu.value = false;
});
</script>

<template>
<div class="flex w-full flex-row items-center px-3 lg:px-5 h-20 z-50 fixed bg-white/85 dark:bg-black/85 backdrop-blur-sm dark:backdrop-blur-lg">
<div class="flex flex-row w-full flex-nowrap items-center gap-2">
<NuxtLink
class="flex items-center justify-center min-w-[52px] min-h-[52px] max-lg:min-w-12 max-lg:min-h-12 hover:bg-black/5 hover:dark:bg-white/15 max-lg:dark:bg-white/15 max-lg:bg-black/5 max-lg:hover:bg-black/10 max-lg:hover:dark:bg-white/20 rounded-2xl max-lg:rounded-full transition active:scale-95"
to="/">
<svg viewBox="0 0 30.72 30.72" class="rounded-lg max-lg:rounded-full bg-[#b31015] w-8 h-8">
<path
d="M -1e-4,1e-4 H 15.3296 C 14.7944,0.0047 14.2692,0.1464 13.8054,0.4117 13.334,0.6813 12.9429,1.0691 12.6707,1.536 L -1e-4,23.2893 Z m 15.3807,0 h 15.3392 v 5.1132 c -0.4077,-0.1874 -0.8524,-0.2855 -1.304,-0.2855 -0.5439,0 -1.0786,0.142 -1.5494,0.4116 -0.4711,0.2696 -0.8623,0.6577 -1.1341,1.1245 L 23.7908,11.4167 18.0395,1.536 C 17.7674,1.0691 17.376,0.6813 16.9048,0.4117 16.4411,0.1464 15.9158,0.0047 15.3806,1e-4 Z M 30.7198,13.6563 V 25.8989 H 26.6036 L 23.791,30.7198 H 11.8305 c 4.2401,-0.0117 7.3693,-1.8658 9.5244,-5.4729 l 5.2487,-9.0088 2.8114,-4.8214 z M 11.6157,25.8941 4.1115,25.8924 15.3602,6.5839 l 5.6126,9.6542 -3.7579,6.4525 c -1.4357,2.348 -3.0668,3.2035 -5.5992,3.2035 z"
fill="#ed3237" />
</svg>
</NuxtLink>
<NuxtLink
exactActiveClass="bg-black dark:bg-white text-white dark:text-black"
class="font-semibold cursor-pointer px-4 rounded-full hover:bg-black hover:dark:bg-white h-12 items-center justify-center hover:text-white hover:dark:text-black transition active:scale-95 lg:flex hidden"
to="/categories">
Categories
</NuxtLink>
<NuxtLink
exactActiveClass="bg-black dark:bg-white text-white dark:text-black"
class="font-semibold cursor-pointer px-4 rounded-full hover:bg-black hover:dark:bg-white h-12 items-center justify-center hover:text-white hover:dark:text-black transition active:scale-95 lg:flex hidden"
to="/favorites">
Favorites
</NuxtLink>
<NuxtLink
exactActiveClass="!bg-black/10 dark:!bg-white/30"
class="lg:hidden flex items-center justify-center min-w-12 min-h-12 rounded-full bg-black/5 dark:bg-white/15 hover:bg-black/10 hover:dark:bg-white/20 transition active:scale-95"
to="/categories">
<UIcon class="text-[#5f5f5f] dark:text-[#b7b7b7]" name="i-iconamoon-category-fill" size="26" />
</NuxtLink>
<NuxtLink
exactActiveClass="!bg-black/10 dark:!bg-white/30"
class="lg:hidden flex items-center justify-center min-w-12 min-h-12 rounded-full bg-black/5 dark:bg-white/15 hover:bg-black/10 hover:dark:bg-white/20 transition active:scale-95"
to="/favorites">
<UIcon class="text-[#5f5f5f] dark:text-[#b7b7b7]" name="i-iconamoon-heart-fill" size="26" />
</NuxtLink>
<div class="flex flex-shrink flex-grow flex-col text-sm font-semibold text-[#111] dark:text-[#eee]">
<div
:class="[
'flex h-12 flex-grow rounded-full pl-4 pr-3 transition-all hover:bg-black/10 hover:dark:bg-white/20',
suggestionMenu ? 'bg-black/10 dark:bg-white/20' : 'bg-black/5 dark:bg-white/15',
]">
<div @click="suggestionMenu = true" class="flex w-full items-center gap-4">
<div v-if="!suggestionMenu" class="flex text-neutral-500 dark:text-neutral-400">
<UIcon name="i-iconamoon-search-bold" size="20" />
</div>
<div class="flex w-full">
<input
class="w-full bg-transparent py-2 outline-none placeholder:text-[#757575] placeholder:dark:text-neutral-400"
type="text"
v-model="searchQuery"
@keyup.enter="search"
:placeholder="route.query.category ? `Search in ${route.query.category}...` : 'Search...'" />
<div v-if="searchQuery || suggestionMenu" @click.stop="clearSearch" class="flex items-center justify-center cursor-pointer transition-all">
<UIcon v-if="!isLoading" class="text-black dark:text-white" name="i-iconamoon-close-circle-1-fill" size="24" />
<UIcon v-else name="i-svg-spinners-bars-rotate-fade" size="20" />
</div>
</div>
</div>
</div>
</div>
<div
class="hover:bg-black/5 hover:dark:bg-white/15 max-lg:dark:bg-white/15 max-lg:bg-black/5 max-lg:hover:bg-black/10 max-lg:hover:dark:bg-white/20 min-w-12 min-h-12 flex items-center justify-center rounded-full cursor-pointer">
<UIcon class="text-[#5f5f5f] dark:text-[#b7b7b7]" name="i-iconamoon-shopping-bag-fill" size="26" />
</div>
</div>
</div>
<div
v-if="suggestionMenu"
ref="suggestionMenuRef"
class="fixed top-20 left-0 right-0 z-50 bg-white/85 dark:bg-black/85 backdrop-blur-sm dark:backdrop-blur-lg lg:rounded-b-3xl w-full">
<div class="max-h-[calc(100vh-80px)] overflow-auto">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-80">
<div class="bg-black/10 dark:bg-white/20 flex rounded-full w-12 h-12 items-center justify-center skeleton">
<UIcon class="text-white dark:text-black" name="i-svg-spinners-8-dots-rotate" size="26" />
</div>
</div>
<!-- Empty State -->
<div v-else-if="!searchResults.length" class="w-full items-center flex flex-col justify-center text-center p-8">
<div class="w-28 h-28 bg-black/10 dark:bg-white/20 rounded-full items-center justify-center flex">
<UIcon name="i-iconamoon-search-bold" class="w-16 h-16 dark:text-white" />
</div>
<div class="font-semibold text-3xl my-6">
No items matching for:
<strong>{{ searchQuery }}</strong>
</div>
<div class="text-sm text-center mb-5">
Try improving your results by double checking your spelling
<br />
or trying a more general keyword.
</div>
</div>
<!-- Results State-->
<div v-else class="mx-auto p-4 max-w-screen-2xl">
<h2 v-if="!searchQuery" class="text-2xl font-bold tracking-tight">New Products</h2>
<div class="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5 mt-5">
<NuxtLink
@click="suggestionMenu = false"
:to="`/product/${product.slug}-${product.sku.split('-')[0]}`"
v-for="(product, i) in searchResults"
:key="i"
class="group select-none">
<div class="cursor-pointer transition ease-[ease] duration-300">
<div class="relative pb-[133%] dark:shadow-[0_8px_24px_rgba(0,0,0,.5)] rounded-2xl overflow-hidden">
<NuxtImg
loading="lazy"
:title="product.name"
:src="product.galleryImages.nodes[0].sourceUrl"
class="absolute h-full w-full dark:bg-neutral-800 bg-neutral-200 object-cover" />
<NuxtImg
loading="lazy"
:title="product.name"
:src="product.image.sourceUrl"
class="absolute h-full w-full dark:bg-neutral-800 bg-neutral-200 object-cover transition-opacity duration-300 group-hover:opacity-0" />
</div>
<div class="grid gap-0.5 pt-3 pb-4 px-1.5 text-sm font-semibold">
<div class="flex gap-1">
<div v-html="product.salePrice"></div>
<div class="text-[#5f5f5f] dark:text-[#a3a3a3] line-through" v-html="product.regularPrice"></div>
</div>
<div>{{ product.name }}</div>
<div class="font-normal text-[#5f5f5f] dark:text-[#a3a3a3]">
{{ product.allPaStyle.nodes[0].name }}
</div>
</div>
</div>
</NuxtLink>
</div>
</div>
<div v-if="searchQuery && !isLoading && searchResults.length" class="flex items-center justify-center border-t border-black/10 dark:border-white/20 p-4">
<button
@click="search"
class="bg-black/15 dark:bg-white/15 hover:bg-black/10 hover:dark:bg-white/20 px-4 py-2 rounded-full active:scale-95 tracking-wide text-sm transition">
View All Results
</button>
</div>
</div>
</div>
<div v-if="suggestionMenu" class="fixed inset-0 z-40">
<div class="w-full h-full bg-black/70"></div>
</div>
</template>
7 changes: 7 additions & 0 deletions components/ButtonBack.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<div class="fixed px-7 mt-0.5 -left-1.5 max-2xl:hidden z-40">
<button @click.prevent="useRouter().back()" class="flex p-3 rounded-full dark:bg-black hover:dark:bg-neutral-800 bg-white hover:bg-neutral-200 active:scale-95 transition">
<UIcon name="i-iconamoon-arrow-left-1-bold" size="24" />
</button>
</div>
</template>
Loading

0 comments on commit dc1eead

Please sign in to comment.