mirror of https://github.com/Budibase/budibase.git
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 ( |
|||
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; |
|||
INSERT INTO sales (sale_name, sold_by) |
|||
select 'Sale ' || id, floor(random() * 50 + 1)::int |
|||
FROM GENERATE_SERIES(1, 200) as id; |
|||
|
|||
@ -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 <Component {...pageProps} /> |
|||
return ( |
|||
<Layout> |
|||
<Component {...pageProps} /> |
|||
</Layout> |
|||
) |
|||
} |
|||
|
|||
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" |
|||
|
|||
// 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" |
|||
#__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 { |
|||
padding: 0 2rem; |
|||
} |
|||
|
|||
.main { |
|||
min-height: 100vh; |
|||
padding: 4rem 0; |
|||
flex: 1; |
|||
width: 100vw; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
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; |
|||
padding: 5rem 2rem 0; |
|||
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; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.grid { |
|||
.buttons { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-wrap: wrap; |
|||
max-width: 800px; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.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; |
|||
} |
|||
@ -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