diff --git a/.gitignore b/.gitignore
index 01355d6..6c9c37e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,3 @@ node_modules
package-lock.json
ontologies
-
-next-movie-database
-react-movie-database
\ No newline at end of file
diff --git a/examples/next-movie-database/.eslintrc.json b/examples/next-movie-database/.eslintrc.json
new file mode 100644
index 0000000..913cdba
--- /dev/null
+++ b/examples/next-movie-database/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["next", "next/core-web-vitals", "prettier"]
+}
diff --git a/examples/next-movie-database/.gitignore b/examples/next-movie-database/.gitignore
new file mode 100644
index 0000000..fd3dbb5
--- /dev/null
+++ b/examples/next-movie-database/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/examples/next-movie-database/.vscode/settings.json b/examples/next-movie-database/.vscode/settings.json
new file mode 100644
index 0000000..100de18
--- /dev/null
+++ b/examples/next-movie-database/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.tabSize": 2
+}
diff --git a/examples/next-movie-database/README.md b/examples/next-movie-database/README.md
new file mode 100644
index 0000000..a1a93a3
--- /dev/null
+++ b/examples/next-movie-database/README.md
@@ -0,0 +1,20 @@
+# Linked Data Movie Database
+
+This is a IMDb-like movie database based on Linked Data and DBpedia.
+
+Build with [LDkit](https://ldkit.io), React, TailwindCSS and Next.js.
+
+## Getting Started
+
+1. Install dependencies
+```bash
+npm install
+```
+
+2. Run the development server:
+
+```bash
+npm run dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
diff --git a/examples/next-movie-database/app/favicon.ico b/examples/next-movie-database/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next-movie-database/app/favicon.ico differ
diff --git a/examples/next-movie-database/app/globals.css b/examples/next-movie-database/app/globals.css
new file mode 100644
index 0000000..1a39b4a
--- /dev/null
+++ b/examples/next-movie-database/app/globals.css
@@ -0,0 +1,21 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 255, 255, 255;
+ --background-end-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+ }
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+}
diff --git a/examples/next-movie-database/app/layout.tsx b/examples/next-movie-database/app/layout.tsx
new file mode 100644
index 0000000..5e64926
--- /dev/null
+++ b/examples/next-movie-database/app/layout.tsx
@@ -0,0 +1,27 @@
+import type { Metadata } from "next";
+import "./globals.css";
+import { Header } from "@/components/Header";
+import { Footer } from "@/components/Footer";
+
+export const metadata: Metadata = {
+ title: "Linked Data Movie Database",
+ description: "Browse and search movies and actors from Wikidata",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/examples/next-movie-database/app/movie/page.tsx b/examples/next-movie-database/app/movie/page.tsx
new file mode 100644
index 0000000..a9727cd
--- /dev/null
+++ b/examples/next-movie-database/app/movie/page.tsx
@@ -0,0 +1,108 @@
+import { Suspense } from "react";
+
+import {
+ Movies,
+ MoviesActors,
+ MoviesDirectors,
+ MoviesWriters,
+ MoviesComposers,
+} from "@/data/lens";
+import { Thumbnail } from "@/components/Thumbnail";
+import { Links } from "@/components/Links";
+import { Loading } from "@/components/Loading";
+
+export default async function MoviePage({
+ searchParams,
+}: {
+ searchParams: Record;
+}) {
+ const { iri } = searchParams;
+
+ if (!iri || iri.length < 1) {
+ return No IRI found!
;
+ }
+
+ const movie = await Movies.findOne({
+ $id: iri,
+ name: {
+ $langMatches: "en",
+ },
+ abstract: {
+ $langMatches: "en",
+ },
+ });
+
+ if (movie == null) {
+ return No movie found!
;
+ }
+
+ return (
+
+
+ {movie.name}
+ {movie.abstract}
+ Director
+
+ }>
+
+
+ Cast
+ }>
+
+
+ Writer
+ }>
+
+
+ Composer
+ }>
+
+
+
+
+
+ );
+}
+
+async function Director({ iri }: { iri: string }) {
+ const movieWithDirectors = await MoviesDirectors.findByIri(iri);
+ return {movieWithDirectors?.directors};
+}
+
+async function Writer({ iri }: { iri: string }) {
+ const movieWithWriters = await MoviesWriters.findByIri(iri);
+ return {movieWithWriters?.writers};
+}
+
+async function Composer({ iri }: { iri: string }) {
+ const movieWithComposers = await MoviesComposers.findByIri(iri);
+ return {movieWithComposers?.composers};
+}
+
+async function Cast({ iri }: { iri: string }) {
+ const movieWithActors = await MoviesActors.findByIri(iri);
+ return {movieWithActors?.starring};
+}
+
+function PersonList({
+ children,
+}: {
+ children: { $id: string; name: string }[] | undefined;
+}) {
+ if (children === undefined || children.length < 1) {
+ return No records found.
;
+ }
+
+ return (
+
+ );
+}
diff --git a/examples/next-movie-database/app/page.tsx b/examples/next-movie-database/app/page.tsx
new file mode 100644
index 0000000..44bec3f
--- /dev/null
+++ b/examples/next-movie-database/app/page.tsx
@@ -0,0 +1,127 @@
+import { Loading } from "@/components/Loading";
+import { Search, SearchInterface } from "@/data/lens";
+import { yago } from "@/data/namespaces";
+import { schema } from "ldkit/namespaces";
+import { Suspense } from "react";
+
+export default function Home() {
+ return (
+ <>
+
+ Use the search bar above to look for people or movies, or pick some
+ selection below.
+
+
+
+
+
+ Top Directors
+ }>
+
+
+
+
+ >
+ );
+}
+
+async function TopMovies() {
+ const movies = await search(
+ [
+ "http://dbpedia.org/resource/The_Shawshank_Redemption",
+ "http://dbpedia.org/resource/The_Godfather",
+ "http://dbpedia.org/resource/The_Dark_Knight",
+ "http://dbpedia.org/resource/Pulp_Fiction",
+ "http://dbpedia.org/resource/Schindler's_List",
+ "http://dbpedia.org/resource/The_Matrix",
+ "http://dbpedia.org/resource/Fight_Club",
+ "http://dbpedia.org/resource/Goodfellas",
+ "http://dbpedia.org/resource/Terminator_2:_Judgment_Day",
+ "http://dbpedia.org/resource/Inception",
+ ],
+ schema.Movie
+ );
+ return ;
+}
+
+async function TopActors() {
+ const movies = await search(
+ [
+ "http://dbpedia.org/resource/Christian_Bale",
+ "http://dbpedia.org/resource/Helena_Bonham_Carter",
+ "http://dbpedia.org/resource/Robert_De_Niro",
+ "http://dbpedia.org/resource/Al_Pacino",
+ "http://dbpedia.org/resource/Tom_Hanks",
+ "http://dbpedia.org/resource/Leonardo_DiCaprio",
+ "http://dbpedia.org/resource/Meryl_Streep",
+ "http://dbpedia.org/resource/Brad_Pitt",
+ "http://dbpedia.org/resource/Tom_Cruise",
+ "http://dbpedia.org/resource/Sigourney_Weaver",
+ ],
+ yago.Actor109765278
+ );
+ return ;
+}
+
+async function TopDirectors() {
+ const movies = await search(
+ [
+ "http://dbpedia.org/resource/Stanley_Kubrick",
+ "http://dbpedia.org/resource/Quentin_Tarantino",
+ "http://dbpedia.org/resource/Christopher_Nolan",
+ "http://dbpedia.org/resource/Ridley_Scott",
+ "http://dbpedia.org/resource/Steven_Spielberg",
+ "http://dbpedia.org/resource/Martin_Scorsese",
+ "http://dbpedia.org/resource/Alfred_Hitchcock",
+ "http://dbpedia.org/resource/David_Fincher",
+ "http://dbpedia.org/resource/Tim_Burton",
+ "http://dbpedia.org/resource/Guillermo_del_Toro",
+ ],
+ yago.Director110014939
+ );
+ return ;
+}
+
+function SearchResults({ items }: { items: SearchInterface[] }) {
+ return (
+
+ {items.map((x) => (
+
+ ))}
+
+ );
+}
+
+function SearchResult(props: SearchInterface) {
+ const mode = props.types.includes(schema.Movie) ? "movie" : "person";
+
+ return (
+
+ {props.name}
+
+ );
+}
+
+async function search(ids: string[], type: string) {
+ return Search.find({
+ where: {
+ $id: ids,
+ name: {
+ $langMatches: "en",
+ },
+ types: {
+ $in: [type],
+ },
+ },
+ });
+}
diff --git a/examples/next-movie-database/app/person/page.tsx b/examples/next-movie-database/app/person/page.tsx
new file mode 100644
index 0000000..5c682a4
--- /dev/null
+++ b/examples/next-movie-database/app/person/page.tsx
@@ -0,0 +1,102 @@
+import { Suspense } from "react";
+
+import {
+ Persons,
+ ActorMovies,
+ DirectorMovies,
+ WriterMovies,
+ ComposerMovies,
+} from "@/data/lens";
+import { Thumbnail } from "@/components/Thumbnail";
+import { Loading } from "@/components/Loading";
+import { TypesList, TypesView } from "@/components/TypesView";
+import { yago } from "@/data/namespaces";
+import { Links } from "@/components/Links";
+
+export default async function PersonPage({
+ searchParams,
+}: {
+ searchParams: Record;
+}) {
+ const { iri } = searchParams;
+
+ if (!iri || iri.length < 1) {
+ return No IRI found!
;
+ }
+
+ const person = await Persons.findByIri(iri);
+
+ if (person == null) {
+ return No person found!
;
+ }
+
+ return (
+
+
+ {person.name}
+ }>
+
+
+ {person.abstract}
+ Filmography
+ }>
+
+
+
+
+
+ );
+}
+
+async function getFilmography(iri: string) {
+ const records = await Promise.all([
+ ActorMovies.findByIri(iri).then((x) => x?.isStarringIn),
+ DirectorMovies.findByIri(iri).then((x) => x?.isDirecting),
+ WriterMovies.findByIri(iri).then((x) => x?.isWriting),
+ ComposerMovies.findByIri(iri).then((x) => x?.isComposing),
+ ]);
+
+ const filmography: Record<
+ string,
+ { $id: string; name: string; types: string[] }
+ > = {};
+
+ const addMovie = (movie: { $id: string; name: string }, type: string) => {
+ if (!filmography[movie.$id]) {
+ filmography[movie.$id] = { ...movie, types: [type] };
+ } else {
+ filmography[movie.$id].types.push(type);
+ }
+ };
+
+ records[0]?.forEach((x) => addMovie(x, yago.Actor109765278));
+ records[1]?.forEach((x) => addMovie(x, yago.Director110014939));
+ records[2]?.forEach((x) => addMovie(x, yago.Writer110794014));
+ records[3]?.forEach((x) => addMovie(x, yago.Composer109947232));
+
+ return filmography;
+}
+
+async function Filmography({ iri }: { iri: string }) {
+ const filmography = await getFilmography(iri);
+
+ if (Object.values(filmography).length < 1) {
+ return No records found!
;
+ }
+
+ return (
+
+ {Object.values(filmography).map((movie) => (
+ -
+ {movie.name}
+
+
+ ))}
+
+ );
+}
diff --git a/examples/next-movie-database/app/search/page.tsx b/examples/next-movie-database/app/search/page.tsx
new file mode 100644
index 0000000..83fab55
--- /dev/null
+++ b/examples/next-movie-database/app/search/page.tsx
@@ -0,0 +1,83 @@
+import { Suspense } from "react";
+
+import { sparql as $ } from "ldkit/sparql";
+import { DataFactory } from "ldkit/rdf";
+import { schema } from "ldkit/namespaces";
+
+import { Search, type SearchInterface } from "@/data/lens";
+import { yago } from "@/data/namespaces";
+import { Loading } from "@/components/Loading";
+import { TypesView } from "@/components/TypesView";
+
+const df = new DataFactory();
+
+const typeIRIs = [
+ yago.Actor109765278,
+ yago.Director110014939,
+ yago.Writer110794014,
+ yago.Composer109947232,
+ schema.Movie,
+];
+
+export default async function SearchPage({
+ searchParams,
+}: {
+ searchParams: Record;
+}) {
+ const { query } = searchParams;
+
+ if (!query || query.length < 1) {
+ return Search for something!
;
+ }
+
+ return (
+
+
Searching for "{query}"
+ }>
+
+
+
+ );
+}
+
+async function search(query: string) {
+ return Search.find({
+ where: {
+ name: {
+ $langMatches: "en",
+ $filter: $`bif:contains(?value, '${df.literal(query.toLowerCase())}')`,
+ },
+ types: {
+ $in: typeIRIs,
+ },
+ },
+ take: 100,
+ });
+}
+
+async function SearchResults({ query }: { query: string }) {
+ const data = await search(query);
+ const count = data.length < 100 ? data.length : "100+";
+ return (
+
+
Found {count} results
+
+ {data.map((x) => (
+
+ ))}
+
+
+ );
+}
+
+function SearchResult(props: SearchInterface) {
+ const mode = props.types.includes(schema.Movie) ? "movie" : "person";
+
+ return (
+
+ {props.name}
+
+
+
+ );
+}
diff --git a/examples/next-movie-database/components/Footer.tsx b/examples/next-movie-database/components/Footer.tsx
new file mode 100644
index 0000000..b7df479
--- /dev/null
+++ b/examples/next-movie-database/components/Footer.tsx
@@ -0,0 +1,16 @@
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/examples/next-movie-database/components/Header.tsx b/examples/next-movie-database/components/Header.tsx
new file mode 100644
index 0000000..5143181
--- /dev/null
+++ b/examples/next-movie-database/components/Header.tsx
@@ -0,0 +1,52 @@
+import { MagnifyingGlassIcon } from "@heroicons/react/16/solid";
+
+export function Header() {
+ return (
+
+ );
+}
+
+export function IconGitHub() {
+ return (
+
+ );
+}
diff --git a/examples/next-movie-database/components/Links.tsx b/examples/next-movie-database/components/Links.tsx
new file mode 100644
index 0000000..c5cb8bc
--- /dev/null
+++ b/examples/next-movie-database/components/Links.tsx
@@ -0,0 +1,25 @@
+import { ArrowTopRightOnSquareIcon } from "@heroicons/react/16/solid";
+
+export function Links({ iri }: { iri: string }) {
+ return (
+ <>
+
+
+ DBpedia
+
+
+
+
+ Wikipedia
+
+
+ >
+ );
+}
+
+function getWikipediaLink(iri: string) {
+ return iri.replace(
+ "http://dbpedia.org/resource/",
+ "https://wikipedia.org/en/"
+ );
+}
diff --git a/examples/next-movie-database/components/Loading.tsx b/examples/next-movie-database/components/Loading.tsx
new file mode 100644
index 0000000..03c17ad
--- /dev/null
+++ b/examples/next-movie-database/components/Loading.tsx
@@ -0,0 +1,3 @@
+export function Loading() {
+ return Loading...
;
+}
diff --git a/examples/next-movie-database/components/Thumbnail.tsx b/examples/next-movie-database/components/Thumbnail.tsx
new file mode 100644
index 0000000..9a48ef3
--- /dev/null
+++ b/examples/next-movie-database/components/Thumbnail.tsx
@@ -0,0 +1,10 @@
+export function Thumbnail({ imageUrl }: { imageUrl: string | null }) {
+ if (!imageUrl) {
+ return No image found
;
+ }
+ return (
+
+
+
+ );
+}
diff --git a/examples/next-movie-database/components/TypesView.tsx b/examples/next-movie-database/components/TypesView.tsx
new file mode 100644
index 0000000..baa827c
--- /dev/null
+++ b/examples/next-movie-database/components/TypesView.tsx
@@ -0,0 +1,41 @@
+import { Types } from "@/data/lens";
+import { yago } from "@/data/namespaces";
+import { schema } from "ldkit/namespaces";
+
+async function getTypes(iri: string) {
+ const entity = await Types.findByIri(iri);
+ return entity?.types ?? [];
+}
+
+export async function TypesList({ iri }: { iri: string }) {
+ const types = await getTypes(iri);
+ return ;
+}
+
+export function TypesView({ types }: { types: string[] }) {
+ return (
+
+ {types
+ .map(typeToLabel)
+ .filter((label) => label != undefined)
+ .join(" ยท ")}
+
+ );
+}
+
+function typeToLabel(type: string) {
+ switch (type) {
+ case yago.Actor109765278:
+ return "Actor";
+ case yago.Director110014939:
+ return "Director";
+ case yago.Writer110794014:
+ return "Writer";
+ case yago.Composer109947232:
+ return "Composer";
+ case schema.Movie:
+ return "Movie";
+ default:
+ return undefined;
+ }
+}
diff --git a/examples/next-movie-database/data/lens.ts b/examples/next-movie-database/data/lens.ts
new file mode 100644
index 0000000..aa7a20d
--- /dev/null
+++ b/examples/next-movie-database/data/lens.ts
@@ -0,0 +1,36 @@
+import { createLens, type SchemaInterface } from "ldkit";
+
+import { options } from "./options";
+import {
+ SearchSchema,
+ PersonSchema,
+ MovieSchema,
+ MovieActorsSchema,
+ MovieDirectorsSchema,
+ MovieWritersSchema,
+ MovieComposersSchema,
+ ActorMoviesSchema,
+ TypesSchema,
+ DirectorMoviesSchema,
+ WriterMoviesSchema,
+ ComposerMoviesSchema,
+} from "./schemas";
+
+export const Types = createLens(TypesSchema, options);
+
+export const Persons = createLens(PersonSchema, options);
+
+export const ActorMovies = createLens(ActorMoviesSchema, options);
+export const DirectorMovies = createLens(DirectorMoviesSchema, options);
+export const WriterMovies = createLens(WriterMoviesSchema, options);
+export const ComposerMovies = createLens(ComposerMoviesSchema, options);
+
+export const Movies = createLens(MovieSchema, options);
+
+export const MoviesActors = createLens(MovieActorsSchema, options);
+export const MoviesDirectors = createLens(MovieDirectorsSchema, options);
+export const MoviesWriters = createLens(MovieWritersSchema, options);
+export const MoviesComposers = createLens(MovieComposersSchema, options);
+
+export const Search = createLens(SearchSchema, options);
+export type SearchInterface = SchemaInterface;
diff --git a/examples/next-movie-database/data/namespaces.ts b/examples/next-movie-database/data/namespaces.ts
new file mode 100644
index 0000000..7f10784
--- /dev/null
+++ b/examples/next-movie-database/data/namespaces.ts
@@ -0,0 +1,12 @@
+import { createNamespace } from "ldkit";
+
+export const yago = createNamespace({
+ iri: "http://dbpedia.org/class/yago/",
+ prefix: "yago:",
+ terms: [
+ "Actor109765278",
+ "Composer109947232",
+ "Director110014939",
+ "Writer110794014",
+ ],
+} as const);
diff --git a/examples/next-movie-database/data/options.ts b/examples/next-movie-database/data/options.ts
new file mode 100644
index 0000000..8ed7b81
--- /dev/null
+++ b/examples/next-movie-database/data/options.ts
@@ -0,0 +1,7 @@
+import { type Options } from "ldkit";
+
+export const options: Options = {
+ source: "https://dbpedia.org/sparql",
+ language: "en",
+ logQuery: (query) => console.log("SPARQL QUERY", query),
+};
diff --git a/examples/next-movie-database/data/schemas.ts b/examples/next-movie-database/data/schemas.ts
new file mode 100644
index 0000000..e33d22b
--- /dev/null
+++ b/examples/next-movie-database/data/schemas.ts
@@ -0,0 +1,138 @@
+import { type Schema } from "ldkit";
+import { rdfs, rdf, dbo, ldkit } from "ldkit/namespaces";
+
+export const TypesSchema = {
+ types: {
+ "@id": rdf.type,
+ "@type": ldkit.IRI,
+ "@array": true,
+ },
+} satisfies Schema;
+
+export const LabelSchema = {
+ name: rdfs.label,
+} satisfies Schema;
+
+export const SearchSchema = {
+ ...LabelSchema,
+ ...TypesSchema,
+} satisfies Schema;
+
+export const PersonSchema = {
+ name: rdfs.label,
+ abstract: dbo.abstract,
+ birthDate: {
+ "@id": dbo.birthDate,
+ "@type": "xsd:date",
+ "@optional": true,
+ },
+ deathDate: {
+ "@id": dbo.deathDate,
+ "@type": "xsd:date",
+ "@optional": true,
+ },
+ thumbnail: {
+ "@id": dbo.thumbnail,
+ "@optional": true,
+ },
+} satisfies Schema;
+
+export const ActorMoviesSchema = {
+ isStarringIn: {
+ "@id": dbo.starring,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ "@inverse": true,
+ },
+} satisfies Schema;
+
+export const DirectorMoviesSchema = {
+ isDirecting: {
+ "@id": dbo.director,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ "@inverse": true,
+ },
+} satisfies Schema;
+
+export const WriterMoviesSchema = {
+ isWriting: {
+ "@id": dbo.writer,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ "@inverse": true,
+ },
+} satisfies Schema;
+
+export const ComposerMoviesSchema = {
+ isComposing: {
+ "@id": dbo.musicComposer,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ "@inverse": true,
+ },
+} satisfies Schema;
+
+export const MovieSchema = {
+ name: rdfs.label,
+ abstract: dbo.abstract,
+ director: {
+ "@id": dbo.director,
+ "@schema": LabelSchema,
+ "@optional": true,
+ },
+ writer: {
+ "@id": dbo.writer,
+ "@schema": LabelSchema,
+ "@optional": true,
+ },
+ musicComposer: {
+ "@id": dbo.musicComposer,
+ "@schema": LabelSchema,
+ "@optional": true,
+ },
+ thumbnail: {
+ "@id": dbo.thumbnail,
+ "@optional": true,
+ },
+} satisfies Schema;
+
+export const MovieActorsSchema = {
+ starring: {
+ "@id": dbo.starring,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ },
+} satisfies Schema;
+
+export const MovieDirectorsSchema = {
+ directors: {
+ "@id": dbo.director,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ },
+} satisfies Schema;
+
+export const MovieWritersSchema = {
+ writers: {
+ "@id": dbo.writer,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ },
+} satisfies Schema;
+
+export const MovieComposersSchema = {
+ composers: {
+ "@id": dbo.musicComposer,
+ "@schema": LabelSchema,
+ "@array": true,
+ "@optional": true,
+ },
+} satisfies Schema;
diff --git a/examples/next-movie-database/data/utils.ts b/examples/next-movie-database/data/utils.ts
new file mode 100644
index 0000000..e69de29
diff --git a/examples/next-movie-database/next.config.js b/examples/next-movie-database/next.config.js
new file mode 100644
index 0000000..767719f
--- /dev/null
+++ b/examples/next-movie-database/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/examples/next-movie-database/package.json b/examples/next-movie-database/package.json
new file mode 100644
index 0000000..b7ce809
--- /dev/null
+++ b/examples/next-movie-database/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "next-movie-database",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@heroicons/react": "^2.1.1",
+ "ldkit": "^2",
+ "next": "14.0.4",
+ "react": "^18",
+ "react-dom": "^18"
+ },
+ "devDependencies": {
+ "@tailwindcss/typography": "^0.5.10",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10.0.1",
+ "eslint": "^8",
+ "eslint-config-next": "14.0.4",
+ "eslint-config-prettier": "^9.1.0",
+ "postcss": "^8",
+ "prettier": "^3.1.1",
+ "tailwindcss": "^3.3.0",
+ "typescript": "^5"
+ }
+}
diff --git a/examples/next-movie-database/postcss.config.js b/examples/next-movie-database/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/examples/next-movie-database/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/next-movie-database/public/next.svg b/examples/next-movie-database/public/next.svg
new file mode 100644
index 0000000..5174b28
--- /dev/null
+++ b/examples/next-movie-database/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/next-movie-database/public/vercel.svg b/examples/next-movie-database/public/vercel.svg
new file mode 100644
index 0000000..d2f8422
--- /dev/null
+++ b/examples/next-movie-database/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/next-movie-database/tailwind.config.ts b/examples/next-movie-database/tailwind.config.ts
new file mode 100644
index 0000000..5227bdf
--- /dev/null
+++ b/examples/next-movie-database/tailwind.config.ts
@@ -0,0 +1,12 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {},
+ plugins: [require("@tailwindcss/typography")],
+};
+export default config;
diff --git a/examples/next-movie-database/tsconfig.json b/examples/next-movie-database/tsconfig.json
new file mode 100644
index 0000000..c714696
--- /dev/null
+++ b/examples/next-movie-database/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}