diff --git a/.vscode/launch.json b/.vscode/launch.json index 799386b..d3d2d05 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,13 +2,19 @@ "version": "0.2.0", "configurations": [ { - "command": "yarn dev", - "name": "Next.js", + "type": "node", "request": "launch", + "name": "Next", + "runtimeExecutable": "yarn", "cwd": "${workspaceFolder}/Next", - "type": "node-terminal" + "runtimeArgs": [ + "dev" + ], + "port": 9229, + "skipFiles": [ + "/**" + ] }, - { "name": "TestAppRuna", "type": "coreclr", diff --git a/Next/APIClient.ts b/Next/APIClient.ts index 3941fa7..bb3af2f 100644 --- a/Next/APIClient.ts +++ b/Next/APIClient.ts @@ -664,8 +664,8 @@ export interface CartDeleteModel { export interface CustomerListItem { customerID: string; - name?: string | undefined; - email?: string | undefined; + name: string; + email: string; } export interface CustomerCreateUpdateModel { @@ -675,7 +675,7 @@ export interface CustomerCreateUpdateModel { export interface ProductListItem { productID: string; - name?: string | undefined; + name: string; price: number; } diff --git a/Next/components/Errors.tsx b/Next/components/Errors.tsx new file mode 100644 index 0000000..b2e14f4 --- /dev/null +++ b/Next/components/Errors.tsx @@ -0,0 +1,6 @@ + +export default function Errors({ errors }) { + return errors.map(x => ( +
  • {x}
  • + )); +} \ No newline at end of file diff --git a/Next/components/ProductForm.tsx b/Next/components/ProductForm.tsx new file mode 100644 index 0000000..767f8e7 --- /dev/null +++ b/Next/components/ProductForm.tsx @@ -0,0 +1,47 @@ +import { faChevronUp } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useEffect, useState } from "react"; + + +export default function ProductForm({ name, price, onNameChange, onPriceChange, onSubmit, busy }) { + let [form,setForm] = useState({ + name: "", + value: "", + }) + let [formBusy,setFormBusy] = useState(false); + let nameChanged = (e: React.ChangeEvent) => { + name = e.target.value; + onNameChange(e.target.value); + } + let priceChanged = (e: React.ChangeEvent) => { + price = e.target.value; + onPriceChange(e.target.value); + } + let submit = (e: React.MouseEvent) => { + e.preventDefault() + onSubmit(); + } + formBusy = busy; + useEffect(() => { + setFormBusy(busy); + },[busy]); + return ( +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/Next/interfaces/product.ts b/Next/interfaces/product.ts new file mode 100644 index 0000000..2c1cdd6 --- /dev/null +++ b/Next/interfaces/product.ts @@ -0,0 +1,23 @@ +import { ProductListItem } from "../APIClient"; + +export interface ProductDataProps { + productID: string, +} +export interface DeleteProductProps extends ProductDataProps { + onDelete: (val) => void +} +export interface ProductListRowsProps { + products: ProductListItem[], + onDelete: (val) => void, +} +export interface ProductState { + product: ProductListItem[] +} +export interface ProductFormState { + form: { + name: string, + price: number, + }, + errors: string[], + busy: boolean, +} \ No newline at end of file diff --git a/Next/pages/customer/edit/[id].tsx b/Next/pages/customer/edit/[id].tsx new file mode 100644 index 0000000..5f8ec06 --- /dev/null +++ b/Next/pages/customer/edit/[id].tsx @@ -0,0 +1,141 @@ +import React, { ChangeEvent } from 'react'; +import { withRouter, NextRouter } from 'next/router'; +import { Layout } from '../../shared/Layout'; +import { CustomerClient } from '../../../APIClient'; +import Swal from 'sweetalert2'; + +function Errors({ errors }) { + return errors.map(x => ( +
  • {x}
  • + )); +} +class EditCustomer extends React.Component<{ + router: NextRouter, +}, { + form: { + name: string, + email: string, + }, + errors: string[], + busy: boolean, +}> { + constructor(props) { + super(props) + const router = props.router; + const { id } = router.query!; + this.state = { + form: { + name: "", + email: "", + }, + errors: [], + busy: false, + } + } + async componentDidMount() { + const client = new CustomerClient(); + console.log(this.props.router) + if (this.props.router.query['id']) { + let data = await client.get(this.props.router.query['id']); + this.setState({ + form: { + name: data.name, + email: data.email, + } + }); + } + } + onNameChanged(e: ChangeEvent) { + this.setState({ + form: { + name: e.target.value, + email: this.state.form.email, + } + }); + } + onEmailChanged(e: ChangeEvent) { + this.setState({ + form: { + name: this.state.form.name, + email: e.target.value + } + }); + } + async validate() { + let errors: string[] = []; + if (!this.state.form.name) { + errors.push("Name required"); + } + if (!this.state.form.email) { + errors.push("Email required"); + } + await new Promise((resolve) => { + this.setState({ + errors + }, () => resolve(undefined)); + }) + return errors.length == 0; + } + async onSubmit(e) { + e.preventDefault(); + if (await this.validate()) { + await new Promise((resolve) => { + this.setState({ + busy: true, + }, () => resolve(undefined)); + }) + try { + const form = this.state.form; + const client = new CustomerClient(); + await client.put(this.props.router.query.id, { + name: form.name, + email: form.email, + }) + Swal.fire({ + title: "Submitted!", + text: "Customer is now in database", + }) + } catch (error) { + + } finally { + await new Promise((resolve) => { + this.setState({ + busy: false, + }, () => resolve(undefined)); + }) + + } + } + } + render() { + return ( +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
  • + +
  • +
    +
    + ) + } +} + +export default function EditCustomerPage() { + let EditCustomerMain = withRouter(EditCustomer); + return ( + + + + ) +} \ No newline at end of file diff --git a/Next/pages/customer/index.tsx b/Next/pages/customer/index.tsx index 1bf904d..dfeec60 100644 --- a/Next/pages/customer/index.tsx +++ b/Next/pages/customer/index.tsx @@ -35,6 +35,7 @@ const CustomerListItemRows: React.FunctionComponent<{ let rows = customers.map(x => { return ( + {x.customerID} {x.name} {x.email} @@ -87,6 +88,7 @@ class Customer extends React.Component<{}, { customer: CustomerListItem[] }> { return ( + diff --git a/Next/pages/product/create.tsx b/Next/pages/product/create.tsx new file mode 100644 index 0000000..df586cd --- /dev/null +++ b/Next/pages/product/create.tsx @@ -0,0 +1,116 @@ +import React, { ChangeEvent } from 'react'; +import { Layout } from '../shared/Layout'; +import { ProductClient } from '../../APIClient'; +import Swal from 'sweetalert2'; +import ProductForm from '../../components/ProductForm'; +import Errors from '../../components/Errors'; +import Link from 'next/link'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; +import { ProductFormState } from '../../interfaces/product'; + +class CreateProduct extends React.Component<{}, ProductFormState> { + constructor(props) { + super(props) + this.state = { + form: { + name: "", + price: 0, + }, + errors: [], + busy: false, + } + } + onNameChanged(newName) { + this.setState({ + form: { + name: newName, + price: this.state.form.price, + } + }); + } + onPriceChanged(newPrice) { + this.setState({ + form: { + name: this.state.form.name, + price: newPrice + } + }); + } + async validate() { + let errors: string[] = []; + if (!this.state.form.name) { + errors.push("Name required"); + } + if (!this.state.form.price) { + errors.push("Price required"); + } + await new Promise((resolve) => { + this.setState({ + errors + }, () => resolve(undefined)); + }) + return errors.length == 0; + } + async onSubmit() { + if (await this.validate()) { + await new Promise((resolve) => { + this.setState({ + busy: true, + }, () => resolve(undefined)); + }) + try { + const form = this.state.form; + const client = new ProductClient(); + await client.post({ + name: form.name, + price: form.price, + }) + Swal.fire({ + title: "Submitted!", + text: "Product is now in database", + }) + } catch (error) { + + } finally { + await new Promise((resolve) => { + this.setState({ + busy: false, + }, () => resolve(undefined)); + }) + + } + } + } + render() { + return ( +
    +

    Create Product

    + + + Return to index + + + +
      + +
    +
    + ) + } +} + + +export default function CreateProductPage() { + return ( + + + + ) +} \ No newline at end of file diff --git a/Next/pages/product/edit/[id].tsx b/Next/pages/product/edit/[id].tsx new file mode 100644 index 0000000..6dac913 --- /dev/null +++ b/Next/pages/product/edit/[id].tsx @@ -0,0 +1,152 @@ +import React, { ChangeEvent } from 'react'; +import { withRouter, NextRouter, useRouter } from 'next/router'; +import { Layout } from '../../shared/Layout'; +import { ProductClient } from '../../../APIClient'; +import Swal from 'sweetalert2'; +import ProductForm from '../../../components/ProductForm'; +import Link from 'next/link'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; +import { ProductDataProps, ProductFormState } from '../../../interfaces/product'; + +function Errors({ errors }) { + return errors.map(x => ( +
  • {x}
  • + )); +} +class EditProduct extends React.Component { + constructor(props) { + super(props) + const id = props.productID; + this.state = { + form: { + name: "", + price: 0, + }, + errors: [], + busy: false, + } + } + async componentDidMount() { + const client = new ProductClient(); + await new Promise((resolve) => { + this.setState({ + busy: true, + }, () => resolve(undefined)); + }) + if (this.props.productID) { + let data = await client.get(this.props.productID); + await new Promise((resolve) => { + this.setState({ + busy: false, + }, () => resolve(undefined)); + }) + this.setState({ + form: { + name: data.name, + price: data.price, + } + }); + } + } + onNameChanged(newName: string) { + this.setState({ + form: { + name: newName, + price: this.state.form.price, + } + }); + } + onPriceChanged(newPrice: number) { + this.setState({ + form: { + name: this.state.form.name, + price: newPrice, + } + }); + } + async validate() { + let errors: string[] = []; + if (!this.state.form.name) { + errors.push("Name required"); + } + if (!this.state.form.price) { + errors.push("Email required"); + } + await new Promise((resolve) => { + this.setState({ + errors + }, () => resolve(undefined)); + }) + return errors.length == 0; + } + async onSubmit() { + if (await this.validate()) { + await new Promise((resolve) => { + this.setState({ + busy: true, + }, () => resolve(undefined)); + }) + try { + const form = this.state.form; + const client = new ProductClient(); + await client.put(this.props.productID, { + name: form.name, + price: form.price, + }) + Swal.fire({ + title: "Submitted!", + text: "Product is modified in database", + }) + } catch (error) { + + } finally { + await new Promise((resolve) => { + this.setState({ + busy: false, + }, () => resolve(undefined)); + }) + + } + } + } + render() { + if (typeof this.props.productID != "string") { + return ( +
    +
    + ) + } + return ( +
    +

    Edit Product

    + + + Return to index + + + +
      + +
    +
    + ) + } +} + +export default function EditProductPage() { + const router = useRouter(); + const id = router.query['id']; + + return ( + + + + ) +} \ No newline at end of file diff --git a/Next/pages/product/index.tsx b/Next/pages/product/index.tsx new file mode 100644 index 0000000..1cbbdaa --- /dev/null +++ b/Next/pages/product/index.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import axios from 'axios'; +import { ProductClient, ProductListItem } from '../../APIClient'; +import { Layout } from '../shared/Layout'; +import Swal from 'sweetalert2'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faEdit, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; +import { DeleteProductProps, ProductDataProps, ProductListRowsProps, ProductState } from '../../interfaces/product'; + +const DeleteProductButton: React.FunctionComponent = ({ productID, onDelete }) => { + const onClickDelete = async () => { + const result = await Swal.fire({ + title: 'Confirm delete?', + text: 'Delete product?', + icon: 'warning', + showCancelButton: true, + }) + if (result.isConfirmed) { + onDelete(productID); + } + } + return ( + + ); +} +const EditProductButton: React.FunctionComponent = ({ productID }) => { + const router = useRouter(); + return ( + + + + + + ); +} +const ProductListItemRows: React.FunctionComponent = ({ products, onDelete }) => { + let rows = products.map(x => { + return ( +
    + + + + + + + ); + }) + return ( + + {rows} + + ) +} +class Product extends React.Component<{},ProductState> { + constructor(props) { + super(props); + this.state = { + product: [], + } + } + async componentDidMount() { + this.getProducts(); + } + async getProducts() { + try { + const client = new ProductClient(); + let data = await client.getAll(); + console.log(data); + this.setState({ + product: data + }); + } catch (error) { + this.setState({ + product: [], + }); + } + } + async deleteProduct(productId) { + try { + const client = new ProductClient(); + await client.delete(productId); + // Remove the product in the state by filter()ing it + let product = this.state.product.filter(x => x.productID != productId); + this.setState({ + product + }) + Swal.fire({ + title: "Deleted!", + text: "Product is deleted in database", + }) + } catch (error) { + alert(error); + } + } + render() { + return ( +
    +

    Products

    +
    ID Name Email
    {x.productID}{x.name}{x.price}
    + + + + + + + + + +
    IDNamePriceEditDelete
    + + + Create + + + + ); + } +} + +export default function ProductPage() { + return ( + + + + ) +} \ No newline at end of file diff --git a/TestAppRuna/Shop.sqlite.db b/TestAppRuna/Shop.sqlite.db index a437520..77747a9 100644 Binary files a/TestAppRuna/Shop.sqlite.db and b/TestAppRuna/Shop.sqlite.db differ