forked from tsai/budibase
16 changed files with 458 additions and 222 deletions
@ -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 ( |
||||
|
<> |
||||
|
<nav className="navbar" role="navigation" aria-label="main navigation"> |
||||
|
<div id="navbar" className="navbar-menu"> |
||||
|
<div className="logo"> |
||||
|
<Image src="/bb-emblem.svg" width="50" height="50" /> |
||||
|
</div> |
||||
|
<div className="navbar-start"> |
||||
|
<Link href="/"> |
||||
|
<a className="navbar-item"> |
||||
|
Home |
||||
|
</a> |
||||
|
</Link> |
||||
|
<Link href="/save"> |
||||
|
<a className="navbar-item"> |
||||
|
Save |
||||
|
</a> |
||||
|
</Link> |
||||
|
</div> |
||||
|
|
||||
|
<div className="navbar-end"> |
||||
|
<div className="navbar-item"> |
||||
|
<div className="buttons"> |
||||
|
<a className="button is-primary" href="https://budibase.readme.io/reference"> |
||||
|
<strong>API Documentation</strong> |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</nav> |
||||
|
<ReactNotifications /> |
||||
|
{props.children} |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default layout |
||||
@ -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 |
||||
@ -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<any> { |
||||
|
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<App> { |
||||
|
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<Table> { |
||||
|
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 |
||||
|
} |
||||
@ -1,23 +1,21 @@ |
|||||
CREATE TABLE IF NOT EXISTS sales_people ( |
CREATE TABLE IF NOT EXISTS sales_people ( |
||||
person_id INT NOT NULL, |
person_id SERIAL PRIMARY KEY, |
||||
name varchar(200) NOT NULL, |
name varchar(200) NOT NULL |
||||
PRIMARY KEY (person_id) |
|
||||
); |
); |
||||
|
|
||||
CREATE TABLE IF NOT EXISTS sales ( |
CREATE TABLE IF NOT EXISTS sales ( |
||||
sale_id INT NOT NULL, |
sale_id SERIAL PRIMARY KEY, |
||||
sale_name varchar(200) NOT NULL, |
sale_name varchar(200) NOT NULL, |
||||
sold_by INT, |
sold_by INT, |
||||
PRIMARY KEY (sale_id), |
|
||||
CONSTRAINT sold_by_fk |
CONSTRAINT sold_by_fk |
||||
FOREIGN KEY(sold_by) |
FOREIGN KEY(sold_by) |
||||
REFERENCES sales_people(person_id) |
REFERENCES sales_people(person_id) |
||||
); |
); |
||||
|
|
||||
INSERT INTO sales_people |
INSERT INTO sales_people (name) |
||||
select id, concat('Sales person ', id) |
select 'Salesperson ' || id |
||||
FROM GENERATE_SERIES(1, 50) as id; |
FROM GENERATE_SERIES(1, 50) as id; |
||||
|
|
||||
INSERT INTO sales |
INSERT INTO sales (sale_name, sold_by) |
||||
select id, concat('Sale ', id), floor(random() * 50 + 1)::int |
select 'Sale ' || id, floor(random() * 50 + 1)::int |
||||
FROM GENERATE_SERIES(1, 200) as id; |
FROM GENERATE_SERIES(1, 200) as id; |
||||
|
|||||
@ -1,8 +1,13 @@ |
|||||
import "../styles/global.sass" |
import "../styles/global.sass" |
||||
import type { AppProps } from "next/app" |
import type { AppProps } from "next/app" |
||||
|
import Layout from "../components/layout" |
||||
|
|
||||
function MyApp({ Component, pageProps }: AppProps) { |
function MyApp({ Component, pageProps }: AppProps) { |
||||
return <Component {...pageProps} /> |
return ( |
||||
|
<Layout> |
||||
|
<Component {...pageProps} /> |
||||
|
</Layout> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default MyApp |
export default MyApp |
||||
|
|||||
@ -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) |
||||
|
} |
||||
|
} |
||||
@ -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 ( |
||||
|
<div className={styles.container}> |
||||
|
<div className={styles.formSection}> |
||||
|
<h1 className="subtitle">New sale</h1> |
||||
|
<form onSubmit={saveSale}> |
||||
|
<div className="field"> |
||||
|
<label className="label">Name</label> |
||||
|
<div className="control"> |
||||
|
<input id="name" className="input" type="text" placeholder="Text input" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className="field"> |
||||
|
<label className="label">Sold by</label> |
||||
|
<div className="control"> |
||||
|
<div className="select"> |
||||
|
<select id="soldBy"> |
||||
|
{salespeople.map((person: any) => <option key={person._id} value={person._id}>{person.name}</option>)} |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className="control"> |
||||
|
<button className="button is-link">Submit</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Save |
||||
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,12 +1,25 @@ |
|||||
@charset "utf-8" |
@charset "utf-8" |
||||
|
|
||||
// Import a Google Font |
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700') |
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700') |
||||
|
|
||||
$family-sans-serif: "Roboto", sans-serif |
$family-sans-serif: "Roboto", sans-serif |
||||
//$grey-dark: color |
|
||||
//$grey-light: color |
|
||||
//$primary: color |
|
||||
//$link: color |
|
||||
|
|
||||
@import "../node_modules/bulma/bulma.sass" |
#__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 |
||||
@ -1,116 +1,30 @@ |
|||||
.container { |
.container { |
||||
padding: 0 2rem; |
width: 100vw; |
||||
} |
|
||||
|
|
||||
.main { |
|
||||
min-height: 100vh; |
|
||||
padding: 4rem 0; |
|
||||
flex: 1; |
|
||||
display: flex; |
display: flex; |
||||
flex-direction: column; |
flex-direction: column; |
||||
justify-content: center; |
padding: 5rem 2rem 0; |
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
.footer { |
|
||||
display: flex; |
|
||||
flex: 1; |
|
||||
padding: 2rem 0; |
|
||||
border-top: 1px solid #eaeaea; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
.footer a { |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
align-items: center; |
||||
flex-grow: 1; |
flex: 1 1 auto; |
||||
} |
|
||||
|
|
||||
.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 { |
.buttons { |
||||
display: flex; |
display: flex; |
||||
align-items: center; |
flex-direction: row; |
||||
justify-content: center; |
justify-content: space-between; |
||||
flex-wrap: wrap; |
|
||||
max-width: 800px; |
|
||||
} |
} |
||||
|
|
||||
.card { |
.tableSection { |
||||
margin: 1rem; |
padding: 2rem; |
||||
padding: 1.5rem; |
background: #D3D3D3; |
||||
text-align: left; |
width: 800px; |
||||
color: inherit; |
|
||||
text-decoration: none; |
|
||||
border: 1px solid #eaeaea; |
|
||||
border-radius: 10px; |
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) { |
.table table { |
||||
.grid { |
|
||||
width: 100%; |
width: 100%; |
||||
flex-direction: column; |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
|
.tableSection h1 { |
||||
|
text-align: center; |
||||
|
color: black; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
Loading…
Reference in new issue