diff --git a/examples/with-tigris/.env.development b/examples/with-tigris/.env.development new file mode 100644 index 000000000000000..2035afbfb91cea8 --- /dev/null +++ b/examples/with-tigris/.env.development @@ -0,0 +1,4 @@ +# DO NOT ADD SECRETS TO THIS FILE. This is a good place for defaults. +# If you want to add secrets use `.env.development.local` instead. + +TIGRIS_URI=localhost:8081 diff --git a/examples/with-tigris/.env.local.example b/examples/with-tigris/.env.local.example new file mode 100644 index 000000000000000..d020d0b248581db --- /dev/null +++ b/examples/with-tigris/.env.local.example @@ -0,0 +1,7 @@ +# Enter your tigris uri, ex :- localhost:8081, api.preview.tigrisdata.cloud etc. +TIGRIS_URI= + +# Client credentials, if using auth, can be generated from Tigris cloud console. +# See: https://docs.tigrisdata.com/auth +TIGRIS_CLIENT_ID= +TIGRIS_CLIENT_SECRET= diff --git a/examples/with-tigris/.env.production b/examples/with-tigris/.env.production new file mode 100644 index 000000000000000..b4b9c026379d851 --- /dev/null +++ b/examples/with-tigris/.env.production @@ -0,0 +1,4 @@ +# DO NOT ADD SECRETS TO THIS FILE. This is a good place for defaults. +# If you want to add secrets use `.env.production.local` instead. + +TIGRIS_URI=api.preview.tigrisdata.cloud diff --git a/examples/with-tigris/.gitignore b/examples/with-tigris/.gitignore new file mode 100644 index 000000000000000..b0167ec8627dad4 --- /dev/null +++ b/examples/with-tigris/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# dependencies +/node_modules +/.pnp +.pnp.js +# testing +/coverage +# next.js +/.next/ +/out/ +# production +/build +# misc +.DS_Store +*.pem +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +# local env files +.env*.local +# vercel +.vercel +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/with-tigris/README.md b/examples/with-tigris/README.md new file mode 100644 index 000000000000000..58d9abdd15f4fd1 --- /dev/null +++ b/examples/with-tigris/README.md @@ -0,0 +1,171 @@ +# ⚑ ️Tigris example app on Next.js - Todo list + +A simple todo app built on [Next.js][next-url] and [Tigris](https://docs.tigrisdata.com/) +using [TypeScript client](https://docs.tigrisdata.com/typescript/), deployed on [Vercel][vercel-url]. + +### Project demo + +https://tigris-nextjs-starter-kit.vercel.app/ + +# βš™οΈ Deploying your own + +All you need is a [Github](https://github.com), [Vercel][vercel-url] and Tigris +account([sign up for a free account](https://www.tigrisdata.com/nextjs#signup-form)). Now, Hit "Deploy" +and follow instructions to deploy app to your Vercel account + +[![Deploy with Vercel](https://vercel.com/button)][deploy-url] + +:tada: All done. You should be able to use app on the URL provided by Vercel. Feel free to play around +or do a [code walkthrough](#code-walkthrough) next :tada: + +> [Tigris integration](https://vercel.com/integrations/tigris) with Vercel will automatically fetch +> access keys to populate [Environment Variables](.env.local.example) when deploying app. + +
+2. Running Next.js server & Tigris dev environment on your local computer + +## πŸ“– Running Next.js server & Tigris locally + +### Prerequisites + +1. Tigris installed on your dev computer + 1. For **macOS**: `brew install tigrisdata/tigris/tigris-cli` + 2. Other operating systems: [See installation instructions here](https://docs.tigrisdata.com/cli/installation) +2. Node.js version 16+ + +### Instructions + +1. Clone this repo on your computer + +```shell +git clone https://github.com/tigrisdata/tigris-vercel-starter +``` + +2. Install dependencies + +```shell +cd tigris-vercel-starter +npm install +``` + +3. Start Tigris local development environment + +```shell +tigris dev start +``` + +4. Run the Next.js server + +```shell +npm run dev +``` + +> Note: This step uses a custom dev & build script to initialize Tigris database and collection for +> the app and requires [ts-node](https://www.npmjs.com/package/ts-node#installation) to be installed. + +:tada: All done. You should be able to use app on `localhost:3000` in browser. Feel free to play +around or do a [code walk-through](#code-walkthrough) next :tada: + +
+ +# πŸ“– How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: + +```bash +npx create-next-app --example with-tigris tigris-next-app +``` + +```bash +yarn create next-app --example with-tigris tigris-next-app +``` + +```bash +pnpm create next-app --example with-tigris tigris-next-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). + +# πŸ‘€ Code walkthrough + +
+ πŸ“‚ File structure + +```text +β”œβ”€β”€ package.json +β”œβ”€β”€ lib +β”‚ β”œβ”€β”€ tigris.ts +β”œβ”€β”€ models +β”‚ └── tigris +β”‚ └── todoStarterApp +β”‚ └── todoItems.ts +└── pages + β”œβ”€β”€ index.tsx + └── api + β”œβ”€β”€ item + β”‚ β”œβ”€β”€ [id].ts + └── items + β”œβ”€β”€ index.ts + └── search.ts +``` + +
+ +
+ πŸͺ’ Tigris schema definition + +[models/tigris/todoStarterApp/todoItems.ts](models/tigris/todoStarterApp/todoItems.ts) - The to-do list app +has a single collection `todoItems` that stores the to-do items in `todoStarterApp` database. The +Database and Collection get automatically provisioned by the [setup script](scripts/setup.ts). + +This is an inspiration from Next.js based file system router. Create a folder or drop a schema file +inside database folder under `models/tigris/`, and you're able to instantly create Databases and +Collections in Tigris for your application. + +
+ +
+ 🌐 Connecting to Tigris + +[lib/tigris.ts](lib/tigris.ts) - Loads the environment variables you specified previously in creating a Vercel project +section and uses them to configure the Tigris client. + +
+ +
+ ❇️ API routes to access data in Tigris collection + +All the Next.js API routes are defined under `pages/api/`. We have three files exposing endpoints: + +#### [`pages/api/items/index.ts`](pages/api/items/index.ts) + +- `GET /api/items` to get an array of to-do items as Array +- `POST /api/items` to add an item to the list + +#### [`/pages/api/items/search.ts`](/pages/api/items/search.ts) + +- `GET /api/items/search?q=query` to find and return items matching the given query + +#### [`pages/api/item/[id].ts`](pages/api/item/[id].ts) + +- `GET /api/item/{id}` to fetch an item +- `PUT /api/item/{id}` to update the given item +- `DELETE /api/item/[id]` to delete an item + +
+ +# πŸš€ Next steps + +In a few steps, we learnt how to bootstrap a Next.js app using Tigris and deploy it on Vercel. Feel +free to add more functionalities or customize App for your use-case and learn more about +[Tigris data platform](https://docs.tigrisdata.com/overview/) + + + +[typescript]: https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white +[typescript-url]: https://www.typescriptlang.org/ +[vercel]: https://img.shields.io/badge/vercel-F22F46?style=for-the-badge&logo=vercel&logoColor=white +[vercel-url]: https://vercel.com/ +[deploy-url]: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftigrisdata%2Ftigris-vercel-starter&project-name=todo-list-app-tigris&repo-name=todo-list-webapp-tigris&demo-title=My%20To-do%20list%20webapp&demo-description=A%20To-do%20list%20webapp%20using%20NextJS%20and%20Tigris&integration-ids=oac_Orjx197uMuJobdSaEpVv2Zn8 +[next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white +[next-url]: https://nextjs.org/ diff --git a/examples/with-tigris/components/EachToDo.tsx b/examples/with-tigris/components/EachToDo.tsx new file mode 100644 index 000000000000000..2409a68f4a5f631 --- /dev/null +++ b/examples/with-tigris/components/EachToDo.tsx @@ -0,0 +1,47 @@ +import Image from 'next/image' +import React from 'react' +import { TodoItem } from '../models/tigris/todoStarterApp/todoItems' +import styles from '../styles/EachToDo.module.css' + +type Props = { + toDoItem: TodoItem + deleteHandler: (id?: number) => void + updateHandler: (item: TodoItem) => void +} +const EachTodo = ({ toDoItem, deleteHandler, updateHandler }: Props) => { + return ( + <> +
  • + + +
  • + + ) +} + +export default EachTodo diff --git a/examples/with-tigris/components/LoaderWave.tsx b/examples/with-tigris/components/LoaderWave.tsx new file mode 100644 index 000000000000000..4e9e0ceb5d8df03 --- /dev/null +++ b/examples/with-tigris/components/LoaderWave.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import styles from '../styles/LoaderWave.module.css' + +const LoaderWave = () => { + return ( +
    +
    +
    +
    +
    + ) +} + +export default LoaderWave diff --git a/examples/with-tigris/lib/tigris.ts b/examples/with-tigris/lib/tigris.ts new file mode 100644 index 000000000000000..365739f87642b48 --- /dev/null +++ b/examples/with-tigris/lib/tigris.ts @@ -0,0 +1,28 @@ +import { DB, Tigris } from '@tigrisdata/core' + +const DB_NAME = 'todoStarterApp' + +declare global { + // eslint-disable-next-line no-var + var tigrisDb: DB +} + +let tigrisDb: DB + +// Caching the client because `next dev` would otherwise create a +// new connection on every file save while previous connection is active due to +// hot reloading. However, in production, Next.js would completely tear down before +// restarting, thus, disconnecting and reconnecting to Tigris. +if (process.env.NODE_ENV !== 'production') { + if (!global.tigrisDb) { + const tigrisClient = new Tigris() + global.tigrisDb = tigrisClient.getDatabase(DB_NAME) + } + tigrisDb = global.tigrisDb +} else { + const tigrisClient = new Tigris() + tigrisDb = tigrisClient.getDatabase(DB_NAME) +} + +// export to share DB across modules +export default tigrisDb diff --git a/examples/with-tigris/models/tigris/todoStarterApp/todoItems.ts b/examples/with-tigris/models/tigris/todoStarterApp/todoItems.ts new file mode 100644 index 000000000000000..9953fe98f6ec013 --- /dev/null +++ b/examples/with-tigris/models/tigris/todoStarterApp/todoItems.ts @@ -0,0 +1,22 @@ +import { + TigrisCollectionType, + TigrisDataTypes, + TigrisSchema, +} from '@tigrisdata/core/dist/types' + +export const COLLECTION_NAME = 'todoItems' + +export interface TodoItem extends TigrisCollectionType { + id?: number + text: string + completed: boolean +} + +export const TodoItemSchema: TigrisSchema = { + id: { + type: TigrisDataTypes.INT32, + primary_key: { order: 1, autoGenerate: true }, + }, + text: { type: TigrisDataTypes.STRING }, + completed: { type: TigrisDataTypes.BOOLEAN }, +} diff --git a/examples/with-tigris/next.config.js b/examples/with-tigris/next.config.js new file mode 100644 index 000000000000000..897aacd8340bad0 --- /dev/null +++ b/examples/with-tigris/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + swcMinify: true, +} + +module.exports = nextConfig diff --git a/examples/with-tigris/package.json b/examples/with-tigris/package.json new file mode 100644 index 000000000000000..56eea5008620eba --- /dev/null +++ b/examples/with-tigris/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "scripts": { + "predev": "APP_ENV=development npm run setup", + "dev": "next dev", + "build": "next build", + "postbuild": "APP_ENV=production npm run setup", + "start": "next start", + "setup": "npx ts-node scripts/setup.ts" + }, + "dependencies": { + "@tigrisdata/core": "beta", + "next": "latest", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/node": "18.11.2", + "@types/react": "18.0.21", + "@types/react-dom": "18.0.6", + "typescript": "4.8.4" + } +} diff --git a/examples/with-tigris/pages/_app.tsx b/examples/with-tigris/pages/_app.tsx new file mode 100644 index 000000000000000..7a8af80e5ba7faf --- /dev/null +++ b/examples/with-tigris/pages/_app.tsx @@ -0,0 +1,9 @@ +import '../styles/globals.css' +import type { AppProps } from 'next/app' +import React from 'react' + +function MyApp({ Component, pageProps }: AppProps) { + return +} + +export default MyApp diff --git a/examples/with-tigris/pages/api/item/[id].ts b/examples/with-tigris/pages/api/item/[id].ts new file mode 100644 index 000000000000000..6e5901053cc363e --- /dev/null +++ b/examples/with-tigris/pages/api/item/[id].ts @@ -0,0 +1,85 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + COLLECTION_NAME, + TodoItem, +} from '../../../models/tigris/todoStarterApp/todoItems' +import tigrisDb from '../../../lib/tigris' + +type Data = { + result?: TodoItem + error?: string +} + +async function handleGet( + req: NextApiRequest, + res: NextApiResponse, + itemId: number +) { + try { + const itemsCollection = tigrisDb.getCollection(COLLECTION_NAME) + const item = await itemsCollection.findOne({ id: itemId }) + if (!item) { + res.status(404).json({ error: 'No item found' }) + } else { + res.status(200).json({ result: item }) + } + } catch (err) { + const error = err as Error + res.status(500).json({ error: error.message }) + } +} + +async function handlePut(req: NextApiRequest, res: NextApiResponse) { + try { + const item = JSON.parse(req.body) as TodoItem + const itemsCollection = tigrisDb.getCollection(COLLECTION_NAME) + const updated = await itemsCollection.insertOrReplaceOne(item) + res.status(200).json({ result: updated }) + } catch (err) { + const error = err as Error + res.status(500).json({ error: error.message }) + } +} + +async function handleDelete( + req: NextApiRequest, + res: NextApiResponse, + itemId: number +) { + try { + const itemsCollection = tigrisDb.getCollection(COLLECTION_NAME) + const status = (await itemsCollection.deleteOne({ id: itemId })).status + if (status === 'deleted') { + res.status(200).json({}) + } else { + res.status(500).json({ error: `Failed to delete ${itemId}` }) + } + } catch (err) { + const error = err as Error + res.status(500).json({ error: error.message }) + } +} + +// GET /api/item/[id] -- gets item from collection where id = [id] +// PUT /api/item/[id] {ToDoItem} -- updates the item in collection where id = [id] +// DELETE /api/item/[id] -- deletes the item in collection where id = [id] +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { id } = req.query + switch (req.method) { + case 'GET': + await handleGet(req, res, Number(id)) + break + case 'PUT': + await handlePut(req, res) + break + case 'DELETE': + await handleDelete(req, res, Number(id)) + break + default: + res.setHeader('Allow', ['GET', 'PUT', 'DELETE']) + res.status(405).end(`Method ${req.method} Not Allowed`) + } +} diff --git a/examples/with-tigris/pages/api/items/index.ts b/examples/with-tigris/pages/api/items/index.ts new file mode 100644 index 000000000000000..7f65b578246dc2a --- /dev/null +++ b/examples/with-tigris/pages/api/items/index.ts @@ -0,0 +1,54 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import { + COLLECTION_NAME, + TodoItem, +} from '../../../models/tigris/todoStarterApp/todoItems' +import tigrisDb from '../../../lib/tigris' + +type Response = { + result?: Array + error?: string +} + +async function handleGet(req: NextApiRequest, res: NextApiResponse) { + try { + const itemsCollection = tigrisDb.getCollection(COLLECTION_NAME) + const cursor = itemsCollection.findMany() + const items = await cursor.toArray() + res.status(200).json({ result: items }) + } catch (err) { + const error = err as Error + res.status(500).json({ error: error.message }) + } +} + +async function handlePost(req: NextApiRequest, res: NextApiResponse) { + try { + const item = JSON.parse(req.body) as TodoItem + const itemsCollection = tigrisDb.getCollection(COLLECTION_NAME) + const inserted = await itemsCollection.insertOne(item) + res.status(200).json({ result: [inserted] }) + } catch (err) { + const error = err as Error + res.status(500).json({ error: error.message }) + } +} + +// GET /api/items -- gets items from collection +// POST /api/items {ToDoItem} -- inserts a new item to collection +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + switch (req.method) { + case 'GET': + await handleGet(req, res) + break + case 'POST': + await handlePost(req, res) + break + default: + res.setHeader('Allow', ['GET', 'POST']) + res.status(405).end(`Method ${req.method} Not Allowed`) + } +} diff --git a/examples/with-tigris/pages/api/items/search.ts b/examples/with-tigris/pages/api/items/search.ts new file mode 100644 index 000000000000000..e4e2dd0115df037 --- /dev/null +++ b/examples/with-tigris/pages/api/items/search.ts @@ -0,0 +1,37 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + COLLECTION_NAME, + TodoItem, +} from '../../../models/tigris/todoStarterApp/todoItems' +import { SearchRequest } from '@tigrisdata/core/dist/search/types' +import tigrisDb from '../../../lib/tigris' + +type Data = { + result?: Array + error?: string +} + +// GET /api/items/search?q=searchQ -- searches for items matching text `searchQ` +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const query = req.query['q'] + if (query === undefined) { + res.status(400).json({ error: 'No search query found in request' }) + return + } + try { + const itemsCollection = tigrisDb.getCollection(COLLECTION_NAME) + const searchRequest: SearchRequest = { q: query as string } + const searchResult = await itemsCollection.search(searchRequest) + const items = new Array() + for (const hit of searchResult.hits) { + items.push(hit.document) + } + res.status(200).json({ result: items }) + } catch (err) { + const error = err as Error + res.status(500).json({ error: error.message }) + } +} diff --git a/examples/with-tigris/pages/index.tsx b/examples/with-tigris/pages/index.tsx new file mode 100644 index 000000000000000..689fbc9bcb45c6e --- /dev/null +++ b/examples/with-tigris/pages/index.tsx @@ -0,0 +1,255 @@ +import type { NextPage } from 'next' +import Image from 'next/image' +import Head from 'next/head' +import React, { useEffect, useState } from 'react' +import EachTodo from '../components/EachToDo' +import LoaderWave from '../components/LoaderWave' +import { TodoItem } from '../models/tigris/todoStarterApp/todoItems' +import styles from '../styles/Home.module.css' + +const Home: NextPage = () => { + // This is the input field + const [textInput, setTextInput] = useState('') + + // Todo list array which displays the todo items + const [todoList, setTodoList] = useState([]) + + // Loading and Error flags for the template + const [isLoading, setIsLoading] = useState(false) + const [isError, setIsError] = useState(false) + + // This is use to animate the input text field + const [wiggleError, setWiggleError] = useState(false) + + // Two separate views. 1. List view for todo items & 2. Search result view + type viewModeType = 'list' | 'search' + const [viewMode, setViewMode] = useState('list') + + // Util search query/input check + /* + The is a helper util method, that validtes the input field via a regex and returns a true or false. + This also wiggles the text input if the regex doesn't find any match. + */ + const queryCheckWiggle = () => { + const result: RegExpMatchArray | null = textInput.match('^\\S.{0,100}$') + if (result === null) { + setWiggleError(true) + return true + } + return false + } + + useEffect(() => { + if (!wiggleError) { + return + } + const timeOut = setTimeout(() => { + setWiggleError(false) + }, 500) + + return () => clearTimeout(timeOut) + }, [wiggleError]) + + // Search query + /* + 'searchQuery' method takes the state from 'textInput' and send it over to the 'api/items/search' endpoint via a query param 'q'. + The response is the same as the response from "fetch('/api/items')", an array of TodoItems if successful. + */ + const searchQuery = () => { + if (queryCheckWiggle()) { + return + } + setIsLoading(true) + + fetch(`/api/items/search?q=${encodeURI(textInput)}`, { + method: 'GET', + }) + .then((response) => response.json()) + .then((data) => { + setIsLoading(false) + if (data.result) { + setViewMode('search') + setTodoList(data.result) + } + }) + } + + // Fetch Todo List + /* + 'fetchListItems' is the first method that's called when the component is mounted from the useEffect below. + This sets some of the state like 'isLoading' and 'isError' before it fetches for data from the endpoint defined under 'pages/api/items/index'. + The api endpoint returns a json with the key 'result' and a status 200 if successful or returns a status 500 along with the 'error' key. + If the 'result' key is present we safely set the 'todoList'. + */ + const fetchListItems = () => { + setIsLoading(true) + setIsError(false) + + fetch('/api/items') + .then((response) => response.json()) + .then((data) => { + setIsLoading(false) + if (data.result) { + setViewMode('list') + setTodoList(data.result) + } else { + setIsError(true) + } + }) + .catch(() => { + setIsLoading(false) + setIsError(true) + }) + } + + // Load the initial list of todo-items + useEffect(() => { + fetchListItems() + }, []) + + // Add a new todo-item + /* + 'addToDoItem' takes the 'textInput' state, creates a 'TodoItem' & converts it to a JSON. + Sends it over as body payload to the api endpoint; which is how the api expects and is defined in 'pages/api/items' 'POST' switch. + */ + const addToDoItem = () => { + if (queryCheckWiggle()) { + return + } + setIsLoading(true) + + fetch('/api/items', { + method: 'POST', + body: JSON.stringify({ text: textInput, completed: false }), + }).then(() => { + setIsLoading(false) + setTextInput('') + fetchListItems() + }) + } + + // Delete Todo-item + /* + 'deleteTodoItem' requires an id value of the TodoItem. When the user presses the 'delete'(cross) button from a TodoItem, this method is invoked. + It calls the endpoint 'api/item/' with the 'DELETE' method. Read the method 'handleDelete' under pages/api/item/[id]' to learn more how the api handles deletion. + */ + const deleteTodoItem = (id?: number) => { + setIsLoading(true) + + fetch('/api/item/' + id, { + method: 'DELETE', + }).then(() => { + setIsLoading(false) + if (viewMode === 'list') { + fetchListItems() + } else { + searchQuery() + } + }) + } + + // Update Todo-item (mark complete/incomplete) + /* + 'updateTodoItem' takes the TodoItem object, inverts the 'completed' boolean and calls the same endpoint as 'deletion' but with a different method 'PUT'. + Navigate to 'api/item/[id]' and read more how the api handles updates under the 'handlePut' method. + */ + const updateTodoItem = (item: TodoItem) => { + item.completed = !item.completed + setIsLoading(true) + + fetch('/api/item/' + item.id, { + method: 'PUT', + body: JSON.stringify(item), + }).then(() => { + setIsLoading(false) + if (viewMode === 'list') { + fetchListItems() + } else { + searchQuery() + } + }) + } + + return ( +
    + + Todo App using Next.js + Tigris + + + +
    +

    Sample Todo app using Next.js and Tigris

    + + {/* Search Header */} +
    + { + setWiggleError(false) + setTextInput(e.target.value) + }} + placeholder="Type an item to add or search" + /> + + +
    + + {/* Results section */} +
    + {/* Loader, Errors and Back to List mode */} + {isError && ( +

    Something went wrong..

    + )} + {isLoading && } + {viewMode === 'search' && ( + + )} + + {/* Todo Item List */} + {todoList.length < 1 ? ( +

    + {viewMode === 'search' + ? 'No items found.. ' + : 'Add a todo by typing in the field above and hit Add!'} +

    + ) : ( +
      + {todoList.map((each) => { + return ( + + ) + })} +
    + )} +
    + + + Tigris Logo + +
    +
    + ) +} + +export default Home diff --git a/examples/with-tigris/public/circle-checked.svg b/examples/with-tigris/public/circle-checked.svg new file mode 100644 index 000000000000000..b4427995977150e --- /dev/null +++ b/examples/with-tigris/public/circle-checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/with-tigris/public/circle.svg b/examples/with-tigris/public/circle.svg new file mode 100644 index 000000000000000..1fb66edd924fd90 --- /dev/null +++ b/examples/with-tigris/public/circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-tigris/public/delete.svg b/examples/with-tigris/public/delete.svg new file mode 100644 index 000000000000000..8f293bca4d43ce5 --- /dev/null +++ b/examples/with-tigris/public/delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/with-tigris/public/favicon.ico b/examples/with-tigris/public/favicon.ico new file mode 100644 index 000000000000000..0097d88f2c065a9 Binary files /dev/null and b/examples/with-tigris/public/favicon.ico differ diff --git a/examples/with-tigris/public/readme/logo.svg b/examples/with-tigris/public/readme/logo.svg new file mode 100755 index 000000000000000..9fb56cdded3d1ab --- /dev/null +++ b/examples/with-tigris/public/readme/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/with-tigris/public/readme/todo_app_screenshot.jpg b/examples/with-tigris/public/readme/todo_app_screenshot.jpg new file mode 100644 index 000000000000000..46e681701515a4e Binary files /dev/null and b/examples/with-tigris/public/readme/todo_app_screenshot.jpg differ diff --git a/examples/with-tigris/public/tigris_logo.svg b/examples/with-tigris/public/tigris_logo.svg new file mode 100644 index 000000000000000..9a5e50e0fe76f5b --- /dev/null +++ b/examples/with-tigris/public/tigris_logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/with-tigris/public/vercel.svg b/examples/with-tigris/public/vercel.svg new file mode 100644 index 000000000000000..fbf0e25a651c289 --- /dev/null +++ b/examples/with-tigris/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/examples/with-tigris/scripts/setup.ts b/examples/with-tigris/scripts/setup.ts new file mode 100644 index 000000000000000..42ad6b69d308a79 --- /dev/null +++ b/examples/with-tigris/scripts/setup.ts @@ -0,0 +1,19 @@ +import { Tigris } from '@tigrisdata/core' +import { loadEnvConfig } from '@next/env' + +// Run the config loader only when not executing within next runtime +if (process.env.NODE_ENV === undefined) { + if (process.env.APP_ENV === 'development') { + loadEnvConfig(process.cwd(), true) + } else if (process.env.APP_ENV === 'production') { + loadEnvConfig(process.cwd()) + } +} + +async function main() { + // setup client and register schemas + const tigrisClient = new Tigris() + await tigrisClient.registerSchemas('models/tigris') +} + +main() diff --git a/examples/with-tigris/styles/EachToDo.module.css b/examples/with-tigris/styles/EachToDo.module.css new file mode 100644 index 000000000000000..ed1556d2cd5cab0 --- /dev/null +++ b/examples/with-tigris/styles/EachToDo.module.css @@ -0,0 +1,58 @@ +.each { + display: flex; + flex-direction: row; + border-bottom: 1px solid #dddddd; + padding: 10px 25px; + animation: fadeInAnimation 0.4s cubic-bezier(0.62, 0.57, 0, 1.02); +} +.each:last-child { + border-bottom: none; +} + +.eachButton { + background-color: transparent !important; + width: 100%; + text-align: left; + vertical-align: middle; + padding: 0 !important; +} + +.eachButton:hover img { + content: url('/circle-checked.svg'); +} + +.eachButton:hover span { + text-decoration: line-through; +} + +.each span { + vertical-align: middle; + padding-left: 10px; + font-size: 14px; +} + +.deleteBtn { + background-color: transparent !important; + text-align: center !important; + min-width: 20px !important; + padding: 10px !important; +} + +.deleteBtn:hover, +.deleteBtn:focus, +.deleteBtn:active, +.deleteBtn:focus-visible { + filter: brightness(0) saturate(100%) invert(98%) sepia(27%) saturate(4410%) + hue-rotate(308deg) brightness(115%) contrast(98%); +} + +@keyframes fadeInAnimation { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: none; + } +} diff --git a/examples/with-tigris/styles/Home.module.css b/examples/with-tigris/styles/Home.module.css new file mode 100644 index 000000000000000..d6f108465eeea37 --- /dev/null +++ b/examples/with-tigris/styles/Home.module.css @@ -0,0 +1,138 @@ +.container { + display: flex; + flex-wrap: wrap; + flex-direction: column; + align-items: center; + gap: 20px; + margin-top: 50px; + justify-content: space-between; + align-content: space-around; +} + +.searchHeader { + display: flex; + flex-direction: row; + gap: 10px; + margin-top: 20px; +} + +.searchInput { + min-width: 300px; +} + +.results { + position: relative; + background-color: #fff; + min-width: 600px; + min-height: 200px; + border-radius: 8px; + color: #0f2832; + overflow: hidden; +} + +.container input { + border: 1px solid #fff; + background-color: white; + padding: 8px 16px; + background-color: #fff; + border-radius: 8px; + border-color: #d9dbe1; + color: #0f2832; + width: 100%; + display: block; + height: 40px; + font-size: 12px; + font-weight: 400; + line-height: 22px; + letter-spacing: 0px; + transition: background-color 0.2s, box-shadow 0.2s; + caret-color: #4ed9b2; +} + +.container input:hover, +.container input:focus, +.container input:active, +.container input:focus-visible { + border-color: #4ed9b2; + outline: #4ed9b2; +} + +.container input:disabled { + pointer-events: none; + border: 1px solid #d9dbe1; + color: #a2a3a8; +} + +.container button { + align-items: center; + border-radius: 8px; + color: #0f2832; + cursor: pointer; + font-size: 12px; + font-weight: 400; + line-height: 19px; + min-height: 40px; + min-width: 80px; + padding: 6px 20px; + position: relative; + overflow: hidden; + transition: background-color 0.5s; + background-color: #efb700; +} + +.container button:hover, +.container button:focus, +.container button:active { + background-color: #fddc7b; +} + +.errorText { + font-size: 12px; + text-align: center; + padding: 20px; + color: #ed6f6c; +} + +@keyframes shake { + 0% { + transform: translateX(0); + } + 25% { + transform: translateX(2px); + } + 50% { + transform: translateX(0px); + } + 75% { + transform: translateX(-2px); + } + 100% { + transform: translateX(0); + } +} + +.invalid { + animation: shake 0.2s ease-in-out 0s 2; +} + +.noItems { + padding: 20px; + text-align: center; + margin-top: 20px; + color: #a2a3a8; +} + +.clearSearch { + background-color: transparent !important; + color: #efb700 !important; + border-radius: 4px !important; + margin: 0 auto; + display: block; + text-align: center; + min-height: 20px !important; + margin-top: 20px; +} + +.clearSearch:hover { + color: #fddc7b !important; +} diff --git a/examples/with-tigris/styles/LoaderWave.module.css b/examples/with-tigris/styles/LoaderWave.module.css new file mode 100644 index 000000000000000..f34a1a1b20a60a0 --- /dev/null +++ b/examples/with-tigris/styles/LoaderWave.module.css @@ -0,0 +1,52 @@ +.holder { + position: absolute; + width: 100%; + height: 100%; + text-align: center; + padding: 50px; + background-color: rgba(255, 255, 255, 0); + z-index: 1; + border-radius: 8px; +} + +.l1 { + animation: load7 1s infinite ease-in-out; +} +.l2 { + animation: load7 1s infinite ease-in-out; + animation-delay: 0.13s; +} +.l3 { + animation: load7 1s infinite ease-in-out; + animation-delay: 0.35s; +} + +.loader { + display: inline-block; + color: #4b4821; + font-size: 8px; + width: 14px; + height: 14px; + background-color: #9a9c9f; + transform: scale(0); + text-indent: -9999em; + border-radius: 50%; +} + +@keyframes load7 { + 0% { + transform: scale(0); + } + 50% { + transform: scale(1.1); + } + 60% { + transform: scale(0.8); + } + 80% { + transform: scale(0); + } + 100% { + transform: scale(0); + } +} diff --git a/examples/with-tigris/styles/globals.css b/examples/with-tigris/styles/globals.css new file mode 100644 index 000000000000000..c3660a7014c711a --- /dev/null +++ b/examples/with-tigris/styles/globals.css @@ -0,0 +1,168 @@ +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} + +html, +body { + background-color: #0f2832; +} + +body { + min-width: 768px; + line-height: 1; + font-family: 'Rubik', sans-serif; + color: #ffffff; +} + +ol, +ul { + list-style: none; +} + +blockquote, +q { + quotes: none; +} + +button { + cursor: pointer; + border: 0; + font-family: inherit; + background-color: transparent; +} + +a { + color: #4ed9b2; + text-decoration: none; +} + +mark { + background-color: transparent; +} + +input { + font-family: inherit; +} + +h4 { + font-size: 26px; + line-height: 32px; + font-weight: 600; +} +h3 { + font-size: 24px; + line-height: 32px; + font-weight: 500; +} +h2 { + font-size: 20px; + line-height: 30px; + font-weight: 400; +} +h1 { + font-size: 16px; + line-height: 20px; + font-weight: 300; +} + +* { + box-sizing: border-box; +} diff --git a/examples/with-tigris/tsconfig.json b/examples/with-tigris/tsconfig.json new file mode 100644 index 000000000000000..7b7e1fbbfe22422 --- /dev/null +++ b/examples/with-tigris/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}