diff --git a/examples/nextjs-api-sales/components/layout.tsx b/examples/nextjs-api-sales/components/layout.tsx new file mode 100644 index 0000000000..63049af025 --- /dev/null +++ b/examples/nextjs-api-sales/components/layout.tsx @@ -0,0 +1,43 @@ +import Link from "next/link" +import Image from "next/image" +import { ReactNotifications } from "react-notifications-component" + +function layout(props: any) { + return ( + <> + + + + + + + + + Home + + + + + Save + + + + + + + + + API Documentation + + + + + + + + {props.children} + > + ) +} + +export default layout \ No newline at end of file diff --git a/examples/nextjs-api-sales/components/notifications.ts b/examples/nextjs-api-sales/components/notifications.ts new file mode 100644 index 0000000000..6e528dfeaa --- /dev/null +++ b/examples/nextjs-api-sales/components/notifications.ts @@ -0,0 +1,28 @@ +import { Store } from "react-notifications-component" + +const notifications = { + error: (error: string, title: string) => { + Store.addNotification({ + container: "top-right", + type: "danger", + message: error, + title: title, + dismiss: { + duration: 10000, + } + }) + }, + success: (message: string, title: string) => { + Store.addNotification({ + container: "top-right", + type: "success", + message: message, + title: title, + dismiss: { + duration: 3000, + } + }) + } +} + +export default notifications \ No newline at end of file diff --git a/examples/nextjs-api-sales/components/utils.ts b/examples/nextjs-api-sales/components/utils.ts new file mode 100644 index 0000000000..b0d9a8ebca --- /dev/null +++ b/examples/nextjs-api-sales/components/utils.ts @@ -0,0 +1,70 @@ +import { App, AppSearch, Table, TableSearch } from "../definitions" +import getConfig from "next/config" + +const { serverRuntimeConfig } = getConfig() +const apiKey = serverRuntimeConfig["apiKey"] +const appName = serverRuntimeConfig["appName"] +const host = serverRuntimeConfig["host"] + +let APP: App | null = null +let TABLES: { [key: string]: Table } = {} + +export async function makeCall(method: string, url: string, opts?: { body?: any, appId?: string }): Promise { + const fetchOpts: any = { + method, + headers: { + "x-budibase-api-key": apiKey, + } + } + if (opts?.appId) { + fetchOpts.headers["x-budibase-app-id"] = opts.appId + } + if (opts?.body) { + fetchOpts.body = typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body + fetchOpts.headers["Content-Type"] = "application/json" + } + const finalUrl = `${host}/api/public/v1/${url}` + const response = await fetch(finalUrl, fetchOpts) + if (response.ok) { + return response.json() + } else { + const error = await response.text() + console.error("Budibase server error - ", error) + throw new Error(error) + } +} + +export async function getApp(): Promise { + if (APP) { + return APP + } + const apps: AppSearch = await makeCall("post", "applications/search", { + body: { + name: appName, + } + }) + const app = apps.data.find((app: App) => app.name === appName) + if (!app) { + throw new Error("Could not find app, please make sure app name in config is correct.") + } + APP = app + return app +} + +export async function findTable(appId: string, tableName: string): Promise { + if (TABLES[tableName]) { + return TABLES[tableName] + } + const tables: TableSearch = await makeCall("post", "tables/search", { + body: { + name: tableName, + }, + appId, + }) + const table = tables.data.find((table: Table) => table.name === tableName) + if (!table) { + throw new Error("Could not find table, please make sure your app is configured with the Postgres datasource correctly.") + } + TABLES[tableName] = table + return table +} diff --git a/examples/nextjs-api-sales/db/init.sql b/examples/nextjs-api-sales/db/init.sql index 462d64ff30..f0a0c9d100 100644 --- a/examples/nextjs-api-sales/db/init.sql +++ b/examples/nextjs-api-sales/db/init.sql @@ -1,23 +1,21 @@ CREATE TABLE IF NOT EXISTS sales_people ( - person_id INT NOT NULL, - name varchar(200) NOT NULL, - PRIMARY KEY (person_id) + person_id SERIAL PRIMARY KEY, + name varchar(200) NOT NULL ); CREATE TABLE IF NOT EXISTS sales ( - sale_id INT NOT NULL, + sale_id SERIAL PRIMARY KEY, sale_name varchar(200) NOT NULL, sold_by INT, - PRIMARY KEY (sale_id), CONSTRAINT sold_by_fk FOREIGN KEY(sold_by) REFERENCES sales_people(person_id) ); -INSERT INTO sales_people -select id, concat('Sales person ', id) +INSERT INTO sales_people (name) +select 'Salesperson ' || id FROM GENERATE_SERIES(1, 50) as id; -INSERT INTO sales -select id, concat('Sale ', id), floor(random() * 50 + 1)::int -FROM GENERATE_SERIES(1, 200) as id; \ No newline at end of file +INSERT INTO sales (sale_name, sold_by) +select 'Sale ' || id, floor(random() * 50 + 1)::int +FROM GENERATE_SERIES(1, 200) as id; diff --git a/examples/nextjs-api-sales/package.json b/examples/nextjs-api-sales/package.json index a278a3050a..6d75c85f01 100644 --- a/examples/nextjs-api-sales/package.json +++ b/examples/nextjs-api-sales/package.json @@ -14,7 +14,8 @@ "node-fetch": "^3.2.2", "node-sass": "^7.0.1", "react": "17.0.2", - "react-dom": "17.0.2" + "react-dom": "17.0.2", + "react-notifications-component": "^3.4.1" }, "devDependencies": { "@types/node": "17.0.21", diff --git a/examples/nextjs-api-sales/pages/_app.tsx b/examples/nextjs-api-sales/pages/_app.tsx index 71f53e7f8a..c6c68091e3 100644 --- a/examples/nextjs-api-sales/pages/_app.tsx +++ b/examples/nextjs-api-sales/pages/_app.tsx @@ -1,8 +1,13 @@ import "../styles/global.sass" import type { AppProps } from "next/app" +import Layout from "../components/layout" function MyApp({ Component, pageProps }: AppProps) { - return + return ( + + + + ) } export default MyApp diff --git a/examples/nextjs-api-sales/pages/api/sales.ts b/examples/nextjs-api-sales/pages/api/sales.ts index 0700711e88..95d01d6c40 100644 --- a/examples/nextjs-api-sales/pages/api/sales.ts +++ b/examples/nextjs-api-sales/pages/api/sales.ts @@ -1,73 +1,4 @@ -import getConfig from "next/config" -import {App, AppSearch, Table, TableSearch} from "../../definitions" - -const { serverRuntimeConfig } = getConfig() -const apiKey = serverRuntimeConfig["apiKey"] -const appName = serverRuntimeConfig["appName"] -const host = serverRuntimeConfig["host"] - -let APP: App | null = null -let TABLES: { [key: string]: Table } = {} - -async function makeCall(method: string, url: string, opts?: { body?: any, appId?: string }): Promise { - const fetchOpts: any = { - method, - headers: { - "x-budibase-api-key": apiKey, - } - } - if (opts?.appId) { - fetchOpts.headers["x-budibase-app-id"] = opts.appId - } - if (opts?.body) { - fetchOpts.body = typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body - fetchOpts.headers["Content-Type"] = "application/json" - } - const finalUrl = `${host}/api/public/v1/${url}` - const response = await fetch(finalUrl, fetchOpts) - if (response.ok) { - return response.json() - } else { - const error = await response.text() - console.error("Budibase server error - ", error) - throw new Error(error) - } -} - -async function getApp(): Promise { - if (APP) { - return APP - } - const apps: AppSearch = await makeCall("post", "applications/search", { - body: { - name: appName, - } - }) - const app = apps.data.find((app: App) => app.name === appName) - if (!app) { - throw new Error("Could not find app, please make sure app name in config is correct.") - } - APP = app - return app -} - -async function findTable(appId: string, tableName: string): Promise { - if (TABLES[tableName]) { - return TABLES[tableName] - } - const tables: TableSearch = await makeCall("post", "tables/search", { - body: { - name: tableName, - }, - appId, - }) - const table = tables.data.find((table: Table) => table.name === tableName) - if (!table) { - throw new Error("Could not find table, please make sure your app is configured with the Postgres datasource correctly.") - } - TABLES[tableName] = table - return table -} +import { getApp, findTable, makeCall } from "../../components/utils" async function getSales(req: any) { const { page } = req.query @@ -79,7 +10,7 @@ async function getSales(req: any) { limit: 10, sort: { type: "string", - order: "ascending", + order: "descending", column: "sale_id", }, paginate: true, diff --git a/examples/nextjs-api-sales/pages/api/salespeople.ts b/examples/nextjs-api-sales/pages/api/salespeople.ts new file mode 100644 index 0000000000..190e901486 --- /dev/null +++ b/examples/nextjs-api-sales/pages/api/salespeople.ts @@ -0,0 +1,31 @@ +import { getApp, findTable, makeCall } from "../../components/utils" + +async function getSalespeople() { + const { _id: appId } = await getApp() + const table = await findTable(appId, "sales_people") + return await makeCall("post", `tables/${table._id}/rows/search`, { + appId, + body: { + sort: { + type: "string", + order: "ascending", + column: "person_id", + }, + } + }) +} + +export default async function handler(req: any, res: any) { + let response: any = {} + try { + if (req.method === "GET") { + response = await getSalespeople() + } else { + res.status(404) + return + } + res.status(200).json(response) + } catch (err: any) { + res.status(400).send(err) + } +} \ No newline at end of file diff --git a/examples/nextjs-api-sales/pages/index.tsx b/examples/nextjs-api-sales/pages/index.tsx index 1c4b450b53..01e1feb67a 100644 --- a/examples/nextjs-api-sales/pages/index.tsx +++ b/examples/nextjs-api-sales/pages/index.tsx @@ -1,61 +1,81 @@ import type { NextPage } from "next" import styles from "../styles/home.module.css" -import { useState, useEffect } from "react" +import { useState, useEffect, useCallback } from "react" +import Notifications from "../components/notifications" const Home: NextPage = () => { const [sales, setSales] = useState([]) const [currentPage, setCurrentPage] = useState(1) + const [loaded, setLoaded] = useState(false) - const getSales = async (page: Number = 1) => { + const getSales = useCallback(async (page: Number = 1) => { let url = "/api/sales" if (page) { url += `?page=${page}` } const response = await fetch(url) if (!response.ok) { - throw new Error(await response.text()) + const error = await response.text() + Notifications.error(error, "Failed to get sales") + return } const sales = await response.json() // @ts-ignore setCurrentPage(page) return setSales(sales.data) - } + }, []) - const saveSale = async () => { - const response = await fetch("/api/sales", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }) - if (!response.ok) { - throw new Error(await response.text()) - } - } - - const goToNextPage = async () => { + const goToNextPage = useCallback(async () => { await getSales(currentPage + 1) - } + }, [currentPage, getSales]) - const goToPrevPage = async () => { + const goToPrevPage = useCallback(async () => { if (currentPage > 1) { await getSales(currentPage - 1) } - } + }, [currentPage, getSales]) useEffect(() => { - getSales().catch(() => { + getSales().then(() => { + setLoaded(true) + }).catch(() => { setSales([]) }) }, []) + if (!loaded) { + return null + } + return ( - Sales - {sales.map((sale: any) => {sale.sale_id})} - Prev Page - Next Page + + Sales + + + + + Sale ID + name + Sold by + + + + {sales.map((sale: any) => + + {sale.sale_id} + {sale.sale_name} + {sale.sales_people?.map((person: any) => person.primaryDisplay)[0]} + + )} + + + + Prev Page + Next Page + + + ) } diff --git a/examples/nextjs-api-sales/pages/save.tsx b/examples/nextjs-api-sales/pages/save.tsx new file mode 100644 index 0000000000..5b1ab5abff --- /dev/null +++ b/examples/nextjs-api-sales/pages/save.tsx @@ -0,0 +1,81 @@ +import type { NextPage } from "next" +import { useCallback, useEffect, useState } from "react" +import styles from "../styles/save.module.css" +import Notifications from "../components/notifications" + +const Save: NextPage = () => { + const [salespeople, setSalespeople] = useState([]) + const [loaded, setLoaded] = useState(false) + + const saveSale = useCallback(async (event: any) => { + event.preventDefault() + const sale = { + sale_name: event.target.name.value, + sales_person: [event.target.soldBy.value], + } + const response = await fetch("/api/sales", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(sale), + }) + if (!response.ok) { + const error = await response.text() + Notifications.error(error, "Failed to save sale") + return + } + Notifications.success("Sale saved successfully!", "Sale saved") + }, []) + + const getSalespeople = useCallback(async () => { + const response: any = await fetch("/api/salespeople") + if (!response.ok) { + throw new Error(await response.text()) + } + const json = await response.json() + setSalespeople(json.data) + }, []) + + useEffect(() => { + getSalespeople().then(() => { + setLoaded(true) + }).catch(() => { + setSalespeople([]) + }) + }, []) + + if (!loaded) { + return null + } + return ( + + + New sale + + + Name + + + + + + Sold by + + + + {salespeople.map((person: any) => {person.name})} + + + + + + Submit + + + + + ) +} + +export default Save \ No newline at end of file diff --git a/examples/nextjs-api-sales/public/bb-emblem.svg b/examples/nextjs-api-sales/public/bb-emblem.svg new file mode 100644 index 0000000000..9f4f3690d5 --- /dev/null +++ b/examples/nextjs-api-sales/public/bb-emblem.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/nextjs-api-sales/public/vercel.svg b/examples/nextjs-api-sales/public/vercel.svg deleted file mode 100644 index fbf0e25a65..0000000000 --- a/examples/nextjs-api-sales/public/vercel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/examples/nextjs-api-sales/styles/global.sass b/examples/nextjs-api-sales/styles/global.sass index 794e1ce4d8..85e46f8591 100644 --- a/examples/nextjs-api-sales/styles/global.sass +++ b/examples/nextjs-api-sales/styles/global.sass @@ -1,12 +1,25 @@ @charset "utf-8" -// Import a Google Font @import url('https://fonts.googleapis.com/css?family=Roboto:400,700') - $family-sans-serif: "Roboto", sans-serif -//$grey-dark: color -//$grey-light: color -//$primary: color -//$link: color -@import "../node_modules/bulma/bulma.sass" \ No newline at end of file +#__next + display: flex + flex-direction: column + justify-content: flex-start + align-items: stretch + height: 100vh + +.logo + padding: 0.75rem + +@import "../node_modules/bulma/bulma.sass" +@import "../node_modules/react-notifications-component/dist/theme.css" + +// applied after bulma styles are enabled +html + overflow-y: auto + +.navbar + background-color: #D3D3D3 + color: white \ No newline at end of file diff --git a/examples/nextjs-api-sales/styles/home.module.css b/examples/nextjs-api-sales/styles/home.module.css index 4906fe57f1..eb71c1072e 100644 --- a/examples/nextjs-api-sales/styles/home.module.css +++ b/examples/nextjs-api-sales/styles/home.module.css @@ -1,116 +1,30 @@ .container { - padding: 0 2rem; -} - -.main { - min-height: 100vh; - padding: 4rem 0; - flex: 1; + width: 100vw; display: flex; flex-direction: column; - justify-content: center; + padding: 5rem 2rem 0; align-items: center; + flex: 1 1 auto; } -.footer { +.buttons { display: flex; - flex: 1; - padding: 2rem 0; - border-top: 1px solid #eaeaea; - justify-content: center; - align-items: center; + flex-direction: row; + justify-content: space-between; } -.footer a { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; -} - -.title a { - color: #0070f3; - text-decoration: none; -} - -.title a:hover, -.title a:focus, -.title a:active { - text-decoration: underline; -} - -.title { - margin: 0; - line-height: 1.15; - font-size: 4rem; -} - -.title, -.description { - text-align: center; -} - -.description { - margin: 4rem 0; - line-height: 1.5; - font-size: 1.5rem; -} - -.code { - background: #fafafa; - border-radius: 5px; - padding: 0.75rem; - font-size: 1.1rem; - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; -} - -.grid { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - max-width: 800px; -} - -.card { - margin: 1rem; - padding: 1.5rem; - text-align: left; - color: inherit; - text-decoration: none; - border: 1px solid #eaeaea; +.tableSection { + padding: 2rem; + background: #D3D3D3; + width: 800px; border-radius: 10px; - transition: color 0.15s ease, border-color 0.15s ease; - max-width: 300px; } -.card:hover, -.card:focus, -.card:active { - color: #0070f3; - border-color: #0070f3; -} - -.card h2 { - margin: 0 0 1rem 0; - font-size: 1.5rem; -} - -.card p { - margin: 0; - font-size: 1.25rem; - line-height: 1.5; -} - -.logo { - height: 1em; - margin-left: 0.5rem; -} - -@media (max-width: 600px) { -.grid { +.table table { width: 100%; - flex-direction: column; -} } + +.tableSection h1 { + text-align: center; + color: black; +} \ No newline at end of file diff --git a/examples/nextjs-api-sales/styles/save.module.css b/examples/nextjs-api-sales/styles/save.module.css new file mode 100644 index 0000000000..177d317afd --- /dev/null +++ b/examples/nextjs-api-sales/styles/save.module.css @@ -0,0 +1,20 @@ +.container { + width: 100vw; + display: flex; + flex-direction: column; + padding: 5rem 2rem 0; + align-items: center; + flex: 1 1 auto; +} + +.formSection { + padding: 2rem; + background: #D3D3D3; + width: 400px; + border-radius: 10px; +} + +.formSection h1 { + text-align: center; + color: black; +} \ No newline at end of file diff --git a/examples/nextjs-api-sales/yarn.lock b/examples/nextjs-api-sales/yarn.lock index b1a66a4ac5..3f32417ba8 100644 --- a/examples/nextjs-api-sales/yarn.lock +++ b/examples/nextjs-api-sales/yarn.lock @@ -2384,6 +2384,11 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-notifications-component@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/react-notifications-component/-/react-notifications-component-3.4.1.tgz#3670aa17210a4e63ba3b4f553853bd99ab6a0150" + integrity sha512-vS/RLdz+VlXZz0dbK+LCcdhgUdUPi1BvSo7mVp58AQDpixI9emGwI0uISXhiTSjqFn/cPibPlJOJQ8kcvgmUrQ== + react@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
{sale.sale_id}