Skip to content

Commit

Permalink
Svelte shop (#12)
Browse files Browse the repository at this point in the history
* init api

* created product-slider

* created stores

* renamed to svelte-shop
  • Loading branch information
manuelJung authored Mar 28, 2024
1 parent 223644d commit 524e0c7
Show file tree
Hide file tree
Showing 31 changed files with 612 additions and 43 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"type": "module",
"dependencies": {
"immer": "^10.0.4"
"immer": "^10.0.4",
"validate": "^5.2.0"
}
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import createProductStore from "$lib/stores/products"
export let name:string
const productsStore = createProductStore(name)
const hits = productsStore.useState(state => state.data)
</script>

<div class='ProductSlider'>
{#each $hits as hit}
<div class="product">
<h2>{hit.name}</h2>
<p>{hit.price}€</p>
</div>
{/each}
</div>


<style>
.ProductSlider {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.product {
width: 200px;
border: 1px solid #ccc;
padding: 1rem;
}
</style>
66 changes: 66 additions & 0 deletions packages/svelte-example-shop/src/lib/stores/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import createStore from "@rlx/svelte";
import { Customer } from "../../server-state";

declare global {
interface RlxStores {
account: ReturnType<typeof createAccountStore>
}
}

export default function createAccountStore () {
const store = createStore({
name: 'account',
persist: true,
state: {
user: null as Customer | null,
initialFetched: false,
fetchError: null as string | null,
isLoggingIn: false,
},
actions: {
/** comment */
initialFetch: () => ({
triggerOnMount: true,
fetcher: () => fetch('/api/account'),
mapResponse: r => ({ user: r, initialFetched: true }),
}),
login: (email: string, password: string) => ({
fetcher: () => fetch('/api/account/login', { email, password }),
concurrency: 'FIRST',
mappings: { data: 'user', isFetching: 'isLoggingIn'}
}),
logout: () => ({
fetcher: () => fetch('/api/account/logout').then(r => r.json()),
concurrency: 'FIRST',
optimisticData: () => ({ user: null }),
}),
register: (email: string, password: string) => ({
fetcher: () => fetch('/api/account/register', { email, password }),
concurrency: 'FIRST',
mappings: { data: 'user' }
}),
updateUser: (user:{name:string}) => ({
fetcher: () => fetch('/api/account/update', user),
optimisticData: (state) => ({ user: {...state.user, ...user} }),
concurrency: 'LAST',
mappings: { data: 'user' }
}),
}
})

store.addRule({
id: 'log-user',
target: 'account/login/request',
consequence: ({action, store}) => store.actions.initialFetch()
})

return store
}

function fetch(url: string, body:any=null) {
return window.fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(r => r.json())
}
24 changes: 24 additions & 0 deletions packages/svelte-example-shop/src/lib/stores/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import createStore from "@rlx/svelte";

export type Channel = 'b2b' | 'b2c'

export default function createChannelStore() {
const store = createStore({
name: 'channel',
persist: true,
state: {
data: 'b2b' as Channel
},
actions: {
set: (channel: Channel) => state => ({data:channel})
}
})

store.addRule({
id: 'log-set',
target: ['account/login/success', 'account/initialFetch/success'],
consequence: ({store}) => store.actions.set('b2b')
})

return store
}
67 changes: 67 additions & 0 deletions packages/svelte-example-shop/src/lib/stores/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import createStore from "@rlx/svelte"
import type { Product } from "../../server-state"

export type FilterValues = {
search: string
color: string
}

declare global {
interface RlxStores {
products: ReturnType<typeof createProductStore>
}
}

export default function createProductStore(name:string, filterValues: Partial<FilterValues> = {}) {
const store = createStore({
name: 'products',
key: name,
state: {
data: [] as Product[],
isFetching: false,
fetchError: null as string | null,
filterValues: {
search: '',
color: '',
...filterValues
} satisfies FilterValues as FilterValues
},
actions: {
fetch: () => ({
triggerOnMount: true,
concurrency: 'SWITCH',
fetcher: state => fetchProducts(state.filterValues),
mapResponse: (response, state) => ({
...state,
data: response.hits,
isFetching: false,
})
}),
/** my comment */
setFilterValue: (key: keyof FilterValues, value: string) => state => ({
filterValues: {
...state.filterValues,
[key]: value
}
})
}
})

store.addRule({
id: 'trigger-fetch',
target: '/setFilterValue',
consequence: (args) => args.store.actions.fetch()
})

return store
}

async function fetchProducts(filterValues: FilterValues) {
const url = new URL('/api/products', window.location.href)

const body = JSON.stringify(filterValues)

return fetch(url, {method: 'POST', body}).then(res => res.json()) as Promise<{
hits: Product[]
}>
}
8 changes: 8 additions & 0 deletions packages/svelte-example-shop/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import ProductSlider from "$lib/components/ProductSlider.svelte";
</script>


<ProductSlider name='home-slider-1'/>
10 changes: 10 additions & 0 deletions packages/svelte-example-shop/src/routes/api/account/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import serverState, { type Customer } from '../../../server-state.js';
import Schema from 'validate'

/** @type {import('./$types').RequestHandler} */
export async function POST (params) {

const customer = serverState.customers.find(c => c.email === serverState.currentCustomerId) ?? null

return new Response(JSON.stringify(customer))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import serverState, { type Customer } from '../../../../server-state.js';
import Schema from 'validate'

const schema = new Schema({
email: {
type: 'string',
required: true
},
password: {
type: 'string',
required: true
}
})

/** @type {import('./$types').RequestHandler} */
export async function POST (params) {
const body = await params.request.json() as {
email: string
password: string
}

const errors = schema.validate(body)
if (errors.length) {
return new Response(JSON.stringify(errors), { status: 400 })
}

const customer = serverState.customers.find(c => c.email === body.email)

if (!customer) {
return new Response(JSON.stringify({error: 'Customer does not exist'}), { status: 400 })
}

serverState.customers.push(customer)
serverState.currentCustomerId = customer.id

return new Response(JSON.stringify(customer))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import serverState, { type Customer } from '../../../../server-state.js';
import Schema from 'validate'

const schema = new Schema({
email: {
type: 'string',
required: true
},
password: {
type: 'string',
required: true
}
})

/** @type {import('./$types').RequestHandler} */
export async function POST (params) {
const body = await params.request.json() as {
email: string
password: string
}

const errors = schema.validate(body)
if (errors.length) {
return new Response(JSON.stringify(errors), { status: 400 })
}

if (serverState.customers.find(c => c.email === body.email)) {
return new Response(JSON.stringify({error: 'Customer already exists'}), { status: 400 })
}

const customer:Customer = {
email: body.email,
password: body.password,
id: Math.random().toString(36).substring(7),
name: 'INITIAL'
}

serverState.customers.push(customer)
serverState.currentCustomerId = customer.id

return new Response(JSON.stringify(customer))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import serverState, { type Customer } from '../../../../server-state.js';
import Schema from 'validate'

const schema = new Schema({
name: {
type: 'string',
required: false
},
})

/** @type {import('./$types').RequestHandler} */
export async function POST (params) {
const body = await params.request.json() as {
name?: string
}

const errors = schema.validate(body)
if (errors.length) {
return new Response(JSON.stringify(errors), { status: 400 })
}

if(serverState.currentCustomerId === null) {
return new Response(JSON.stringify({error: 'Not logged in'}), { status: 401 })
}

const customer = serverState.customers.find(c => c.id === serverState.currentCustomerId)

if (!customer) {
return new Response(JSON.stringify({error: 'Customer does not exist'}), { status: 400 })
}

if(body.name) customer.name = body.name

return new Response(JSON.stringify(customer))
}
10 changes: 10 additions & 0 deletions packages/svelte-example-shop/src/routes/api/cart/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import serverState, { type Customer } from '../../../server-state.js';
import Schema from 'validate'

/** @type {import('./$types').RequestHandler} */
export async function POST (params) {

const cart = serverState.cart

return new Response(JSON.stringify(cart))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import serverState from '../../../../server-state.js';
import Schema from 'validate'

const schema = new Schema({
productID: {
type: 'string',
required: true
},
amount: {
type: 'number',
required: true
}
})

/** @type {import('./$types').RequestHandler} */
export async function POST (params) {
const body = await params.request.json() as { productID: string, amount: number }

const errors = schema.validate(body)
if (errors.length) {
return new Response(JSON.stringify(errors), { status: 400 })
}

if (serverState.cart.find(item => item.productID === body.productID)) {
serverState.cart = serverState.cart.map(item => {
if (item.productID === body.productID) {
item.amount += body.amount
}
return item
})
}
else {
serverState.cart.push(body)
}


return new Response(JSON.stringify(serverState.cart))
}
Loading

0 comments on commit 524e0c7

Please sign in to comment.