Skip to content

Commit

Permalink
Allow editing shipping information in profile settings (#82)
Browse files Browse the repository at this point in the history
* Add /accounts/get and */update to fetch/update shipping info

* Update db schema adding shipping info

* Add ShippingInformationForm component and use it on the setting page
  • Loading branch information
andrii-balitskyi authored Oct 18, 2024
1 parent e70cf87 commit a60c997
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 3 deletions.
Binary file modified bun.lockb
Binary file not shown.
33 changes: 31 additions & 2 deletions fake-snippets-api/lib/db/db-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ const initializer = combine(databaseSchema.parse({}), (set, get) => ({
const state = get()
return state.orderFiles.find((file) => file.order_file_id === orderFileId)
},
addAccount: (account: Omit<Account, "account_id">) => {
addAccount: (
account: Omit<Account, "account_id"> & Partial<Pick<Account, "account_id">>,
) => {
const newAccount = {
account_id: `account_${get().idCounter + 1}`,
account_id: account.account_id || `account_${get().idCounter + 1}`,
...account,
}

Expand Down Expand Up @@ -227,6 +229,33 @@ const initializer = combine(databaseSchema.parse({}), (set, get) => ({
const state = get()
return state.accounts.find((account) => account.account_id === account_id)
},
updateAccount: (
account_id: string,
updates: Partial<Account>,
): Account | undefined => {
let updatedAccount: Account | undefined
set((state) => {
const accountIndex = state.accounts.findIndex(
(account) => account.account_id === account_id,
)
if (accountIndex !== -1) {
updatedAccount = { ...state.accounts[accountIndex] }
if (updates.shippingInfo) {
updatedAccount.shippingInfo = {
...updatedAccount.shippingInfo,
...updates.shippingInfo,
}
delete updates.shippingInfo
}
updatedAccount = { ...updatedAccount, ...updates }
const updatedAccounts = [...state.accounts]
updatedAccounts[accountIndex] = updatedAccount
return { ...state, accounts: updatedAccounts }
}
return state
})
return updatedAccount
},
createSession: (session: Omit<Session, "session_id">): Session => {
const newSession = { session_id: `session_${Date.now()}`, ...session }
set((state) => ({
Expand Down
10 changes: 10 additions & 0 deletions fake-snippets-api/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,19 @@ export const loginPageSchema = z.object({
})
export type LoginPage = z.infer<typeof loginPageSchema>

const shippingInfoSchema = z.object({
fullName: z.string(),
address: z.string(),
city: z.string(),
state: z.string(),
zipCode: z.string(),
country: z.string(),
})

export const accountSchema = z.object({
account_id: z.string(),
github_username: z.string(),
shippingInfo: shippingInfoSchema.optional(),
})
export type Account = z.infer<typeof accountSchema>

Expand Down
9 changes: 9 additions & 0 deletions fake-snippets-api/lib/db/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import type { DbClient } from "./db-client"

export const seed = (db: DbClient) => {
const { account_id } = db.addAccount({
account_id: "account-1234",
github_username: "testuser",
shippingInfo: {
fullName: "Test User",
address: "123 Test St",
city: "Testville",
state: "NY",
zipCode: "10001",
country: "United States",
},
})
db.addAccount({
github_username: "seveibar",
Expand Down
21 changes: 21 additions & 0 deletions fake-snippets-api/routes/api/accounts/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
import { z } from "zod"
import { accountSchema } from "fake-snippets-api/lib/db/schema"

export default withRouteSpec({
methods: ["GET"],
auth: "session",
jsonResponse: z.object({
account: accountSchema,
}),
})(async (req, ctx) => {
const account = ctx.db.getAccount(ctx.auth.account_id)
if (!account) {
return ctx.error(404, {
error_code: "account_not_found",
message: "Account not found",
})
}

return ctx.json({ account })
})
38 changes: 38 additions & 0 deletions fake-snippets-api/routes/api/accounts/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
import { z } from "zod"
import { accountSchema } from "fake-snippets-api/lib/db/schema"

const shippingInfoSchema = z.object({
fullName: z.string(),
address: z.string(),
city: z.string(),
state: z.string(),
zipCode: z.string(),
country: z.string(),
})

export default withRouteSpec({
methods: ["POST"],
auth: "session",
jsonBody: z.object({
shippingInfo: shippingInfoSchema,
}),
jsonResponse: z.object({
account: accountSchema,
}),
})(async (req, ctx) => {
const { shippingInfo } = req.jsonBody

const updatedAccount = ctx.db.updateAccount(ctx.auth.account_id, {
shippingInfo,
})

if (!updatedAccount) {
return ctx.error(404, {
error_code: "account_not_found",
message: "Account not found",
})
}

return ctx.json({ account: updatedAccount })
})
12 changes: 11 additions & 1 deletion fake-snippets-api/tests/fixtures/get-test-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,17 @@ export const getTestServer = async (): Promise<TestFixture> => {
}

const seedDatabase = (db: DbClient) => {
const account = db.addAccount({ github_username: "testuser" })
const account = db.addAccount({
github_username: "testuser",
shippingInfo: {
fullName: "Test User",
address: "123 Test St",
city: "Testville",
state: "NY",
zipCode: "10001",
country: "United States",
},
})
const order = db.addOrder({
account_id: account.account_id,
is_draft: true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@types/ms": "^0.7.34",
"@typescript/ata": "^0.9.7",
"@valtown/codemirror-ts": "^2.2.0",
"change-case": "^5.4.4",
"circuit-json": "^0.0.85",
"circuit-json-to-bom-csv": "^0.0.6",
"circuit-json-to-gerber": "^0.0.12",
Expand Down
155 changes: 155 additions & 0 deletions src/components/ShippingInformationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React, { useReducer, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useToast } from "@/hooks/use-toast"
import { useAxios } from "@/hooks/use-axios"
import { useQuery, useMutation, useQueryClient } from "react-query"
import { Loader2 } from "lucide-react"
import { sentenceCase } from "change-case"

type ShippingInfo = {
fullName: string
address: string
city: string
state: string
zipCode: string
country: string
}

type Action =
| { type: "SET_FIELD"; field: keyof ShippingInfo; value: string }
| { type: "SET_ALL"; payload: ShippingInfo }

const initialState: ShippingInfo = {
fullName: "",
address: "",
zipCode: "",
country: "",
city: "",
state: "",
}

const shippingPlaceholders: ShippingInfo = {
fullName: "Enter your full name",
address: "Enter your street address",
zipCode: "Enter your zip code",
country: "Enter your country",
city: "Enter your city",
state: "Enter your state",
}

const ShippingInformationForm: React.FC = () => {
const [form, setField] = useReducer(
(state: ShippingInfo, action: Action): ShippingInfo => {
switch (action.type) {
case "SET_FIELD":
return { ...state, [action.field]: action.value }
case "SET_ALL":
return action.payload
default:
return state
}
},
initialState,
)
const { toast } = useToast()
const axios = useAxios()
const queryClient = useQueryClient()

const { data: account, isLoading: isLoadingAccount } = useQuery(
"account",
async () => {
const response = await axios.get("/accounts/get")
return response.data.account
},
)

const updateShippingMutation = useMutation(
(shippingInfo: ShippingInfo) =>
axios.post("/accounts/update", { shippingInfo }),
{
onSuccess: () => {
queryClient.invalidateQueries("account")
toast({
title: "Success",
description: "Shipping information updated successfully",
})
},
onError: () => {
toast({
title: "Error",
description: "Failed to update shipping information",
variant: "destructive",
})
},
},
)

useEffect(() => {
if (account?.shippingInfo) {
setField({ type: "SET_ALL", payload: account.shippingInfo })
}
}, [account])

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
updateShippingMutation.mutate(form)
}

if (isLoadingAccount) {
return (
<div className="flex justify-center items-center h-64">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
)
}

const fieldOrder: (keyof ShippingInfo)[] = [
"fullName",
"address",
"zipCode",
"country",
"city",
"state",
]

return (
<form onSubmit={handleSubmit} className="space-y-4">
{fieldOrder.map((key) => (
<div key={key}>
<label
htmlFor={key}
className="block text-sm font-medium text-gray-700"
>
{sentenceCase(key)}
</label>
<Input
id={key}
value={form[key]}
onChange={(e) =>
setField({
type: "SET_FIELD",
field: key,
value: e.target.value,
})
}
placeholder={shippingPlaceholders[key]}
disabled={updateShippingMutation.isLoading}
/>
</div>
))}
<Button type="submit" disabled={updateShippingMutation.isLoading}>
{updateShippingMutation.isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating...
</>
) : (
"Update"
)}
</Button>
</form>
)
}

export default ShippingInformationForm
11 changes: 11 additions & 0 deletions src/pages/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import Header from "@/components/Header"
import Footer from "@/components/Footer"
import ShippingInformationForm from "@/components/ShippingInformationForm"

export const SettingsPage = () => {
return (
<div>
<Header />
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Settings</h1>
<div className="flex">
<div className="w-1/2 pr-4">
<div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<h2 className="text-2xl font-semibold mb-4">
Shipping Information
</h2>
<ShippingInformationForm />
</div>
</div>
</div>
</div>
<Footer />
</div>
Expand Down

0 comments on commit a60c997

Please sign in to comment.