Adding in pagination to next frontend.
This commit is contained in:
parent
493c2ec266
commit
5162a3ee17
|
@ -1,7 +1,7 @@
|
||||||
import { components } from "./openapi"
|
import { components } from "./openapi"
|
||||||
|
|
||||||
export type App = components["schemas"]["applicationOutput"]["data"]
|
export type App = components["schemas"]["applicationOutput"]["data"]
|
||||||
export type AppSearch = {
|
export type Table = components["schemas"]["tableOutput"]["data"]
|
||||||
data: App[]
|
export type TableSearch = components["schemas"]["tableSearch"]
|
||||||
}
|
export type AppSearch = components["schemas"]["applicationSearch"]
|
||||||
export type RowSearch = components["schemas"]["searchOutput"]
|
export type RowSearch = components["schemas"]["searchOutput"]
|
|
@ -95,9 +95,7 @@ export interface paths {
|
||||||
/** Returns the applications that were found based on the search parameters. */
|
/** Returns the applications that were found based on the search parameters. */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": components["schemas"]["applicationSearch"]
|
||||||
data: components["schemas"]["application"][]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,9 +147,7 @@ export interface paths {
|
||||||
/** Returns the queries found based on the search parameters. */
|
/** Returns the queries found based on the search parameters. */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": components["schemas"]["querySearch"]
|
||||||
data: components["schemas"]["query"][]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -449,9 +445,7 @@ export interface paths {
|
||||||
/** Returns the found tables, based on the search parameters. */
|
/** Returns the found tables, based on the search parameters. */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": components["schemas"]["tableSearch"]
|
||||||
data: components["schemas"]["table"][]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -541,9 +535,7 @@ export interface paths {
|
||||||
/** Returns the found users based on search parameters. */
|
/** Returns the found users based on search parameters. */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": components["schemas"]["userSearch"]
|
||||||
data: components["schemas"]["user"][]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -589,6 +581,31 @@ export interface components {
|
||||||
lockedBy?: { [key: string]: unknown }
|
lockedBy?: { [key: string]: unknown }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applicationSearch: {
|
||||||
|
data: {
|
||||||
|
/** @description The name of the app. */
|
||||||
|
name: string
|
||||||
|
/** @description The URL by which the app is accessed, this must be URL encoded. */
|
||||||
|
url: string
|
||||||
|
/** @description The ID of the app. */
|
||||||
|
_id: string
|
||||||
|
/**
|
||||||
|
* @description The status of the app, stating it if is the development or published version.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
status: "development" | "published"
|
||||||
|
/** @description States when the app was created, will be constant. Stored in ISO format. */
|
||||||
|
createdAt: string
|
||||||
|
/** @description States the last time the app was updated - stored in ISO format. */
|
||||||
|
updatedAt: string
|
||||||
|
/** @description States the version of the Budibase client this app is currently based on. */
|
||||||
|
version: string
|
||||||
|
/** @description In a multi-tenant environment this will state the tenant this app is within. */
|
||||||
|
tenantId?: string
|
||||||
|
/** @description The user this app is currently being built by. */
|
||||||
|
lockedBy?: { [key: string]: unknown }
|
||||||
|
}[]
|
||||||
|
}
|
||||||
/** @description The row to be created/updated, based on the table schema. */
|
/** @description The row to be created/updated, based on the table schema. */
|
||||||
row: { [key: string]: unknown }
|
row: { [key: string]: unknown }
|
||||||
searchOutput: {
|
searchOutput: {
|
||||||
|
@ -817,6 +834,113 @@ export interface components {
|
||||||
_id: string
|
_id: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tableSearch: {
|
||||||
|
data: {
|
||||||
|
/** @description The name of the table. */
|
||||||
|
name: string
|
||||||
|
/** @description The name of the column which should be used in relationship tags when relating to this table. */
|
||||||
|
primaryDisplay?: string
|
||||||
|
schema: {
|
||||||
|
[key: string]:
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* @description A relationship column.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type?: "link"
|
||||||
|
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */
|
||||||
|
constraints?: {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "string" | "number" | "object" | "boolean"
|
||||||
|
/** @description Defines whether the column is required or not. */
|
||||||
|
presence?: boolean
|
||||||
|
}
|
||||||
|
/** @description The name of the column. */
|
||||||
|
name?: string
|
||||||
|
/** @description Defines whether the column is automatically generated. */
|
||||||
|
autocolumn?: boolean
|
||||||
|
/** @description The name of the column which a relationship column is related to in another table. */
|
||||||
|
fieldName?: string
|
||||||
|
/** @description The ID of the table which a relationship column is related to. */
|
||||||
|
tableId?: string
|
||||||
|
/**
|
||||||
|
* @description Defines the type of relationship that this column will be used for.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
relationshipType?:
|
||||||
|
| "one-to-many"
|
||||||
|
| "many-to-one"
|
||||||
|
| "many-to-many"
|
||||||
|
/** @description When using a SQL table that contains many to many relationships this defines the table the relationships are linked through. */
|
||||||
|
through?: string
|
||||||
|
/** @description When using a SQL table that contains a one to many relationship this defines the foreign key. */
|
||||||
|
foreignKey?: string
|
||||||
|
/** @description When using a SQL table that utilises a through table, this defines the primary key in the through table for this table. */
|
||||||
|
throughFrom?: string
|
||||||
|
/** @description When using a SQL table that utilises a through table, this defines the primary key in the through table for the related table. */
|
||||||
|
throughTo?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* @description A formula column.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type?: "formula"
|
||||||
|
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */
|
||||||
|
constraints?: {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "string" | "number" | "object" | "boolean"
|
||||||
|
/** @description Defines whether the column is required or not. */
|
||||||
|
presence?: boolean
|
||||||
|
}
|
||||||
|
/** @description The name of the column. */
|
||||||
|
name?: string
|
||||||
|
/** @description Defines whether the column is automatically generated. */
|
||||||
|
autocolumn?: boolean
|
||||||
|
/** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */
|
||||||
|
formula?: string
|
||||||
|
/**
|
||||||
|
* @description Defines whether this is a static or dynamic formula.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
formulaType?: "static" | "dynamic"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* @description Defines the type of the column, most explain themselves, a link column is a relationship.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type?:
|
||||||
|
| "string"
|
||||||
|
| "longform"
|
||||||
|
| "options"
|
||||||
|
| "number"
|
||||||
|
| "boolean"
|
||||||
|
| "array"
|
||||||
|
| "datetime"
|
||||||
|
| "attachment"
|
||||||
|
| "link"
|
||||||
|
| "formula"
|
||||||
|
| "auto"
|
||||||
|
| "json"
|
||||||
|
| "internal"
|
||||||
|
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */
|
||||||
|
constraints?: {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "string" | "number" | "object" | "boolean"
|
||||||
|
/** @description Defines whether the column is required or not. */
|
||||||
|
presence?: boolean
|
||||||
|
}
|
||||||
|
/** @description The name of the column. */
|
||||||
|
name?: string
|
||||||
|
/** @description Defines whether the column is automatically generated. */
|
||||||
|
autocolumn?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @description The ID of the table. */
|
||||||
|
_id: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
/** @description The query body must contain the required parameters for the query, this depends on query type, setup and bindings. */
|
/** @description The query body must contain the required parameters for the query, this depends on query type, setup and bindings. */
|
||||||
executeQuery: { [key: string]: unknown }
|
executeQuery: { [key: string]: unknown }
|
||||||
executeQueryOutput: {
|
executeQueryOutput: {
|
||||||
|
@ -855,6 +979,31 @@ export interface components {
|
||||||
/** @description Whether the query has readable data. */
|
/** @description Whether the query has readable data. */
|
||||||
readable?: boolean
|
readable?: boolean
|
||||||
}
|
}
|
||||||
|
querySearch: {
|
||||||
|
data: {
|
||||||
|
/** @description The ID of the query. */
|
||||||
|
_id: string
|
||||||
|
/** @description The ID of the data source the query belongs to. */
|
||||||
|
datasourceId?: string
|
||||||
|
/** @description The bindings which are required to perform this query. */
|
||||||
|
parameters?: string[]
|
||||||
|
/** @description The fields that are used to perform this query, e.g. the sql statement */
|
||||||
|
fields?: { [key: string]: unknown }
|
||||||
|
/**
|
||||||
|
* @description The verb that describes this query.
|
||||||
|
* @enum {undefined}
|
||||||
|
*/
|
||||||
|
queryVerb?: "create" | "read" | "update" | "delete"
|
||||||
|
/** @description The name of the query. */
|
||||||
|
name: string
|
||||||
|
/** @description The schema of the data returned when the query is executed. */
|
||||||
|
schema: { [key: string]: unknown }
|
||||||
|
/** @description The JavaScript transformer function, applied after the query responds with data. */
|
||||||
|
transformer?: string
|
||||||
|
/** @description Whether the query has readable data. */
|
||||||
|
readable?: boolean
|
||||||
|
}[]
|
||||||
|
}
|
||||||
user: {
|
user: {
|
||||||
/** @description The email address of the user, this must be unique. */
|
/** @description The email address of the user, this must be unique. */
|
||||||
email: string
|
email: string
|
||||||
|
@ -917,6 +1066,39 @@ export interface components {
|
||||||
_id: string
|
_id: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
userSearch: {
|
||||||
|
data: {
|
||||||
|
/** @description The email address of the user, this must be unique. */
|
||||||
|
email: string
|
||||||
|
/** @description The password of the user if using password based login - this will never be returned. This can be left out of subsequent requests (updates) and will be enriched back into the user structure. */
|
||||||
|
password?: string
|
||||||
|
/**
|
||||||
|
* @description The status of the user, if they are active.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
status?: "active"
|
||||||
|
/** @description The first name of the user */
|
||||||
|
firstName?: string
|
||||||
|
/** @description The last name of the user */
|
||||||
|
lastName?: string
|
||||||
|
/** @description If set to true forces the user to reset their password on first login. */
|
||||||
|
forceResetPassword?: boolean
|
||||||
|
/** @description Describes if the user is a builder user or not. */
|
||||||
|
builder?: {
|
||||||
|
/** @description If set to true the user will be able to build any app in the system. */
|
||||||
|
global?: boolean
|
||||||
|
}
|
||||||
|
/** @description Describes if the user is an admin user or not. */
|
||||||
|
admin?: {
|
||||||
|
/** @description If set to true the user will be able to administrate the system. */
|
||||||
|
global?: boolean
|
||||||
|
}
|
||||||
|
/** @description Contains the roles of the user per app (assuming they are not a builder user). */
|
||||||
|
roles: { [key: string]: string }
|
||||||
|
/** @description The ID of the user. */
|
||||||
|
_id: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
nameSearch: {
|
nameSearch: {
|
||||||
/** @description The name to be used when searching - this will be used in a case insensitive starts with match. */
|
/** @description The name to be used when searching - this will be used in a case insensitive starts with match. */
|
||||||
name: string
|
name: string
|
||||||
|
|
|
@ -6,8 +6,8 @@ const nextConfig = {
|
||||||
includePaths: [join(__dirname, "styles")]
|
includePaths: [join(__dirname, "styles")]
|
||||||
},
|
},
|
||||||
serverRuntimeConfig: {
|
serverRuntimeConfig: {
|
||||||
apiKey: "",
|
apiKey: "bf4d86af933b5ac0af0fdbe4bf7d89ff-f929752a1eeaafb00f4b5e3325097d51a44fe4b39f22ed857923409cc75414b379323a25ebfb4916",
|
||||||
appName: "",
|
appName: "sales",
|
||||||
host: "http://localhost:10000"
|
host: "http://localhost:10000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import getConfig from "next/config"
|
import getConfig from "next/config"
|
||||||
import fetch from "node-fetch"
|
import {App, AppSearch, Table, TableSearch} from "../../definitions"
|
||||||
import { App, AppSearch, RowSearch } from "../../definitions"
|
|
||||||
|
|
||||||
const { serverRuntimeConfig } = getConfig()
|
const { serverRuntimeConfig } = getConfig()
|
||||||
const apiKey = serverRuntimeConfig["apiKey"]
|
const apiKey = serverRuntimeConfig["apiKey"]
|
||||||
const appName = serverRuntimeConfig["appName"]
|
const appName = serverRuntimeConfig["appName"]
|
||||||
const host = serverRuntimeConfig["host"]
|
const host = serverRuntimeConfig["host"]
|
||||||
|
|
||||||
async function makeCall(method: string, url: string, opts?: { body?: any, appId?: string } = {}): Promise<any> {
|
let APP: App | null = null
|
||||||
|
let TABLES: { [key: string]: Table } = {}
|
||||||
|
|
||||||
|
async function makeCall(method: string, url: string, opts?: { body?: any, appId?: string }): Promise<any> {
|
||||||
const fetchOpts: any = {
|
const fetchOpts: any = {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -18,49 +20,96 @@ async function makeCall(method: string, url: string, opts?: { body?: any, appId?
|
||||||
fetchOpts.headers["x-budibase-app-id"] = opts.appId
|
fetchOpts.headers["x-budibase-app-id"] = opts.appId
|
||||||
}
|
}
|
||||||
if (opts?.body) {
|
if (opts?.body) {
|
||||||
fetchOpts.body = JSON.stringify(opts?.body)
|
fetchOpts.body = typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body
|
||||||
fetchOpts.headers["Content-Type"] = "application/json"
|
fetchOpts.headers["Content-Type"] = "application/json"
|
||||||
}
|
}
|
||||||
const response = await fetch(`${host}/public/v1/${url}`, fetchOpts)
|
const finalUrl = `${host}/api/public/v1/${url}`
|
||||||
if (response.status === 200) {
|
const response = await fetch(finalUrl, fetchOpts)
|
||||||
|
if (response.ok) {
|
||||||
return response.json()
|
return response.json()
|
||||||
} else {
|
} else {
|
||||||
throw new Error(await response.text())
|
const error = await response.text()
|
||||||
|
console.error("Budibase server error - ", error)
|
||||||
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getApp(): Promise<App> {
|
async function getApp(): Promise<App> {
|
||||||
|
if (APP) {
|
||||||
|
return APP
|
||||||
|
}
|
||||||
const apps: AppSearch = await makeCall("post", "applications/search", {
|
const apps: AppSearch = await makeCall("post", "applications/search", {
|
||||||
body: {
|
body: {
|
||||||
name: appName,
|
name: appName,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!Array.isArray(apps?.data)) {
|
|
||||||
throw new Error("Fatal error, no apps found.")
|
|
||||||
}
|
|
||||||
const app = apps.data.find((app: App) => app.name === appName)
|
const app = apps.data.find((app: App) => app.name === appName)
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new Error("Could not find app, please make sure app name in config is correct.")
|
throw new Error("Could not find app, please make sure app name in config is correct.")
|
||||||
}
|
}
|
||||||
|
APP = app
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
async function getSales(req: any) {
|
async function getSales(req: any) {
|
||||||
|
const { page } = req.query
|
||||||
const { _id: appId } = await getApp()
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows/search`, {
|
||||||
|
appId,
|
||||||
|
body: {
|
||||||
|
limit: 10,
|
||||||
|
sort: {
|
||||||
|
type: "string",
|
||||||
|
order: "ascending",
|
||||||
|
column: "sale_id",
|
||||||
|
},
|
||||||
|
paginate: true,
|
||||||
|
bookmark: parseInt(page),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSale(req: any) {
|
async function saveSale(req: any) {
|
||||||
const { _id: appId } = await getApp()
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows`, {
|
||||||
|
body: req.body,
|
||||||
|
appId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(req: any, res: any) {
|
export default async function handler(req: any, res: any) {
|
||||||
let response: any = {}
|
let response: any = {}
|
||||||
if (req.method === "POST") {
|
try {
|
||||||
response = await saveSale(req)
|
if (req.method === "POST") {
|
||||||
} else if (req.method === "GET") {
|
response = await saveSale(req)
|
||||||
response = await getSales(req)
|
} else if (req.method === "GET") {
|
||||||
} else {
|
response = await getSales(req)
|
||||||
res.status(404)
|
} else {
|
||||||
|
res.status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(200).json(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(400).send(err)
|
||||||
}
|
}
|
||||||
res.status(200).json(response)
|
|
||||||
}
|
}
|
|
@ -1,70 +1,61 @@
|
||||||
import type { NextPage } from "next"
|
import type { NextPage } from "next"
|
||||||
import Head from "next/head"
|
|
||||||
import Image from "next/image"
|
|
||||||
import styles from "../styles/home.module.css"
|
import styles from "../styles/home.module.css"
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
|
const [sales, setSales] = useState([])
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
|
||||||
|
const getSales = 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 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 () => {
|
||||||
|
await getSales(currentPage + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToPrevPage = async () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
await getSales(currentPage - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSales().catch(() => {
|
||||||
|
setSales([])
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Head>
|
<h1>Sales</h1>
|
||||||
<title>Create Next App</title>
|
<div>{sales.map((sale: any) => <p key={sale.sale_id}>{sale.sale_id}</p>)}</div>
|
||||||
<meta name="description" content="Generated by create next app" />
|
<button onClick={goToPrevPage}>Prev Page</button>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<button onClick={goToNextPage}>Next Page</button>
|
||||||
</Head>
|
|
||||||
|
|
||||||
<main className={styles.main}>
|
|
||||||
<h1 className={styles.title}>
|
|
||||||
Welcome to <a href="https://nextjs.org">Next.js!</a>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className={styles.description}>
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className={styles.code}>pages/index.tsx</code>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className={styles.grid}>
|
|
||||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
|
||||||
<h2>Documentation →</h2>
|
|
||||||
<p>Find in-depth information about Next.js features and API.</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://nextjs.org/learn" className={styles.card}>
|
|
||||||
<h2>Learn →</h2>
|
|
||||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://github.com/vercel/next.js/tree/canary/examples"
|
|
||||||
className={styles.card}
|
|
||||||
>
|
|
||||||
<h2>Examples →</h2>
|
|
||||||
<p>Discover and deploy boilerplate example Next.js projects.</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
>
|
|
||||||
<h2>Deploy →</h2>
|
|
||||||
<p>
|
|
||||||
Instantly deploy your Next.js site to a public URL with Vercel.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer className={styles.footer}>
|
|
||||||
<a
|
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Powered by{" "}
|
|
||||||
<span className={styles.logo}>
|
|
||||||
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue