init
This commit is contained in:
commit
98186148ef
|
|
@ -0,0 +1,2 @@
|
||||||
|
APP_URL="http://localhost:3000"
|
||||||
|
WS_URL="ws://localhost:3001"
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
|
||||||
|
"sourceType": "module", // Allows for the use of imports,
|
||||||
|
"project": "tsconfig.json"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
},
|
||||||
|
// "overrides": [
|
||||||
|
// {
|
||||||
|
// "files": [],
|
||||||
|
// "rules": {
|
||||||
|
// "@typescript-eslint/no-unused-vars": "off"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: 'CodeQL'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, 0.x]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
- cron: '27 0 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ['typescript']
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
name: E2E-testing
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
env:
|
||||||
|
POSTGRES_DATABASE: trpcdb
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
env:
|
||||||
|
NODE_ENV: test
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: ['18.x']
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8.5.1
|
||||||
|
|
||||||
|
- name: Use Node ${{ matrix.node }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
# cache: 'pnpm' # You can active this cache when your repo has a lockfile
|
||||||
|
|
||||||
|
- name: Install deps (with cache)
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Next.js cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/.next/cache
|
||||||
|
key: ${{ runner.os }}-${{ runner.node }}-${{ hashFiles('**/pnpm-lock.yaml') }}-nextjs
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: pnpm build && pnpm lint
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# 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*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
gameState.json
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"prisma.prisma"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Gameshow 2023
|
||||||
|
|
||||||
|
## Getting Startet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm i
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
version: '3.6'
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:13
|
||||||
|
ports:
|
||||||
|
- '5932:5432' # expose pg on port 5932 to not collide with pg from elsewhere
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${PGPASSWORD}
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
|
@ -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,19 @@
|
||||||
|
/**
|
||||||
|
* @link https://nextjs.org/docs/api-reference/next.config.js/introduction
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
|
const config = {
|
||||||
|
serverRuntimeConfig: {
|
||||||
|
// Will only be available on the server side
|
||||||
|
},
|
||||||
|
publicRuntimeConfig: {
|
||||||
|
// Will be available on both server and client
|
||||||
|
APP_URL: process.env.APP_URL,
|
||||||
|
WS_URL: process.env.WS_URL,
|
||||||
|
},
|
||||||
|
/** We run eslint as a separate task in CI */
|
||||||
|
eslint: { ignoreDuringBuilds: !!process.env.CI },
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"name": "@examples/next-websockets-starter",
|
||||||
|
"version": "10.31.0",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.15.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build:1-next": "cross-env NODE_ENV=production next build",
|
||||||
|
"build:2-server": "tsc --project tsconfig.server.json",
|
||||||
|
"build": "run-s build:*",
|
||||||
|
"dev:wss": "cross-env PORT=3001 tsx watch src/server/wssDevServer.ts --tsconfig tsconfig.server.json ",
|
||||||
|
"dev:next": "next dev",
|
||||||
|
"dev": "run-p dev:*",
|
||||||
|
"start": "cross-env NODE_ENV=production node dist/server/prodServer.js",
|
||||||
|
"lint": "eslint --cache --ext \".js,.ts,.tsx\" --report-unused-disable-directives --report-unused-disable-directives src",
|
||||||
|
"lint-fix": "pnpm lint --fix"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 80,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@chakra-ui/react": "^2.7.1",
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@tanstack/react-query": "^4.18.0",
|
||||||
|
"@trpc/client": "^10.31.0",
|
||||||
|
"@trpc/next": "^10.31.0",
|
||||||
|
"@trpc/react-query": "^10.31.0",
|
||||||
|
"@trpc/server": "^10.31.0",
|
||||||
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
|
"framer-motion": "^10.12.16",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"next": "^13.4.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"superjson": "^1.7.4",
|
||||||
|
"tsx": "^3.12.7",
|
||||||
|
"ws": "^8.0.0",
|
||||||
|
"zod": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tanstack/react-query-devtools": "^4.18.0",
|
||||||
|
"@types/node": "^18.16.16",
|
||||||
|
"@types/react": "^18.2.8",
|
||||||
|
"@types/ws": "^8.2.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||||
|
"@typescript-eslint/parser": "^5.59.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.40.0",
|
||||||
|
"eslint-config-next": "^13.4.3",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"start-server-and-test": "^1.12.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "restricted"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { extendTheme } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const theme = extendTheme({});
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Button, HStack, Input, useNumberInput } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountInput: React.FC<Props> = (props) => {
|
||||||
|
const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } =
|
||||||
|
useNumberInput({
|
||||||
|
step: 1,
|
||||||
|
value: props.value,
|
||||||
|
min: 0,
|
||||||
|
onChange: (valueString) => {
|
||||||
|
const value = parseFloat(valueString);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.onChange(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inc = getIncrementButtonProps();
|
||||||
|
const dec = getDecrementButtonProps();
|
||||||
|
const input = getInputProps();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack maxW="320px">
|
||||||
|
<Button {...dec}>-</Button>
|
||||||
|
<Input {...input} />
|
||||||
|
<Button {...inc}>+</Button>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CountInput;
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { HStack, Heading } from '@chakra-ui/react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { GameState } from 'types/GameState';
|
||||||
|
import { trpc } from 'utils/trpc';
|
||||||
|
import CountInput from './components/CountInput';
|
||||||
|
|
||||||
|
export const AdminPanel = () => {
|
||||||
|
const [gameState, setGameState] = useState<GameState | undefined>(undefined);
|
||||||
|
const gameStateQuery = trpc.game.get.useQuery();
|
||||||
|
const mutation = trpc.game.update.useMutation();
|
||||||
|
trpc.game.onUpdate.useSubscription(undefined, {
|
||||||
|
onData: (data) => {
|
||||||
|
setGameState(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameStateQuery.data) {
|
||||||
|
setGameState(gameStateQuery.data);
|
||||||
|
}
|
||||||
|
}, [gameStateQuery.data]);
|
||||||
|
if (!gameState) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack alignItems="start">
|
||||||
|
<Heading>Gameshow Admin Panel</Heading>
|
||||||
|
<CountInput
|
||||||
|
value={gameState.round}
|
||||||
|
onChange={(value) => {
|
||||||
|
mutation.mutate({ round: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
|
import type { AppType } from 'next/app';
|
||||||
|
import { trpc } from 'utils/trpc';
|
||||||
|
|
||||||
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
|
return (
|
||||||
|
<ChakraProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</ChakraProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default trpc.withTRPC(MyApp);
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { AdminPanel } from 'client/view/AdminPanel';
|
||||||
|
|
||||||
|
const AdminPage = () => {
|
||||||
|
return <AdminPanel />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminPage;
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* This file contains the tRPC http response handler and context creation for Next.js
|
||||||
|
*/
|
||||||
|
import * as trpcNext from '@trpc/server/adapters/next';
|
||||||
|
import { createContext } from 'server/context';
|
||||||
|
import { AppRouter, appRouter } from 'server/routers/_app';
|
||||||
|
|
||||||
|
export default trpcNext.createNextApiHandler<AppRouter>({
|
||||||
|
router: appRouter,
|
||||||
|
/**
|
||||||
|
* @link https://trpc.io/docs/context
|
||||||
|
*/
|
||||||
|
createContext,
|
||||||
|
/**
|
||||||
|
* @link https://trpc.io/docs/error-handling
|
||||||
|
*/
|
||||||
|
onError({ error }) {
|
||||||
|
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
||||||
|
// send to bug reporting
|
||||||
|
console.error('Something went wrong', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Enable query batching
|
||||||
|
*/
|
||||||
|
batching: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
|
||||||
|
const IndexPage: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button>Test</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndexPage;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as trpc from '@trpc/server';
|
||||||
|
import * as trpcNext from '@trpc/server/adapters/next';
|
||||||
|
import { NodeHTTPCreateContextFnOptions } from '@trpc/server/adapters/node-http';
|
||||||
|
import { IncomingMessage } from 'http';
|
||||||
|
import ws from 'ws';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates context for an incoming request
|
||||||
|
* @link https://trpc.io/docs/context
|
||||||
|
*/
|
||||||
|
export const createContext = async (
|
||||||
|
opts:
|
||||||
|
| trpcNext.CreateNextContextOptions
|
||||||
|
| NodeHTTPCreateContextFnOptions<IncomingMessage, ws>,
|
||||||
|
) => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Context = trpc.inferAsyncReturnType<typeof createContext>;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { GameState } from 'types/GameState';
|
||||||
|
|
||||||
|
const initialState: GameState = {
|
||||||
|
round: 0,
|
||||||
|
players: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGameState = (): GameState => {
|
||||||
|
try {
|
||||||
|
return fs.readJSONSync('gameState.json');
|
||||||
|
} catch (_) {
|
||||||
|
return initialState;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setGameState = (state: GameState) => {
|
||||||
|
return fs.writeJSONSync('gameState.json', state);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { createContext } from './context';
|
||||||
|
import { appRouter } from './routers/_app';
|
||||||
|
import { applyWSSHandler } from '@trpc/server/adapters/ws';
|
||||||
|
import http from 'http';
|
||||||
|
import next from 'next';
|
||||||
|
import { parse } from 'url';
|
||||||
|
import ws from 'ws';
|
||||||
|
|
||||||
|
const port = parseInt(process.env.PORT || '3000', 10);
|
||||||
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
|
const app = next({ dev });
|
||||||
|
const handle = app.getRequestHandler();
|
||||||
|
|
||||||
|
void app.prepare().then(() => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const proto = req.headers['x-forwarded-proto'];
|
||||||
|
if (proto && proto === 'http') {
|
||||||
|
// redirect to ssl
|
||||||
|
res.writeHead(303, {
|
||||||
|
location: `https://` + req.headers.host + (req.headers.url ?? ''),
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const parsedUrl = parse(req.url!, true);
|
||||||
|
void handle(req, res, parsedUrl);
|
||||||
|
});
|
||||||
|
const wss = new ws.Server({ server });
|
||||||
|
const handler = applyWSSHandler({ wss, router: appRouter, createContext });
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM');
|
||||||
|
handler.broadcastReconnectNotification();
|
||||||
|
});
|
||||||
|
server.listen(port);
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.log(
|
||||||
|
`> Server listening at http://localhost:${port} as ${
|
||||||
|
dev ? 'development' : process.env.NODE_ENV
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* This file contains the root router of your tRPC-backend
|
||||||
|
*/
|
||||||
|
import { router, publicProcedure } from '../trpc';
|
||||||
|
import { gameRouter } from './game';
|
||||||
|
import { postRouter } from './post';
|
||||||
|
import { observable } from '@trpc/server/observable';
|
||||||
|
import { clearInterval } from 'timers';
|
||||||
|
|
||||||
|
export const appRouter = router({
|
||||||
|
healthcheck: publicProcedure.query(() => 'yay!'),
|
||||||
|
game: gameRouter,
|
||||||
|
|
||||||
|
randomNumber: publicProcedure.subscription(() => {
|
||||||
|
return observable<number>((emit) => {
|
||||||
|
const int = setInterval(() => {
|
||||||
|
emit.next(Math.random());
|
||||||
|
}, 500);
|
||||||
|
return () => {
|
||||||
|
clearInterval(int);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { publicProcedure, router } from '../trpc';
|
||||||
|
import { GameState, gameStateSchema } from 'types/GameState';
|
||||||
|
import { observable } from '@trpc/server/observable';
|
||||||
|
import { getGameState, setGameState } from 'server/gameStateStore';
|
||||||
|
|
||||||
|
interface GameEvents {
|
||||||
|
update: (state: GameState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface GameEventEmitter {
|
||||||
|
on<U extends keyof GameEvents>(event: U, listener: GameEvents[U]): this;
|
||||||
|
off<U extends keyof GameEvents>(event: U, listener: GameEvents[U]): this;
|
||||||
|
emit<U extends keyof GameEvents>(
|
||||||
|
event: U,
|
||||||
|
...args: Parameters<GameEvents[U]>
|
||||||
|
): boolean;
|
||||||
|
once<U extends keyof GameEvents>(event: U, listener: GameEvents[U]): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameEventEmitter extends EventEmitter {}
|
||||||
|
|
||||||
|
const gameEventEmitter = new GameEventEmitter();
|
||||||
|
|
||||||
|
export const gameRouter = router({
|
||||||
|
update: publicProcedure.input(gameStateSchema).mutation(({ input }) => {
|
||||||
|
setGameState(input);
|
||||||
|
gameEventEmitter.emit('update', input);
|
||||||
|
}),
|
||||||
|
get: publicProcedure.query(() => {
|
||||||
|
return getGameState();
|
||||||
|
}),
|
||||||
|
onUpdate: publicProcedure.subscription(() => {
|
||||||
|
return observable<GameState>((emit) => {
|
||||||
|
const onUpdate = (state: GameState) => emit.next(state);
|
||||||
|
gameEventEmitter.on('update', onUpdate);
|
||||||
|
return () => gameEventEmitter.off('update', onUpdate);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx`
|
||||||
|
*/
|
||||||
|
import { observable } from '@trpc/server/observable';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { authedProcedure, publicProcedure, router } from '../trpc';
|
||||||
|
|
||||||
|
interface MyEvents {
|
||||||
|
add: (data: string) => void;
|
||||||
|
isTypingUpdate: () => void;
|
||||||
|
}
|
||||||
|
declare interface MyEventEmitter {
|
||||||
|
on<TEv extends keyof MyEvents>(event: TEv, listener: MyEvents[TEv]): this;
|
||||||
|
off<TEv extends keyof MyEvents>(event: TEv, listener: MyEvents[TEv]): this;
|
||||||
|
once<TEv extends keyof MyEvents>(event: TEv, listener: MyEvents[TEv]): this;
|
||||||
|
emit<TEv extends keyof MyEvents>(
|
||||||
|
event: TEv,
|
||||||
|
...args: Parameters<MyEvents[TEv]>
|
||||||
|
): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyEventEmitter extends EventEmitter {}
|
||||||
|
|
||||||
|
// In a real app, you'd probably use Redis or something
|
||||||
|
const ee = new MyEventEmitter();
|
||||||
|
|
||||||
|
// who is currently typing, key is `name`
|
||||||
|
const currentlyTyping: Record<string, { lastTyped: Date }> =
|
||||||
|
Object.create(null);
|
||||||
|
|
||||||
|
// every 1s, clear old "isTyping"
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
let updated = false;
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, value] of Object.entries(currentlyTyping)) {
|
||||||
|
if (now - value.lastTyped.getTime() > 3e3) {
|
||||||
|
delete currentlyTyping[key];
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
ee.emit('isTypingUpdate');
|
||||||
|
}
|
||||||
|
}, 3e3);
|
||||||
|
process.on('SIGTERM', () => clearInterval(interval));
|
||||||
|
|
||||||
|
export const postRouter = router({
|
||||||
|
add: authedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.string().uuid().optional(),
|
||||||
|
text: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { name } = ctx.user;
|
||||||
|
ee.emit('add', 'test');
|
||||||
|
delete currentlyTyping[name];
|
||||||
|
ee.emit('isTypingUpdate');
|
||||||
|
}),
|
||||||
|
|
||||||
|
isTyping: authedProcedure
|
||||||
|
.input(z.object({ typing: z.boolean() }))
|
||||||
|
.mutation(({ input, ctx }) => {
|
||||||
|
const { name } = ctx.user;
|
||||||
|
if (!input.typing) {
|
||||||
|
delete currentlyTyping[name];
|
||||||
|
} else {
|
||||||
|
currentlyTyping[name] = {
|
||||||
|
lastTyped: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ee.emit('isTypingUpdate');
|
||||||
|
}),
|
||||||
|
|
||||||
|
whoIsTyping: publicProcedure.subscription(() => {
|
||||||
|
let prev: string[] | null = null;
|
||||||
|
return observable<string[]>((emit) => {
|
||||||
|
const onIsTypingUpdate = () => {
|
||||||
|
const newData = Object.keys(currentlyTyping);
|
||||||
|
|
||||||
|
if (!prev || prev.toString() !== newData.toString()) {
|
||||||
|
emit.next(newData);
|
||||||
|
}
|
||||||
|
prev = newData;
|
||||||
|
};
|
||||||
|
ee.on('isTypingUpdate', onIsTypingUpdate);
|
||||||
|
return () => {
|
||||||
|
ee.off('isTypingUpdate', onIsTypingUpdate);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* This is your entry point to setup the root configuration for tRPC on the server.
|
||||||
|
* - `initTRPC` should only be used once per app.
|
||||||
|
* - We export only the functionality that we use so we can enforce which base procedures should be used
|
||||||
|
*
|
||||||
|
* Learn how to create protected base procedures and other things below:
|
||||||
|
* @see https://trpc.io/docs/v10/router
|
||||||
|
* @see https://trpc.io/docs/v10/procedures
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context } from './context';
|
||||||
|
import { initTRPC, TRPCError } from '@trpc/server';
|
||||||
|
import superjson from 'superjson';
|
||||||
|
|
||||||
|
const t = initTRPC.context<Context>().create({
|
||||||
|
/**
|
||||||
|
* @see https://trpc.io/docs/v10/data-transformers
|
||||||
|
*/
|
||||||
|
transformer: superjson,
|
||||||
|
/**
|
||||||
|
* @see https://trpc.io/docs/v10/error-formatting
|
||||||
|
*/
|
||||||
|
errorFormatter({ shape }) {
|
||||||
|
return shape;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a router
|
||||||
|
* @see https://trpc.io/docs/v10/router
|
||||||
|
*/
|
||||||
|
export const router = t.router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an unprotected procedure
|
||||||
|
* @see https://trpc.io/docs/v10/procedures
|
||||||
|
**/
|
||||||
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://trpc.io/docs/v10/middlewares
|
||||||
|
*/
|
||||||
|
export const middleware = t.middleware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://trpc.io/docs/v10/merging-routers
|
||||||
|
*/
|
||||||
|
export const mergeRouters = t.mergeRouters;
|
||||||
|
|
||||||
|
const isAuthed = middleware(({ next, ctx }) => {
|
||||||
|
const user = ctx.session?.user;
|
||||||
|
|
||||||
|
if (!user?.name) {
|
||||||
|
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
user: {
|
||||||
|
...user,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected base procedure
|
||||||
|
*/
|
||||||
|
export const authedProcedure = t.procedure.use(isAuthed);
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createContext } from './context';
|
||||||
|
import { appRouter } from './routers/_app';
|
||||||
|
import { applyWSSHandler } from '@trpc/server/adapters/ws';
|
||||||
|
import ws from 'ws';
|
||||||
|
|
||||||
|
const wss = new ws.Server({
|
||||||
|
port: 3001,
|
||||||
|
});
|
||||||
|
const handler = applyWSSHandler({ wss, router: appRouter, createContext });
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
console.log(`➕➕ Connection (${wss.clients.size})`);
|
||||||
|
ws.once('close', () => {
|
||||||
|
console.log(`➖➖ Connection (${wss.clients.size})`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log('✅ WebSocket Server listening on ws://localhost:3001');
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM');
|
||||||
|
handler.broadcastReconnectNotification();
|
||||||
|
wss.close();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const playerSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
score: z.number(),
|
||||||
|
answer: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gameStateSchema = z.object({
|
||||||
|
round: z.number(),
|
||||||
|
players: z.array(playerSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GameState = z.infer<typeof gameStateSchema>;
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
|
||||||
|
import { loggerLink } from '@trpc/client/links/loggerLink';
|
||||||
|
import { wsLink, createWSClient } from '@trpc/client/links/wsLink';
|
||||||
|
import { createTRPCNext } from '@trpc/next';
|
||||||
|
import type { inferProcedureOutput } from '@trpc/server';
|
||||||
|
import { NextPageContext } from 'next';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import type { AppRouter } from 'server/routers/_app';
|
||||||
|
import superjson from 'superjson';
|
||||||
|
|
||||||
|
// ℹ️ Type-only import:
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
||||||
|
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
|
const { APP_URL, WS_URL } = publicRuntimeConfig;
|
||||||
|
|
||||||
|
function getEndingLink(ctx: NextPageContext | undefined) {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return httpBatchLink({
|
||||||
|
url: `${APP_URL}/api/trpc`,
|
||||||
|
headers() {
|
||||||
|
if (!ctx?.req?.headers) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// on ssr, forward client's headers to the server
|
||||||
|
return {
|
||||||
|
...ctx.req.headers,
|
||||||
|
'x-ssr': '1',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const client = createWSClient({
|
||||||
|
url: WS_URL,
|
||||||
|
});
|
||||||
|
return wsLink<AppRouter>({
|
||||||
|
client,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
|
||||||
|
* @link https://trpc.io/docs/react#3-create-trpc-hooks
|
||||||
|
*/
|
||||||
|
export const trpc = createTRPCNext<AppRouter>({
|
||||||
|
config({ ctx }) {
|
||||||
|
/**
|
||||||
|
* If you want to use SSR, you need to use the server's full URL
|
||||||
|
* @link https://trpc.io/docs/ssr
|
||||||
|
*/
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @link https://trpc.io/docs/client/links
|
||||||
|
*/
|
||||||
|
links: [
|
||||||
|
// adds pretty logs to your console in development and logs errors in production
|
||||||
|
loggerLink({
|
||||||
|
enabled: (opts) =>
|
||||||
|
(process.env.NODE_ENV === 'development' &&
|
||||||
|
typeof window !== 'undefined') ||
|
||||||
|
(opts.direction === 'down' && opts.result instanceof Error),
|
||||||
|
}),
|
||||||
|
getEndingLink(ctx),
|
||||||
|
],
|
||||||
|
/**
|
||||||
|
* @link https://trpc.io/docs/data-transformers
|
||||||
|
*/
|
||||||
|
transformer: superjson,
|
||||||
|
/**
|
||||||
|
* @link https://tanstack.com/query/v4/docs/react/reference/QueryClient
|
||||||
|
*/
|
||||||
|
queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @link https://trpc.io/docs/ssr
|
||||||
|
*/
|
||||||
|
ssr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// export const transformer = superjson;
|
||||||
|
/**
|
||||||
|
* This is a helper method to infer the output of a query resolver
|
||||||
|
* @example type HelloOutput = inferQueryOutput<'hello'>
|
||||||
|
*/
|
||||||
|
export type inferQueryOutput<
|
||||||
|
TRouteKey extends keyof AppRouter['_def']['queries'],
|
||||||
|
> = inferProcedureOutput<AppRouter['_def']['queries'][TRouteKey]>;
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "dist",
|
||||||
|
"target": "ES2019",
|
||||||
|
"lib": ["ES2019", "DOM"],
|
||||||
|
"isolatedModules": false,
|
||||||
|
"noEmit": false
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue