├── .gitignore ├── README.md ├── api ├── trpc │ └── [trpc].ts └── tsconfig.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── migrations │ ├── 20211125002749_add_voting_model │ │ └── migration.sql │ ├── 20211125012254_add_pokemon_reference │ │ └── migration.sql │ ├── 20211221040220_add_indexes │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── android-chrome-192x192.png ├── android-chrome-384x384.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-150x150.png ├── rings.svg ├── site.webmanifest └── spheal.png ├── src ├── App.tsx ├── assets │ └── favicon.ico ├── index.css ├── index.tsx ├── lib │ └── trpc.ts ├── logo.svg └── pages │ ├── about.tsx │ ├── results.tsx │ └── vote.tsx ├── tailwind.config.js ├── tsconfig.json ├── vercel-dev.json ├── vercel.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vercel 4 | .env 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Solid-tRPC Demo 2 | 3 | Wanted to see if I could get [tRPC](https://trpc.io) working with Solid.js and Vercel serverless functions. 4 | 5 | Turns out I could. Was a tad annoying. [Thankful I ~~found~~ wrote this](https://t3.gg/blog/posts/vite-vercel) 6 | -------------------------------------------------------------------------------- /api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import * as trpc from "@trpc/server"; 2 | import * as trpcNext from "@trpc/server/adapters/next"; 3 | import { z } from "zod"; 4 | import { PrismaClient } from "@prisma/client"; 5 | import { inferAsyncReturnType } from "@trpc/server"; 6 | 7 | const prisma = new PrismaClient(); 8 | 9 | const MAX_DEX_ID = 493; 10 | 11 | export const getRandomPokemon: (notThisOne?: number) => number = ( 12 | notThisOne 13 | ) => { 14 | const pokedexNumber = Math.floor(Math.random() * MAX_DEX_ID) + 1; 15 | 16 | if (pokedexNumber !== notThisOne) return pokedexNumber; 17 | return getRandomPokemon(notThisOne); 18 | }; 19 | 20 | export const getOptionsForVote = () => { 21 | const firstId = getRandomPokemon(); 22 | const secondId = getRandomPokemon(firstId); 23 | 24 | return [firstId, secondId]; 25 | }; 26 | 27 | type RouterContext = inferAsyncReturnType; 28 | 29 | const cachedRouter = trpc.router().query("public-results", { 30 | async resolve({ ctx }) { 31 | return await prisma.pokemon.findMany({ 32 | orderBy: { 33 | VoteFor: { _count: "desc" }, 34 | }, 35 | select: { 36 | id: true, 37 | name: true, 38 | spriteUrl: true, 39 | _count: { 40 | select: { 41 | VoteFor: true, 42 | VoteAgainst: true, 43 | }, 44 | }, 45 | }, 46 | }); 47 | }, 48 | }); 49 | 50 | const mainRouter = trpc 51 | .router() 52 | .query("get-pokemon-pair", { 53 | async resolve() { 54 | const [first, second] = getOptionsForVote(); 55 | 56 | const bothPokemon = await prisma.pokemon.findMany({ 57 | where: { id: { in: [first, second] } }, 58 | }); 59 | 60 | if (bothPokemon.length !== 2) 61 | throw new Error("Failed to find two pokemon"); 62 | 63 | return { firstPokemon: bothPokemon[0], secondPokemon: bothPokemon[1] }; 64 | }, 65 | }) 66 | .mutation("cast-vote", { 67 | input: z.object({ 68 | votedFor: z.number(), 69 | votedAgainst: z.number(), 70 | }), 71 | async resolve({ input }) { 72 | const voteInDb = await prisma.vote.create({ 73 | data: { 74 | votedAgainstId: input.votedAgainst, 75 | votedForId: input.votedFor, 76 | }, 77 | }); 78 | return { success: true, vote: voteInDb }; 79 | }, 80 | }); 81 | 82 | const appRouter = trpc 83 | .router() 84 | .merge(cachedRouter) 85 | .merge(mainRouter); 86 | 87 | // export type definition of API 88 | export type AppRouter = typeof appRouter; 89 | 90 | const createContext = (opts: trpcNext.CreateNextContextOptions) => opts; 91 | 92 | // export API handler 93 | export default trpcNext.createNextApiHandler({ 94 | router: appRouter, 95 | createContext: createContext as any, 96 | responseMeta({ ctx, paths, type, errors }) { 97 | // assuming you have all your public routes with the kewyord `public` in them 98 | const allPublic = paths && paths.every((path) => path.includes("public")); 99 | // checking that no procedures errored 100 | const allOk = errors.length === 0; 101 | // checking we're doing a query request 102 | const isQuery = type === "query"; 103 | 104 | if (ctx?.res && allPublic && allOk && isQuery) { 105 | // cache request for 1 day 106 | const DAY_IN_SECONDS = 60 * 60 * 24; 107 | return { 108 | headers: { 109 | "cache-control": `s-maxage=${DAY_IN_SECONDS}, stale-while-revalidate=${DAY_IN_SECONDS}`, 110 | }, 111 | }; 112 | } 113 | return {}; 114 | }, 115 | }); 116 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "types": ["vite/client"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Solid + tRPC Demo 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-template-solid", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "vite", 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview", 10 | "postinstall": "prisma generate", 11 | "vercel-dev-helper": "vite --port $PORT", 12 | "vdev": "vercel dev --local-config ./vercel-dev.json" 13 | }, 14 | "license": "MIT", 15 | "devDependencies": { 16 | "autoprefixer": "^10.4.0", 17 | "postcss": "^8.4.5", 18 | "prisma": "^3.7.0", 19 | "tailwindcss": "^3.0.7", 20 | "typescript": "^4.5.3", 21 | "vite": "^2.7.1", 22 | "vite-plugin-solid": "^2.1.4" 23 | }, 24 | "dependencies": { 25 | "@prisma/client": "^3.7.0", 26 | "@trpc/client": "^9.16.0", 27 | "@trpc/next": "^9.16.0", 28 | "@trpc/server": "^9.16.0", 29 | "@vercel/node": "^1.12.1", 30 | "solid-app-router": "^0.1.14", 31 | "solid-js": "^1.2.6", 32 | "zod": "^3.11.6" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/20211125002749_add_voting_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Vote` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 5 | `votedFor` INTEGER NOT NULL, 6 | `votedAgainst` INTEGER NOT NULL, 7 | 8 | PRIMARY KEY (`id`) 9 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20211125012254_add_pokemon_reference/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `votedAgainst` on the `Vote` table. All the data in the column will be lost. 5 | - You are about to drop the column `votedFor` on the `Vote` table. All the data in the column will be lost. 6 | - Added the required column `votedAgainstId` to the `Vote` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `votedForId` to the `Vote` table without a default value. This is not possible if the table is not empty. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE `Vote` DROP COLUMN `votedAgainst`, 12 | DROP COLUMN `votedFor`, 13 | ADD COLUMN `votedAgainstId` INTEGER NOT NULL, 14 | ADD COLUMN `votedForId` INTEGER NOT NULL; 15 | 16 | -- CreateTable 17 | CREATE TABLE `Pokemon` ( 18 | `id` INTEGER NOT NULL, 19 | `name` VARCHAR(191) NOT NULL, 20 | `spriteUrl` VARCHAR(191) NOT NULL, 21 | 22 | PRIMARY KEY (`id`) 23 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20211221040220_add_indexes/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX `Vote_votedForId_idx` ON `Vote`(`votedForId`); 3 | 4 | -- CreateIndex 5 | CREATE INDEX `Vote_votedAgainstId_idx` ON `Vote`(`votedAgainstId`); 6 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | previewFeatures = ["referentialIntegrity"] 7 | } 8 | 9 | datasource db { 10 | provider = "mysql" 11 | url = env("DATABASE_URL") 12 | shadowDatabaseUrl = env("SHADOW_URL") 13 | referentialIntegrity = "prisma" 14 | } 15 | 16 | model Vote { 17 | id String @id @default(cuid()) 18 | createdAt DateTime @default(now()) 19 | 20 | votedFor Pokemon @relation(name: "votesFor", fields: [votedForId], references: [id]) 21 | votedForId Int 22 | votedAgainst Pokemon @relation(name: "votesAgainst", fields: [votedAgainstId], references: [id]) 23 | votedAgainstId Int 24 | 25 | @@index([votedForId]) 26 | @@index([votedAgainstId]) 27 | } 28 | 29 | model Pokemon { 30 | id Int @id 31 | 32 | name String 33 | spriteUrl String 34 | VoteFor Vote[] @relation("votesFor") 35 | VoteAgainst Vote[] @relation("votesAgainst") 36 | } 37 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/android-chrome-384x384.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/favicon.ico -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/rings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | 18 | 19 | 20 | 25 | 29 | 33 | 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/spheal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/public/spheal.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | 3 | import { lazy } from "solid-js"; 4 | import { Routes, Route, Link } from "solid-app-router"; 5 | 6 | const VotePage = lazy(() => import("./pages/vote")); 7 | const ResultsPage = lazy(() => import("./pages/results")); 8 | const AboutPage = lazy(() => import("./pages/about")); 9 | 10 | const App: Component = () => { 11 | return ( 12 |
13 | 14 | } /> 15 | } /> 16 | } /> 17 | {/* } /> */} 18 | 19 |
20 | Twitter 21 | {"-"} 22 | Results 23 | {"-"} 24 | About 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/democratic-geodude/88a86992300eb0baaa26438ecfffd2058aafba85/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* ./src/index.css */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | body { 7 | margin: 0; 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 9 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 10 | sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | body { 16 | @apply text-gray-100 bg-gray-800; 17 | } 18 | 19 | code { 20 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 21 | monospace; 22 | } 23 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web"; 2 | 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { Router } from "solid-app-router"; 6 | 7 | render( 8 | () => ( 9 | 10 | 11 | 12 | ), 13 | document.getElementById("root") as HTMLElement 14 | ); 15 | -------------------------------------------------------------------------------- /src/lib/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCClient } from "@trpc/client"; 2 | import { createResource } from "solid-js"; 3 | 4 | import type { inferHandlerInput, inferProcedureOutput } from "@trpc/server"; 5 | import type { AppRouter } from "../../api/trpc/[trpc]"; 6 | 7 | export const trpcClient = createTRPCClient({ url: "/api/trpc" }); 8 | 9 | type AppQueries = AppRouter["_def"]["queries"]; 10 | type AppQueryKeys = keyof AppQueries & string; 11 | 12 | export const createTrpcQuery = ( 13 | path: TPath, 14 | ...args: inferHandlerInput 15 | ) => { 16 | const fetchData = async () => { 17 | return trpcClient.query(path, ...(args as any)); 18 | }; 19 | 20 | return createResource(fetchData); 21 | }; 22 | 23 | type AppMutations = AppRouter["_def"]["mutations"]; 24 | type AppMutationKeys = keyof AppMutations & string; 25 | 26 | export const createTrpcMutation = ( 27 | path: TPath 28 | ) => { 29 | const fetchData = async (...args: inferHandlerInput) => { 30 | return trpcClient.mutation(path, ...(args as any)); 31 | }; 32 | 33 | return { mutate: fetchData }; 34 | }; 35 | 36 | export type inferQueryResponse< 37 | TRouteKey extends keyof AppRouter["_def"]["queries"] 38 | > = inferProcedureOutput; 39 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | const AboutPage = () => { 2 | return ( 3 |
4 |

About

5 |

6 | This is a remake of{" "} 7 | a dumb app I made in Next.js. This 8 | time it's in SolidJS! 9 |

10 |
11 | 28 |
29 | ); 30 | }; 31 | 32 | export default AboutPage; 33 | -------------------------------------------------------------------------------- /src/pages/results.tsx: -------------------------------------------------------------------------------- 1 | import { Component, For, Show } from "solid-js"; 2 | import { createTrpcQuery, inferQueryResponse } from "../lib/trpc"; 3 | 4 | type PokemonQueryResult = inferQueryResponse<"public-results">; 5 | const generateCountPercent = (pokemon: PokemonQueryResult[number]) => { 6 | const { VoteFor, VoteAgainst } = pokemon._count; 7 | if (VoteFor + VoteAgainst === 0) { 8 | return 0; 9 | } 10 | return (VoteFor / (VoteFor + VoteAgainst)) * 100; 11 | }; 12 | 13 | const PokemonListing: Component<{ pokemon: PokemonQueryResult[number] }> = ({ 14 | pokemon, 15 | }) => { 16 | return ( 17 |
18 |
19 | 20 |
{pokemon.name}
21 |
22 |
{generateCountPercent(pokemon).toFixed(2) + "%"}
23 |
24 | ); 25 | }; 26 | 27 | const ResultsPage: Component = () => { 28 | const [data] = createTrpcQuery("public-results"); 29 | return ( 30 |
31 |

Results

32 | } 35 | > 36 | {(pokemon) => ( 37 |
38 | { 40 | const difference = 41 | generateCountPercent(b) - generateCountPercent(a); 42 | 43 | if (difference === 0) { 44 | return b._count.VoteFor - a._count.VoteFor; 45 | } 46 | 47 | return difference; 48 | })} 49 | > 50 | {(currentMon) => } 51 | 52 |
53 | )} 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default ResultsPage; 60 | -------------------------------------------------------------------------------- /src/pages/vote.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Show } from "solid-js"; 2 | import { 3 | createTrpcMutation, 4 | createTrpcQuery, 5 | inferQueryResponse, 6 | } from "../lib/trpc"; 7 | 8 | const btn = 9 | "inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm font-medium rounded-full text-gray-700 bg-white hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"; 10 | 11 | type PokemonFromServer = inferQueryResponse<"get-pokemon-pair">["firstPokemon"]; 12 | 13 | const PokemonListing = (props: { 14 | pokemon: PokemonFromServer; 15 | vote: () => void; 16 | disabled: boolean; 17 | }) => { 18 | return ( 19 |
24 |
25 | {props.pokemon.name} 26 |
27 | 33 | 40 |
41 | ); 42 | }; 43 | 44 | const VotePage: Component = () => { 45 | const [data, { refetch }] = createTrpcQuery("get-pokemon-pair"); 46 | 47 | const { mutate } = createTrpcMutation("cast-vote"); 48 | 49 | const voteForRoundest = (selected: number, against: number) => { 50 | mutate({ votedFor: selected, votedAgainst: against }); 51 | refetch(); 52 | return null; 53 | }; 54 | 55 | const fetchingNext = false; 56 | 57 | return ( 58 | <> 59 |
Which Pokémon is Rounder?
60 | } 63 | > 64 | {(response) => ( 65 |
66 | 69 | voteForRoundest( 70 | response.firstPokemon.id, 71 | response.secondPokemon.id 72 | ) 73 | } 74 | disabled={fetchingNext} 75 | /> 76 |
{"or"}
77 | 80 | voteForRoundest( 81 | response.secondPokemon.id, 82 | response.firstPokemon.id 83 | ) 84 | } 85 | disabled={fetchingNext} 86 | /> 87 |
88 |
89 | )} 90 | 91 | 92 | ); 93 | }; 94 | 95 | export default VotePage; 96 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "jsx": "preserve", 10 | "jsxImportSource": "solid-js", 11 | "types": ["vite/client"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vercel-dev.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/((?!api/.*).*)", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import solidPlugin from "vite-plugin-solid"; 3 | 4 | export default defineConfig({ 5 | plugins: [solidPlugin()], 6 | build: { 7 | target: "esnext", 8 | polyfillDynamicImport: false, 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------