feat: scaffold monorepo

pull/302/head
Yangshun Tay 2 years ago
parent 27a82e8c0f
commit 523d91f920

13
.gitignore vendored

@ -0,0 +1,13 @@
.DS_Store
node_modules
.turbo
*.log
.next
dist
dist-ssr
*.local
.env
.cache
server/dist
public/dist
.turbo

@ -0,0 +1,27 @@
node_modules
.next
build
dist
*.tsbuildinfo
*.gitignore
*.svg
*.lock
*.npmignore
*.sql
*.png
*.jpg
*.jpeg
*.gif
*.ico
*.sh
Dockerfile
Dockerfile.*
.env
.env.*
LICENSE
*.log
.DS_Store
.dockerignore
*.patch
*.toml
*.prisma

@ -0,0 +1,19 @@
{
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"cSpell.autoFormatConfigFile": true,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"javascript.format.enable": true,
"json.format.enable": true,
"eslint.format.enable": false,
"css.format.enable": true,
"css.format.newlineBetweenRules": true,
"css.format.newlineBetweenSelectors": true,
"css.format.preserveNewLines": true,
"typescript.format.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}

@ -0,0 +1,10 @@
# Prisma
DATABASE_URL=postgresql://postgres:[PASSWORD]@localhost:5432/postgres
# Next Auth
NEXTAUTH_SECRET=any_string_you_wish
NEXTAUTH_URL=http://localhost:3000
# Next Auth GitHub Provider
GITHUB_CLIENT_ID=a5164b1943b5413ff2f5
GITHUB_CLIENT_SECRET=

@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ['tih', 'next/core-web-vitals'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
};

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# 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
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo

@ -0,0 +1,50 @@
# Create T3 App
This is an app bootstrapped according to the [init.tips](https://init.tips) stack, also known as the T3-Stack.
## Why are there `.js` files in here?
As per [T3-Axiom #3](https://github.com/t3-oss/create-t3-app/tree/next#3-typesafety-isnt-optional), we take typesafety as a first class citizen. Unfortunately, not all frameworks and plugins support TypeScript which means some of the configuration files have to be `.js` files.
We try to emphasize that these files are javascript for a reason, by explicitly declaring its type (`cjs` or `mjs`) depending on what's supported by the library it is used by. Also, all the `js` files in this project are still typechecked using a `@ts-check` comment at the top.
## What's next? How do I make an app with this?
We try to keep this project as simple as possible, so you can start with the most basic configuration and then move on to more advanced configuration.
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
- [Next-Auth.js](https://next-auth.js.org)
- [Prisma](https://prisma.io)
- [TailwindCSS](https://tailwindcss.com)
- [tRPC](https://trpc.io) (using @next version? [see v10 docs here](https://trpc.io/docs/v10/))
Also checkout these awesome tutorials on `create-t3-app`.
- [Build a Blog With the T3 Stack - tRPC, TypeScript, Next.js, Prisma & Zod](https://www.youtube.com/watch?v=syEWlxVFUrY)
- [Build a Live Chat Application with the T3 Stack - TypeScript, Tailwind, tRPC](https://www.youtube.com/watch?v=dXRRY37MPuk)
- [Build a full stack app with create-t3-app](https://www.nexxel.dev/blog/ct3a-guestbook)
- [A first look at create-t3-app](https://dev.to/ajcwebdev/a-first-look-at-create-t3-app-1i8f)
## How do I deploy this?
### Vercel
We recommend deploying to [Vercel](https://vercel.com/?utm_source=t3-oss&utm_campaign=oss). It makes it super easy to deploy NextJs apps.
- Push your code to a GitHub repository.
- Go to [Vercel](https://vercel.com/?utm_source=t3-oss&utm_campaign=oss) and sign up with GitHub.
- Create a Project and import the repository you pushed your code to.
- Add your environment variables.
- Click **Deploy**
- Now whenever you push a change to your repository, Vercel will automatically redeploy your website!
### Docker
You can also dockerize this stack and deploy a container. See the [Docker deployment page](https://create-t3-app-nu.vercel.app/en/deployment/docker) for details.
## Useful resources
Here are some resources that we commonly refer to:
- [Protecting routes with Next-Auth.js](https://next-auth.js.org/configuration/nextjs#unstable_getserversession)

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

@ -0,0 +1,18 @@
import { env } from './src/env/server.mjs';
/**
* Don't be scared of the generics here.
* All they do is to give us autocompletion when using this.
*
* @template {import('next').NextConfig} T
* @param {T} config - A generic parameter that flows through to the return type
* @constraint {{import('next').NextConfig}}
*/
function defineNextConfig(config) {
return config;
}
export default defineNextConfig({
reactStrictMode: true,
swcMinify: true,
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,44 @@
{
"name": "@tih/portal",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"tsc": "tsc",
"postinstall": "prisma generate",
"prisma": "prisma"
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.4",
"@prisma/client": "^4.4.0",
"@tih/ui": "*",
"@trpc/client": "^9.27.2",
"@trpc/next": "^9.27.2",
"@trpc/react": "^9.27.2",
"@trpc/server": "^9.27.2",
"next": "12.3.1",
"next-auth": "~4.10.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-query": "^3.39.2",
"superjson": "^1.10.0",
"zod": "^3.18.0"
},
"devDependencies": {
"@tih/tsconfig": "*",
"@types/node": "18.0.0",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.6",
"autoprefixer": "^10.4.12",
"postcss": "^8.4.16",
"prisma": "^4.4.0",
"tailwindcss": "^3.1.8",
"typescript": "4.8.3"
},
"ct3aMetadata": {
"initVersion": "5.13.1"
}
}

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

@ -0,0 +1,73 @@
-- CreateTable
CREATE TABLE "Example" (
"id" TEXT NOT NULL,
CONSTRAINT "Example_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

@ -0,0 +1,64 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
// NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.text annotations in model Account below
// Further reading:
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
url = env("DATABASE_URL")
}
model Example {
id String @id @default(cuid())
}
// Necessary for Next auth
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? //@db.Text
access_token String? //@db.Text
expires_at Int?
token_type String?
scope String?
id_token String? //@db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -0,0 +1,38 @@
// @ts-check
import { clientEnv, clientSchema } from './schema.mjs';
const _clientEnv = clientSchema.safeParse(clientEnv);
export const formatErrors = (
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
errors,
) =>
Object.entries(errors)
.map(([name, value]) => {
if (value && '_errors' in value)
return `${name}: ${value._errors.join(', ')}\n`;
})
.filter(Boolean);
if (_clientEnv.success === false) {
console.error(
'❌ Invalid environment variables:\n',
...formatErrors(_clientEnv.error.format()),
);
throw new Error('Invalid environment variables');
}
/**
* Validate that client-side environment variables are exposed to the client.
*/
for (let key of Object.keys(_clientEnv.data)) {
if (!key.startsWith('NEXT_PUBLIC_')) {
console.warn(
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`,
);
throw new Error('Invalid public environment variable name');
}
}
export const env = _clientEnv.data;

@ -0,0 +1,34 @@
// @ts-check
import { z } from 'zod';
/**
* Specify your server-side environment variables schema here.
* This way you can ensure the app isn't built with invalid env vars.
*/
export const serverSchema = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'test', 'production']),
NEXTAUTH_SECRET: z.string(),
NEXTAUTH_URL: z.string().url(),
GITHUB_CLIENT_ID: z.string(),
GITHUB_CLIENT_SECRET: z.string(),
});
/**
* Specify your client-side environment variables schema here.
* This way you can ensure the app isn't built with invalid env vars.
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
*/
export const clientSchema = z.object({
// NEXT_PUBLIC_BAR: z.string(),
});
/**
* You can't destruct `process.env` as a regular object, so you have to do
* it manually here. This is because Next.js evaluates this at build time,
* and only used environment variables are included in the build.
* @type {{ [k in keyof z.infer<typeof clientSchema>]: z.infer<typeof clientSchema>[k] | undefined }}
*/
export const clientEnv = {
// NEXT_PUBLIC_BAR: process.env.NEXT_PUBLIC_BAR,
};

@ -0,0 +1,30 @@
// @ts-check
/**
* This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars.
* It has to be a `.mjs`-file to be imported there.
*/
import { serverSchema } from './schema.mjs';
import { env as clientEnv, formatErrors } from './client.mjs';
const _serverEnv = serverSchema.safeParse(process.env);
if (_serverEnv.success === false) {
console.error(
'❌ Invalid environment variables:\n',
...formatErrors(_serverEnv.error.format()),
);
throw new Error('Invalid environment variables');
}
/**
* Validate that server-side environment variables are not exposed to the client.
*/
for (let key of Object.keys(_serverEnv.data)) {
if (key.startsWith('NEXT_PUBLIC_')) {
console.warn('❌ You are exposing a server-side env-variable:', key);
throw new Error('You are exposing a server-side env-variable');
}
}
export const env = { ..._serverEnv.data, ...clientEnv };

@ -0,0 +1,76 @@
import type { AppType } from 'next/app';
import type { Session } from 'next-auth';
import { SessionProvider } from 'next-auth/react';
import superjson from 'superjson';
import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
import { loggerLink } from '@trpc/client/links/loggerLink';
import { withTRPC } from '@trpc/next';
import type { AppRouter } from '~/server/router';
import '~/styles/globals.css';
const MyApp: AppType<{ session: Session | null }> = ({
Component,
pageProps: { session, ...pageProps },
}) => {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
};
const getBaseUrl = () => {
if (typeof window !== 'undefined') {
return '';
} // Browser should use relative url
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
} // SSR should use vercel url
return `http://localhost:${process.env.PORT ?? 3000}`; // Dev SSR should use localhost
};
export default withTRPC<AppRouter>({
config({ ctx: _ctx }) {
/**
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
*/
const url = `${getBaseUrl()}/api/trpc`;
return {
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error),
}),
httpBatchLink({ url }),
],
transformer: superjson,
url,
/**
* @link https://react-query.tanstack.com/reference/QueryClient
*/
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
// To use SSR properly you need to forward the client's headers to the server
// headers: () => {
// if (ctx?.req) {
// const headers = ctx?.req?.headers;
// delete headers?.connection;
// return {
// ...headers,
// "x-ssr": "1",
// };
// }
// return {};
// }
};
},
/**
* @link https://trpc.io/docs/ssr
*/
ssr: false,
})(MyApp);

@ -0,0 +1,31 @@
import NextAuth, { type NextAuthOptions } from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
// Prisma adapter for NextAuth, optional and can be removed
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { env } from '../../../env/server.mjs';
import { prisma } from '../../../server/db/client';
export const authOptions: NextAuthOptions = {
// Configure one or more authentication providers
adapter: PrismaAdapter(prisma),
// Include user.id on session
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
providers: [
GitHubProvider({
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
}),
// ...add more providers here
],
};
export default NextAuth(authOptions);

@ -0,0 +1,11 @@
// Src/pages/api/examples.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { prisma } from '../../server/db/client';
const examples = async (req: NextApiRequest, res: NextApiResponse) => {
const examplesFromDb = await prisma.example.findMany();
res.status(200).json(examplesFromDb);
};
export default examples;

@ -0,0 +1,23 @@
// Example of a restricted endpoint that only authenticated users can access from https://next-auth.js.org/getting-started/example
import type { NextApiRequest, NextApiResponse } from 'next';
import { getServerAuthSession } from '../../server/common/get-server-auth-session';
const restricted = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getServerAuthSession({ req, res });
if (session) {
res.send({
content:
'This is protected content. You can access this content because you are signed in.',
});
} else {
res.send({
error:
'You must be signed in to view the protected content on this page.',
});
}
};
export default restricted;

@ -0,0 +1,18 @@
// Src/pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { env } from '../../../env/server.mjs';
import { appRouter } from '../../../server/router';
import { createContext } from '../../../server/router/context';
// Export API handler
export default createNextApiHandler({
createContext,
onError:
env.NODE_ENV === 'development'
? ({ path, error }) => {
console.error(`❌ tRPC failed on ${path}: ${error}`);
}
: undefined,
router: appRouter,
});

@ -0,0 +1,91 @@
import type { NextPage } from 'next';
import Head from 'next/head';
import { CounterButton } from '@tih/ui';
import { trpc } from '~/utils/trpc';
const Home: NextPage = () => {
const hello = trpc.useQuery(['example.hello', { text: 'from tRPC!' }]);
const getAll = trpc.useQuery(['example.getAll']);
return (
<>
<Head>
<title>Create T3 App</title>
<meta content="Generated by create-t3-app" name="description" />
<link href="/favicon.ico" rel="icon" />
</Head>
<main className="container mx-auto flex flex-col items-center justify-center min-h-screen p-4">
<h1 className="text-5xl md:text-[5rem] leading-normal font-extrabold text-gray-700">
Create <span className="text-purple-300">T3</span> App
</h1>
<CounterButton />
<p className="text-2xl text-gray-700">This stack uses:</p>
<div className="grid gap-3 pt-3 mt-3 text-center md:grid-cols-2 lg:w-2/3">
<TechnologyCard
description="The React framework for production"
documentation="https://nextjs.org/"
name="NextJS"
/>
<TechnologyCard
description="Strongly typed programming language that builds on JavaScript, giving you better tooling at any scale"
documentation="https://www.typescriptlang.org/"
name="TypeScript"
/>
<TechnologyCard
description="Rapidly build modern websites without ever leaving your HTML"
documentation="https://tailwindcss.com/"
name="TailwindCSS"
/>
<TechnologyCard
description="End-to-end typesafe APIs made easy"
documentation="https://trpc.io/"
name="tRPC"
/>
<TechnologyCard
description="Authentication for Next.js"
documentation="https://next-auth.js.org/"
name="Next-Auth"
/>
<TechnologyCard
description="Build data-driven JavaScript & TypeScript apps in less time"
documentation="https://www.prisma.io/docs/"
name="Prisma"
/>
</div>
<div className="pt-6 text-2xl text-blue-500 flex justify-center items-center w-full">
{hello.data ? <p>{hello.data.greeting}</p> : <p>Loading..</p>}
</div>
<pre className="w-1/2">{JSON.stringify(getAll.data, null, 2)}</pre>
</main>
</>
);
};
export default Home;
type TechnologyCardProps = {
description: string;
documentation: string;
name: string;
};
function TechnologyCard({
name,
description,
documentation,
}: TechnologyCardProps) {
return (
<section className="flex flex-col justify-center p-6 duration-500 border-2 border-gray-500 rounded shadow-xl motion-safe:hover:scale-105">
<h2 className="text-lg text-gray-700">{name}</h2>
<p className="text-sm text-gray-600">{description}</p>
<a
className="mt-3 text-sm underline text-violet-500 decoration-dotted underline-offset-2"
href={documentation}
rel="noreferrer"
target="_blank">
Documentation
</a>
</section>
);
}

@ -0,0 +1,15 @@
// Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs
import type { GetServerSidePropsContext } from 'next';
// eslint-disable-next-line camelcase
import { unstable_getServerSession } from 'next-auth';
import { authOptions as nextAuthOptions } from '~/pages/api/auth/[...nextauth]';
// Next API route example - /pages/api/restricted.ts
export const getServerAuthSession = async (ctx: {
req: GetServerSidePropsContext['req'];
res: GetServerSidePropsContext['res'];
}) => {
return await unstable_getServerSession(ctx.req, ctx.res, nextAuthOptions);
};

@ -0,0 +1,20 @@
// Src/server/db/client.ts
import { PrismaClient } from '@prisma/client';
import { env } from '~/env/server.mjs';
declare global {
// eslint-disable-next-line no-var, init-declarations
var prisma: PrismaClient | undefined;
}
export const prisma =
global.prisma ||
new PrismaClient({
log:
env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (env.NODE_ENV !== 'production') {
global.prisma = prisma;
}

@ -0,0 +1,61 @@
// Src/server/router/context.ts
import type { Session } from 'next-auth';
import * as trpc from '@trpc/server';
import type * as trpcNext from '@trpc/server/adapters/next';
import { getServerAuthSession } from '~/server/common/get-server-auth-session';
import { prisma } from '~/server/db/client';
type CreateContextOptions = {
session: Session | null;
};
/** Use this helper for:
* - testing, where we dont have to Mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
**/
export const createContextInner = async (opts: CreateContextOptions) => {
return {
prisma,
session: opts.session,
};
};
/**
* This is the actual context you'll use in your router
* @link https://trpc.io/docs/context
**/
export const createContext = async (
opts: trpcNext.CreateNextContextOptions,
) => {
const { req, res } = opts;
// Get the session from the server using the unstable_getServerSession wrapper function
const session = await getServerAuthSession({ req, res });
return await createContextInner({
session,
});
};
type Context = trpc.inferAsyncReturnType<typeof createContext>;
export const createRouter = () => trpc.router<Context>();
/**
* Creates a tRPC router that asserts all queries and mutations are from an authorized user. Will throw an unauthorized error if a user is not signed in.
**/
export function createProtectedRouter() {
return createRouter().middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new trpc.TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
// Infers that `session` is non-nullable to downstream resolvers
session: { ...ctx.session, user: ctx.session.user },
},
});
});
}

@ -0,0 +1,23 @@
import { z } from 'zod';
import { createRouter } from './context';
export const exampleRouter = createRouter()
.query('hello', {
input: z
.object({
text: z.string().nullish(),
})
.nullish(),
resolve({ input }) {
return {
greeting: `Hello ${input?.text ?? 'world'}`,
};
},
})
.query('getAll', {
async resolve({ ctx }) {
const items = await ctx.prisma.example.findMany();
return items;
},
});

@ -0,0 +1,14 @@
// Src/server/router/index.ts
import superjson from 'superjson';
import { createRouter } from './context';
import { exampleRouter } from './example';
import { protectedExampleRouter } from './protected-example-router';
export const appRouter = createRouter()
.transformer(superjson)
.merge('example.', exampleRouter)
.merge('auth.', protectedExampleRouter);
// Export type definition of API
export type AppRouter = typeof appRouter;

@ -0,0 +1,14 @@
import { createProtectedRouter } from './context';
// Example router with queries that can only be hit if the user requesting is signed in
export const protectedExampleRouter = createProtectedRouter()
.query('getSession', {
resolve({ ctx }) {
return ctx.session;
},
})
.query('getSecretMessage', {
resolve({ ctx: _ctx }) {
return 'He who asks a question is a fool for five minutes; he who does not ask a question remains a fool forever.';
},
});

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,12 @@
import type { DefaultSession } from 'next-auth';
declare module 'next-auth' {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
type Session = {
user?: DefaultSession['user'] & {
id: string;
};
};
}

@ -0,0 +1,27 @@
// Src/utils/trpc.ts
import { createReactQueryHooks } from '@trpc/react';
import type { inferProcedureInput, inferProcedureOutput } from '@trpc/server';
import type { AppRouter } from '~/server/router';
export const trpc = createReactQueryHooks<AppRouter>();
/**
* These are helper types to infer the input and output of query resolvers
* @example type HelloOutput = inferQueryOutput<'hello'>
*/
export type inferQueryOutput<
TRouteKey extends keyof AppRouter['_def']['queries'],
> = inferProcedureOutput<AppRouter['_def']['queries'][TRouteKey]>;
export type inferQueryInput<
TRouteKey extends keyof AppRouter['_def']['queries'],
> = inferProcedureInput<AppRouter['_def']['queries'][TRouteKey]>;
export type inferMutationOutput<
TRouteKey extends keyof AppRouter['_def']['mutations'],
> = inferProcedureOutput<AppRouter['_def']['mutations'][TRouteKey]>;
export type inferMutationInput<
TRouteKey extends keyof AppRouter['_def']['mutations'],
> = inferProcedureInput<AppRouter['_def']['mutations'][TRouteKey]>;

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

@ -0,0 +1,13 @@
{
"exclude": ["node_modules"],
"extends": "@tih/tsconfig/nextjs.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"baseUrl": "./src",
"paths": {
"~/*": ["*"]
}
},
"include": ["src", "next-env.d.ts"]
}

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['tih'],
};

@ -0,0 +1,24 @@
const path = require('path');
module.exports = {
stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.tsx'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-vite',
},
async viteFinal(config, { configType }) {
// customize the Vite config here
return {
...config,
resolve: {
alias: [
{
find: '@tih/ui',
replacement: path.resolve(__dirname, '../../../packages/ui/'),
},
],
},
};
},
};

@ -0,0 +1,30 @@
{
"name": "@tih/storybook",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "start-storybook -p 6001",
"build": "build-storybook --docs",
"preview-storybook": "serve storybook-static",
"clean": "rm -rf .turbo && rm -rf node_modules"
},
"dependencies": {
"@tih/ui": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@storybook/addon-actions": "^6.4.18",
"@storybook/addon-docs": "^6.4.22",
"@storybook/addon-essentials": "^6.4.18",
"@storybook/addon-links": "^6.4.18",
"@storybook/builder-vite": "^0.1.33",
"@storybook/react": "^6.4.18",
"@tih/tsconfig": "*",
"@vitejs/plugin-react": "^1.3.2",
"eslint-config-tih": "*",
"serve": "^13.0.2",
"typescript": "^4.8.3",
"vite": "^2.9.9"
}
}

@ -0,0 +1,20 @@
import { CounterButton } from '@tih/ui';
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
<Meta title="Components/Button" component={CounterButton} />
# Button
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget consectetur tempor, nisl nunc egestas nisi, euismod aliquam nisl nunc euismod.
## Props
<ArgsTable of={CounterButton} />
## Examples
<Canvas>
<Story name="Default">
<CounterButton>Hello</CounterButton>
</Story>
</Canvas>

File diff suppressed because it is too large Load Diff

@ -6,7 +6,7 @@ function graphTopoSort(numberNodes, edges) {
nodes.set(i, { in: 0, out: new Set() }); nodes.set(i, { in: 0, out: new Set() });
} }
edges.forEach(edge => { edges.forEach((edge) => {
const [node_id, pre_id] = edge; const [node_id, pre_id] = edge;
nodes.get(node_id).in += 1; nodes.get(node_id).in += 1;
nodes.get(pre_id).out.add(node_id); nodes.get(pre_id).out.add(node_id);
@ -32,4 +32,9 @@ function graphTopoSort(numberNodes, edges) {
return order.length == numberNodes ? order : []; return order.length == numberNodes ? order : [];
} }
console.log(graphTopoSort(3, [[0, 1], [0, 2]])); console.log(
graphTopoSort(3, [
[0, 1],
[0, 2],
]),
);

@ -1,5 +1,5 @@
function matrixClone(matrix, defaultValue) { function matrixClone(matrix, defaultValue) {
return matrix.map(row => { return matrix.map((row) => {
return defaultValue === undefined return defaultValue === undefined
? row.slice(0) ? row.slice(0)
: Array(row.length).fill(defaultValue); : Array(row.length).fill(defaultValue);
@ -9,14 +9,53 @@ function matrixClone(matrix, defaultValue) {
const deepEqual = require('./deepEqual'); const deepEqual = require('./deepEqual');
// Test clone. // Test clone.
const a = [[1, 2], [1, 4]]; const a = [
console.log(deepEqual(matrixClone(a), [[1, 2], [1, 4]])); [1, 2],
[1, 4],
];
console.log(
deepEqual(matrixClone(a), [
[1, 2],
[1, 4],
]),
);
a[0][0] = 4; a[0][0] = 4;
console.log(deepEqual(matrixClone(a), [[1, 2], [1, 4]]) === false); console.log(
deepEqual(matrixClone(a), [
[1, 2],
[1, 4],
]) === false,
);
console.log(deepEqual(matrixClone([[1]]), [[1]])); console.log(deepEqual(matrixClone([[1]]), [[1]]));
// Test clone with default value. // Test clone with default value.
console.log(deepEqual(matrixClone([[1, 2], [1, 4]], 1), [[1, 1], [1, 1]]));
console.log( console.log(
deepEqual(matrixClone([[1, 2], [1, 4]], null), [[null, null], [null, null]]), deepEqual(
matrixClone(
[
[1, 2],
[1, 4],
],
1,
),
[
[1, 1],
[1, 1],
],
),
);
console.log(
deepEqual(
matrixClone(
[
[1, 2],
[1, 4],
],
null,
),
[
[null, null],
[null, null],
],
),
); );

@ -1,12 +1,33 @@
function matrixTranspose(matrix) { function matrixTranspose(matrix) {
return matrix[0].map((col, i) => matrix.map(row => row[i])); return matrix[0].map((col, i) => matrix.map((row) => row[i]));
} }
const deepEqual = require('./deepEqual'); const deepEqual = require('./deepEqual');
console.log(deepEqual(matrixTranspose([[1]]), [[1]])); console.log(deepEqual(matrixTranspose([[1]]), [[1]]));
console.log(deepEqual(matrixTranspose([[1, 2]]), [[1], [2]])); console.log(deepEqual(matrixTranspose([[1, 2]]), [[1], [2]]));
console.log(deepEqual(matrixTranspose([[1, 2], [1, 4]]), [[1, 1], [2, 4]]));
console.log( console.log(
deepEqual(matrixTranspose([[1, 2, 3], [4, 5, 6]]), [[1, 4], [2, 5], [3, 6]]), deepEqual(
matrixTranspose([
[1, 2],
[1, 4],
]),
[
[1, 1],
[2, 4],
],
),
);
console.log(
deepEqual(
matrixTranspose([
[1, 2, 3],
[4, 5, 6],
]),
[
[1, 4],
[2, 5],
[3, 6],
],
),
); );

@ -1,14 +1,19 @@
function traverse(matrix) { function traverse(matrix) {
const DIRECTIONS = [[0, 1], [0, -1], [1, 0], [-1, 0]]; const DIRECTIONS = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
const rows = matrix.length; const rows = matrix.length;
const cols = matrix[0].length; const cols = matrix[0].length;
const visited = matrix.map(row => Array(row.length).fill(false)); const visited = matrix.map((row) => Array(row.length).fill(false));
function dfs(i, j) { function dfs(i, j) {
if (visited[i][j]) { if (visited[i][j]) {
return; return;
} }
visited[i][j] = true; visited[i][j] = true;
DIRECTIONS.forEach(dir => { DIRECTIONS.forEach((dir) => {
const row = i + dir[0], const row = i + dir[0],
col = j + dir[1]; col = j + dir[1];
// Boundary check. // Boundary check.

@ -37,26 +37,14 @@ console.log(deepEqual(mergeSort([2, 1]), [1, 2]));
console.log(deepEqual(mergeSort([7, 2, 4, 3, 1, 2]), [1, 2, 2, 3, 4, 7])); console.log(deepEqual(mergeSort([7, 2, 4, 3, 1, 2]), [1, 2, 2, 3, 4, 7]));
console.log(deepEqual(mergeSort([1, 2, 3, 4, 5, 0]), [0, 1, 2, 3, 4, 5])); console.log(deepEqual(mergeSort([1, 2, 3, 4, 5, 0]), [0, 1, 2, 3, 4, 5]));
console.log( console.log(
deepEqual(mergeSort([10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), [ deepEqual(
1, mergeSort([10, 9, 8, 7, 6, 5, 4, 3, 2, 1]),
2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
3, ),
4,
5,
6,
7,
8,
9,
10,
]),
); );
console.log( console.log(
deepEqual(mergeSort([98322, 3242, 876, -234, 34, 12331]), [ deepEqual(
-234, mergeSort([98322, 3242, 876, -234, 34, 12331]),
34, [-234, 34, 876, 3242, 12331, 98322],
876, ),
3242,
12331,
98322,
]),
); );

@ -0,0 +1,20 @@
{
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"build": "turbo run build",
"clean": "turbo run clean",
"dev": "turbo run dev --no-cache --parallel --continue",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"lint": "turbo run lint",
"test": "turbo run test",
"tsc": "turbo run tsc"
},
"devDependencies": {
"prettier": "^2.7.1",
"turbo": "latest"
}
}

@ -0,0 +1,147 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
const OFF = 0;
const WARN = 1;
const ERROR = 2;
module.exports = {
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'simple-import-sort',
'sort-keys-fix',
'typescript-sort-keys',
],
extends: [
'next',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
settings: {
react: {
version: 'detect',
},
},
rules: {
camelcase: [ERROR, { properties: 'never', ignoreDestructuring: true }],
'capitalized-comments': [
ERROR,
'always',
{ ignoreConsecutiveComments: true },
],
'consistent-this': ERROR,
curly: ERROR,
'dot-notation': ERROR,
eqeqeq: [ERROR, 'smart'],
'func-name-matching': ERROR,
'func-names': [ERROR, 'as-needed'],
'func-style': [ERROR, 'declaration', { allowArrowFunctions: true }],
'guard-for-in': ERROR,
'init-declarations': ERROR,
'no-console': [ERROR, { allow: ['warn', 'error', 'info'] }],
'no-else-return': [ERROR, { allowElseIf: false }],
'no-extra-boolean-cast': ERROR,
'no-lonely-if': ERROR,
'no-shadow': ERROR,
'no-unused-vars': OFF, // Use @typescript-eslint/no-unused-vars instead.
'object-shorthand': ERROR,
'one-var': [ERROR, 'never'],
'operator-assignment': ERROR,
'prefer-arrow-callback': ERROR,
'prefer-const': ERROR,
'prefer-destructuring': [
ERROR,
{
object: true,
},
],
radix: ERROR,
'spaced-comment': ERROR,
'react/button-has-type': ERROR,
'react/display-name': OFF,
'react/destructuring-assignment': [ERROR, 'always'],
// 'react/hook-use-state': ERROR,
'react/no-array-index-key': ERROR,
'react/no-unescaped-entities': OFF,
'react/void-dom-elements-no-children': ERROR,
'react/jsx-boolean-value': [ERROR, 'always'],
'react/jsx-curly-brace-presence': [
ERROR,
{ props: 'never', children: 'never' },
],
'react/jsx-no-useless-fragment': ERROR,
'react/jsx-sort-props': [
ERROR,
{
callbacksLast: true,
shorthandFirst: true,
reservedFirst: true,
},
],
'@next/next/no-img-element': OFF,
'@next/next/no-html-link-for-pages': OFF,
'@typescript-eslint/array-type': [
ERROR,
{ default: 'generic', readonly: 'generic' },
],
'@typescript-eslint/consistent-generic-constructors': [
ERROR,
'constructor',
],
'@typescript-eslint/consistent-indexed-object-style': [ERROR, 'record'],
'@typescript-eslint/consistent-type-definitions': [ERROR, 'type'],
'@typescript-eslint/consistent-type-imports': ERROR,
'@typescript-eslint/no-duplicate-enum-values': ERROR,
'@typescript-eslint/no-for-in-array': ERROR,
'@typescript-eslint/no-non-null-assertion': OFF,
'@typescript-eslint/no-unused-vars': [ERROR, { argsIgnorePattern: '^_' }],
'@typescript-eslint/prefer-optional-chain': ERROR,
'@typescript-eslint/require-array-sort-compare': ERROR,
'@typescript-eslint/restrict-plus-operands': ERROR,
'@typescript-eslint/sort-type-union-intersection-members': ERROR,
// Sorting
'typescript-sort-keys/interface': ERROR,
'typescript-sort-keys/string-enum': ERROR,
'sort-keys-fix/sort-keys-fix': ERROR,
'simple-import-sort/exports': WARN,
'simple-import-sort/imports': [
WARN,
{
groups: [
// Ext library & side effect imports.
['^~?\\w', '^\\u0000', '^@'],
// Lib and hooks.
['^~/lib', '^~/hooks'],
// Static data.
['^~/data'],
// Components.
['^~/components'],
// Other imports.
['^~/'],
// Relative paths up until 3 level.
[
'^\\./?$',
'^\\.(?!/?$)',
'^\\.\\./?$',
'^\\.\\.(?!/?$)',
'^\\.\\./\\.\\./?$',
'^\\.\\./\\.\\.(?!/?$)',
'^\\.\\./\\.\\./\\.\\./?$',
'^\\.\\./\\.\\./\\.\\.(?!/?$)',
],
['^~/types'],
// {s}css files
['^.+\\.s?css$'],
// Others that don't fit in.
['^'],
],
},
],
},
};

@ -0,0 +1,21 @@
{
"name": "eslint-config-tih",
"version": "0.0.0",
"private": true,
"license": "MIT",
"main": "index.js",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"eslint-config-next": "^12.3.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "7.28.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sort-keys-fix": "^1.1.2",
"eslint-plugin-typescript-sort-keys": "^2.1.0",
"eslint-config-turbo": "latest"
},
"publishConfig": {
"access": "public"
}
}

@ -0,0 +1,20 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true
},
"exclude": ["node_modules"]
}

@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"allowJs": true,
"declaration": false,
"declarationMap": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"noEmit": true,
"resolveJsonModule": true,
"target": "es5",
"strict": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"]
}

@ -0,0 +1,9 @@
{
"name": "@tih/tsconfig",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}

@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React Library",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2015"],
"module": "ESNext",
"target": "es6"
}
}

@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ['tih'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
};

@ -0,0 +1,30 @@
{
"name": "@tih/ui",
"version": "0.0.0",
"private": true,
"license": "MIT",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/**"
],
"scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"clean": "rm -rf dist",
"tsc": "tsc",
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
"lint": "eslint src/**/*.ts* --fix"
},
"devDependencies": {
"@tih/tsconfig": "*",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"eslint": "^8.24.0",
"eslint-config-tih": "*",
"react": "^18.2.0",
"tsup": "^6.2.3",
"typescript": "^4.8.3"
}
}

@ -0,0 +1,42 @@
import * as React from 'react';
export function CounterButton() {
const [count, setCount] = React.useState(0);
return (
<div
style={{
background: `rgba(0,0,0,0.05)`,
borderRadius: `8px`,
fontWeight: 500,
padding: '1.5rem',
}}>
<p style={{ margin: '0 0 1.5rem 0' }}>
This component is from{' '}
<code
style={{
background: `rgba(0,0,0,0.1)`,
borderRadius: '0.25rem',
padding: '0.2rem 0.3rem',
}}>
@tih/ui
</code>
</p>
<div>
<button
style={{
background: 'black',
border: 'none',
borderRadius: '0.25rem',
color: 'white',
cursor: 'pointer',
display: 'inline-block',
padding: '0.5rem 1rem',
}}
type="button"
onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
</div>
</div>
);
}

@ -0,0 +1,15 @@
import * as React from 'react';
export function NewTabLink({
children,
href,
...other
}: {
children: React.ReactNode;
href: string;
}) {
return (
<a href={href} rel="noreferrer" target="_blank" {...other}>
{children}
</a>
);
}

@ -0,0 +1,2 @@
export { CounterButton } from './CounterButton';
export { NewTabLink } from './NewTabLink';

@ -0,0 +1,8 @@
{
"compilerOptions": {
"lib": ["dom", "ES2015"]
},
"extends": "@tih/tsconfig/react-library.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

@ -0,0 +1,32 @@
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"outputs": [
"dist/**",
".next/**",
"build/**",
"api/**",
"public/build/**"
],
"dependsOn": ["^build"]
},
"test": {
"outputs": ["coverage/**"],
"dependsOn": []
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"tsc": {
"cache": true
},
"dev": {
"cache": false
},
"clean": {
"cache": false
}
}
}

10196
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save