diff --git a/.prettierrc b/.prettierrc
index e3b414c7e090..2f7223964035 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,5 +1,13 @@
{
"semi": false,
"singleQuote": true,
- "trailingComma": "all"
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": "*.md",
+ "options": {
+ "proseWrap": "never"
+ }
+ }
+ ]
}
diff --git a/README.md b/README.md
index 0cf01993cb78..713f1fa20243 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,7 @@
-[![test](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=latest)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+branch%3Alatest)
-[![keep-prisma-dependencies-updated](https://github.com/prisma/prisma-examples/workflows/keep-prisma-dependencies-updated/badge.svg)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Akeep-prisma-dependencies-updated)
-[![keep-dev-branches-in-sync-with-latest](https://github.com/prisma/prisma-examples/workflows/keep-dev-branches-in-sync-with-latest/badge.svg)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Akeep-dev-branches-in-sync-with-latest)
+[![test](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=latest)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+branch%3Alatest) [![keep-prisma-dependencies-updated](https://github.com/prisma/prisma-examples/workflows/keep-prisma-dependencies-updated/badge.svg)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Akeep-prisma-dependencies-updated) [![keep-dev-branches-in-sync-with-latest](https://github.com/prisma/prisma-examples/workflows/keep-dev-branches-in-sync-with-latest/badge.svg)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Akeep-dev-branches-in-sync-with-latest)
[View full CI status](#ci-status)
@@ -42,68 +40,69 @@ Are you missing an example? Please feel free to [open an issue](https://github.c
### Fullstack
-| Demo | Description |
-| :---------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [`rest-nextjs-api-routes`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nextjs-api-routes) | [Next.js](https://nextjs.org/) app with a REST API (using [Next.js API routes](https://nextjs.org/docs/api-routes/introduction)) |
-| [`rest-nextjs-api-routes-auth`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nextjs-api-routes-auth) | [Next.js](https://nextjs.org/) app with a REST API (using [Next.js API routes](https://nextjs.org/docs/api-routes/introduction)) and authentication (using [NextAuth.js](https://next-auth.js.org/)) |
-| [`rest-nextjs-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nextjs-express) | [Next.js](https://nextjs.org/) app with a REST API (using [Express](https://expressjs.com/)) |
-| [`rest-nuxtjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nuxtjs) | [Nuxt.js](https://nuxt.com/) app with a REST API |
-| [`graphql-nextjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nextjs) | [Next.js](https://nextjs.org/) app with a GraphQL API (using [Apollo Server](https://github.com/apollographql/apollo-server) and [GraphQL Nexus](https://github.com/graphql-nexus/nexus)) |
-| [`rest-sveltekit`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-sveltekit) | [SvelteKit](https://kit.svelte.dev/) app with a REST API |
-| [`sveltekit`](https://github.com/prisma/prisma-examples/tree/latest/typescript/sveltekit) | [SvelteKit](https://kit.svelte.dev/) app using SvelteKit's [actions](https://kit.svelte.dev/docs/form-actions) and [load](https://kit.svelte.dev/docs/form-actions#loading-data) functions | | [SvelteKit](https://kit.svelte.dev/) app using SvelteKit's [actions](https://kit.svelte.dev/docs/form-actions) and [load](https://kit.svelte.dev/docs/form-actions#loading-data) functions |
-| [`trpc-nextjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/trpc-nextjs) | [Next.js](https://nextjs.org/) app with [tRPC ](https://trpc.io/)
-| [`remix`](https://github.com/prisma/prisma-examples/tree/latest/typescript/remix) | [Remix](https://remix.run/) app |
+| Demo | Description |
+| :-- | :-- |
+| [`rest-nextjs-api-routes`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nextjs-api-routes) | Next.js app with a REST API (using [Next.js API routes](https://nextjs.org/docs/api-routes/introduction)) |
+| [`rest-nextjs-api-routes-auth`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nextjs-api-routes-auth) | Next.js app with a REST API (using [Next.js API routes](https://nextjs.org/docs/api-routes/introduction)) and authentication (using [NextAuth.js](https://next-auth.js.org/)) |
+| [`rest-nextjs-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nextjs-express) | Next.js app with a REST API (using [Express](https://expressjs.com/)) |
+| [`rest-nuxtjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nuxtjs) | Nuxt.js app with a REST API |
+| [`graphql-nextjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nextjs) | Next.js app with a GraphQL API (using [Apollo Server](https://github.com/apollographql/apollo-server) and [GraphQL Nexus](https://github.com/graphql-nexus/nexus)) |
+| [`rest-sveltekit`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-sveltekit) | SvelteKit app with a REST API |
+| [`sveltekit`](https://github.com/prisma/prisma-examples/tree/latest/typescript/sveltekit) | SvelteKit app using SvelteKit's [actions](https://kit.svelte.dev/docs/form-actions) and [load](https://kit.svelte.dev/docs/form-actions#loading-data) functions |
+| [`trpc-nextjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/trpc-nextjs) | Next.js app with [tRPC](https://trpc.io/) |
+| [`remix`](https://github.com/prisma/prisma-examples/tree/latest/typescript/remix) | Remix app |
+| [`skeet-graphql`](https://github.com/prisma/prisma-examples/tree/latest/typescript/skeet-graphql) | [Skeet](https://skeet.dev/) full-stack GraphQL app ready to deploy |
### Backend only
-| Demo | Description |
-| :------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| [`graphql-apollo-server`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nexus) | GraphQL server based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server/) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
-| [`graphql-auth`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-auth) | GraphQL server with email-password authentication & permissions |
-| [`graphql-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-sdl-first) | GraphQL server based on [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) |
-| [`graphql-subscriptions`](https://github.com/prisma/prisma-examples/tree/latest/typescript/subscriptions-pubsub) | GraphQL server with realtime subscriptions based on [`apollo-server`](https://www.apollographql.com/docs/apollo-server/) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
-| [`graphql-typegraphql`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-typegraphql) | GraphQL server based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server) and [TypeGraphQL](https://github.com/MichalLytek/type-graphql) |
-| [`graphql-typegraphql-crud`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-typegraphql-crud) | CRUD GraphQL API based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server) and [TypeGraphQL](https://github.com/MichalLytek/type-graphql) |
-| [`graphql-fastify`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-fastify) | GraphQL server based on [Fastify](https://fastify.io/), [Mercurius](https://mercurius.dev/), and the SDL-first approach of [`graphql-tools`](https://www.graphql-tools.com/docs/generate-schema/) |
+| Demo | Description |
+| :-- | :-- |
+| [`graphql-apollo-server`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nexus) | GraphQL server based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server/) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
+| [`graphql-auth`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-auth) | GraphQL server with email-password authentication & permissions |
+| [`graphql-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-sdl-first) | GraphQL server based on [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) |
+| [`graphql-subscriptions`](https://github.com/prisma/prisma-examples/tree/latest/typescript/subscriptions-pubsub) | GraphQL server with realtime subscriptions based on [`apollo-server`](https://www.apollographql.com/docs/apollo-server/) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
+| [`graphql-typegraphql`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-typegraphql) | GraphQL server based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server) and [TypeGraphQL](https://github.com/MichalLytek/type-graphql) |
+| [`graphql-typegraphql-crud`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-typegraphql-crud) | CRUD GraphQL API based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server) and [TypeGraphQL](https://github.com/MichalLytek/type-graphql) |
+| [`graphql-fastify`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-fastify) | GraphQL server based on [Fastify](https://fastify.io/), [Mercurius](https://mercurius.dev/), and the SDL-first approach of [`graphql-tools`](https://www.graphql-tools.com/docs/generate-schema/) |
| [`graphql-fastify-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-fastify-sdl-first) | GraphQL server based on [Fastify](https://fastify.io/), [Mercurius](https://mercurius.dev/), and the SDL-first approach of [`graphql-tools`](https://www.graphql-tools.com/docs/generate-schema/) |
-| [`graphql-hapi`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-hapi) | GraphQL server based on [Hapi](https://hapi.dev/) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
-| [`graphql-hapi-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-hapi-sdl-first) | GraphQL server based on [Hapi](https://hapi.dev/) and the SDL-first approach of [Apollo Server Integration for Hapi](https://www.npmjs.com/package/@as-integrations/hapi) |
-| [`graphql-nestjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nestjs) | GraphQL server based on [NestJS](https://nestjs.com/) (code-first) |
-| [`graphql-nestjs-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nestjs-sdl-first) | GraphQL server based on [NestJS](https://nestjs.com/) and the SDL-first approach of [`graphql-tools`](https://www.apollographql.com/docs/graphql-tools/) |
-| [`graphql`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql) | GraphQL server based on [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) and [Pothos](https://pothos-graphql.dev/) |
-| [`graphql-nexus`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nexus) | GraphQL server based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
-| [`grpc`](https://github.com/prisma/prisma-examples/tree/latest/typescript/grpc) | gRPC API including runnable client scripts for testing |
-| [`postgis-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/postgis-express) | Demo of spatial queries using [Postgis](http://postgis.net/) and [Express](https://expressjs.com/) |
-| [`rest-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-express) | REST API with [Express](https://expressjs.com/) |
-| [`rest-fastify`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-fastify) | REST API with [Fastify](https://www.fastify.io/) |
-| [`rest-koa`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-koa) | REST API with [Koa](https://koajs.com/) |
-| [`rest-hapi`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-hapi) | REST API with [hapi](https://hapi.dev/) |
-| [`rest-nestjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nestjs) | REST API with [NestJS](https://docs.nestjs.com/) |
-| [`script`](https://github.com/prisma/prisma-examples/tree/latest/typescript/script) | Usage of Prisma Client JS in a TypeScript script |
-| [`testing-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/testing-express) | Demo of integration tests with [Jest](https://jestjs.io/), [Supertest](https://github.com/visionmedia/supertest) and [Express](https://expressjs.com/) |
+| [`graphql-hapi`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-hapi) | GraphQL server based on [Hapi](https://hapi.dev/) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
+| [`graphql-hapi-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-hapi-sdl-first) | GraphQL server based on [Hapi](https://hapi.dev/) and the SDL-first approach of [Apollo Server Integration for Hapi](https://www.npmjs.com/package/@as-integrations/hapi) |
+| [`graphql-nestjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nestjs) | GraphQL server based on [NestJS](https://nestjs.com/) (code-first) |
+| [`graphql-nestjs-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nestjs-sdl-first) | GraphQL server based on [NestJS](https://nestjs.com/) and the SDL-first approach of [`graphql-tools`](https://www.apollographql.com/docs/graphql-tools/) |
+| [`graphql`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql) | GraphQL server based on [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) and [Pothos](https://pothos-graphql.dev/) |
+| [`graphql-nexus`](https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nexus) | GraphQL server based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server) and [Nexus Schema](https://github.com/graphql-nexus/schema) |
+| [`grpc`](https://github.com/prisma/prisma-examples/tree/latest/typescript/grpc) | gRPC API including runnable client scripts for testing |
+| [`postgis-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/postgis-express) | Demo of spatial queries using [Postgis](http://postgis.net/) and [Express](https://expressjs.com/) |
+| [`rest-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-express) | REST API with [Express](https://expressjs.com/) |
+| [`rest-fastify`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-fastify) | REST API with [Fastify](https://www.fastify.io/) |
+| [`rest-koa`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-koa) | REST API with [Koa](https://koajs.com/) |
+| [`rest-hapi`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-hapi) | REST API with [hapi](https://hapi.dev/) |
+| [`rest-nestjs`](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nestjs) | REST API with [NestJS](https://docs.nestjs.com/) |
+| [`script`](https://github.com/prisma/prisma-examples/tree/latest/typescript/script) | Usage of Prisma Client JS in a TypeScript script |
+| [`testing-express`](https://github.com/prisma/prisma-examples/tree/latest/typescript/testing-express) | Demo of integration tests with [Jest](https://jestjs.io/), [Supertest](https://github.com/visionmedia/supertest) and [Express](https://expressjs.com/) |
## JavaScript (Node.js)
### Fullstack
-| Demo | Description |
-| :-------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
+| Demo | Description |
+| :-- | :-- |
| [`rest-nextjs`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-nextjs) | [Next.js](https://nextjs.org/) app with a REST API (using [Next.js API routes](https://nextjs.org/docs/api-routes/introduction)) |
-| [`rest-nuxtjs`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-nuxtjs) | [NuxtJS](https://nuxtjs.org/) app with a REST API
-| [`rest-sveltekit`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-sveltekit) | [SvelteKit](https://kit.svelte.dev/) app with a REST API |
+| [`rest-nuxtjs`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-nuxtjs) | [NuxtJS](https://nuxtjs.org/) app with a REST API |
+| [`rest-sveltekit`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-sveltekit) | [SvelteKit](https://kit.svelte.dev/) app with a REST API |
### Backend only
-| Demo | Description |
-| :---------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- |
+| Demo | Description |
+| :-- | :-- |
| [`graphql-apollo-server`](https://github.com/prisma/prisma-examples/tree/latest/javascript/graphql-apollo-server) | GraphQL server based on [`@apollo/server`](https://www.apollographql.com/docs/apollo-server/) |
-| [`graphql-auth`](https://github.com/prisma/prisma-examples/tree/latest/javascript/graphql-auth) | GraphQL server with email-password authentication & permissions |
-| [`graphql-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/javascript/graphql-sdl-first) | GraphQL server based on [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) |
-| [`grpc`](https://github.com/prisma/prisma-examples/tree/latest/javascript/grpc) | gRPC API including runnable client scripts for testing |
-| [`rest-express`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-express) | REST API with [Express](https://expressjs.com/) |
-| [`rest-fastify`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-fastify) | REST API with [Fastify](https://www.fastify.io/) |
-| [`rest-koa`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-koa) | REST API with [Koa](https://koajs.com/) |
-| [`script`](https://github.com/prisma/prisma-examples/tree/latest/javascript/script) | Usage of Prisma Client JS in a Node.js script |
+| [`graphql-auth`](https://github.com/prisma/prisma-examples/tree/latest/javascript/graphql-auth) | GraphQL server with email-password authentication & permissions |
+| [`graphql-sdl-first`](https://github.com/prisma/prisma-examples/tree/latest/javascript/graphql-sdl-first) | GraphQL server based on [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) |
+| [`grpc`](https://github.com/prisma/prisma-examples/tree/latest/javascript/grpc) | gRPC API including runnable client scripts for testing |
+| [`rest-express`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-express) | REST API with [Express](https://expressjs.com/) |
+| [`rest-fastify`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-fastify) | REST API with [Fastify](https://www.fastify.io/) |
+| [`rest-koa`](https://github.com/prisma/prisma-examples/tree/latest/javascript/rest-koa) | REST API with [Koa](https://koajs.com/) |
+| [`script`](https://github.com/prisma/prisma-examples/tree/latest/javascript/script) | Usage of Prisma Client JS in a Node.js script |
## Deployment platforms
@@ -149,13 +148,13 @@ If you have a security issue to report, please contact us at [security@prisma.io
## CI status
-| CI Status | Branch |
-| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
-| [![test latest](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=latest)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+branch%3Alatest) | `latest` |
-| [![test dev](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=dev)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+-branch%3Apatch-dev+branch%3Adev) | `dev` |
-| [![test patch-dev](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=patch-dev)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+branch%3Apatch-dev) | `patch-dev` |
+| CI Status | Branch |
+| --- | --- |
+| [![test latest](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=latest)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+branch%3Alatest) | `latest` |
+| [![test dev](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=dev)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+-branch%3Apatch-dev+branch%3Adev) | `dev` |
+| [![test patch-dev](https://github.com/prisma/prisma-examples/workflows/test/badge.svg?branch=patch-dev)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Atest+branch%3Apatch-dev) | `patch-dev` |
-| CI Status |
-| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [![keep-prisma-dependencies-updated](https://github.com/prisma/prisma-examples/workflows/keep-prisma-dependencies-updated/badge.svg)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Akeep-prisma-dependencies-updated) |
+| CI Status |
+| --- |
+| [![keep-prisma-dependencies-updated](https://github.com/prisma/prisma-examples/workflows/keep-prisma-dependencies-updated/badge.svg)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Akeep-prisma-dependencies-updated) |
| [![keep-dev-branches-in-sync-with-latest](https://github.com/prisma/prisma-examples/workflows/keep-dev-branches-in-sync-with-latest/badge.svg)](https://github.com/prisma/prisma-examples/actions?query=workflow%3Akeep-dev-branches-in-sync-with-latest) |
diff --git a/typescript/skeet-graphql/.eslintignore b/typescript/skeet-graphql/.eslintignore
new file mode 100644
index 000000000000..b4501cc587a1
--- /dev/null
+++ b/typescript/skeet-graphql/.eslintignore
@@ -0,0 +1,8 @@
+web-build
+.next
+out
+dist
+build
+src/__generated__
+src / schema.graphql
+*.log
\ No newline at end of file
diff --git a/typescript/skeet-graphql/.eslintrc.json b/typescript/skeet-graphql/.eslintrc.json
new file mode 100644
index 000000000000..c7ecc6a69d2f
--- /dev/null
+++ b/typescript/skeet-graphql/.eslintrc.json
@@ -0,0 +1,33 @@
+{
+ "extends": [
+ "next/core-web-vitals",
+ "eslint:recommended",
+ "plugin:react-hooks/recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier"
+ ],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint", "react-hooks"],
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "rules": {
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn",
+ "@typescript-eslint/no-explicit-any": 0,
+ "@typescript-eslint/no-var-requires": 0,
+ "@typescript-eslint/no-unused-vars": 0,
+ "@typescript-eslint/no-empty-function": 0,
+ "@typescript-eslint/ban-ts-comment": [
+ "off",
+ {
+ "ts-ignore": "allow-with-description"
+ }
+ ]
+ }
+}
diff --git a/typescript/skeet-graphql/.firebaserc b/typescript/skeet-graphql/.firebaserc
new file mode 100644
index 000000000000..eae122a18245
--- /dev/null
+++ b/typescript/skeet-graphql/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "skeet-graphql"
+ }
+}
diff --git a/typescript/skeet-graphql/.gitattributes b/typescript/skeet-graphql/.gitattributes
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/typescript/skeet-graphql/.gitignore b/typescript/skeet-graphql/.gitignore
new file mode 100644
index 000000000000..9f62f900e5ec
--- /dev/null
+++ b/typescript/skeet-graphql/.gitignore
@@ -0,0 +1,61 @@
+
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/apps/*/node_modules
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+/dist
+/web-build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+
+# Gcloud key
+keyfile.json
+
+#firebase
+.firebase
+firebase-debug.log
+firestore.json
+
+graphql/.env*
+*.log
+.env
+
+#PWA
+**/public/precache.*.*.js
+**/public/sw.js
+**/public/workbox-*.js
+**/public/worker-*.js
+**/public/fallback-*.js
+**/public/precache.*.*.js.map
+**/public/sw.js.map
+**/public/workbox-*.js.map
+**/public/worker-*.js.map
+**/public/fallback-*.js
\ No newline at end of file
diff --git a/typescript/skeet-graphql/.node-version b/typescript/skeet-graphql/.node-version
new file mode 100644
index 000000000000..617bcf916bf1
--- /dev/null
+++ b/typescript/skeet-graphql/.node-version
@@ -0,0 +1 @@
+18.14.1
diff --git a/typescript/skeet-graphql/.prettierignore b/typescript/skeet-graphql/.prettierignore
new file mode 100644
index 000000000000..0b18ee2b9054
--- /dev/null
+++ b/typescript/skeet-graphql/.prettierignore
@@ -0,0 +1,8 @@
+
+.next
+out
+dist
+build
+src/__generated__
+src/schema.graphql
+web-build
diff --git a/typescript/skeet-graphql/.prettierrc b/typescript/skeet-graphql/.prettierrc
new file mode 100644
index 000000000000..a72ad62693f0
--- /dev/null
+++ b/typescript/skeet-graphql/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "plugins": ["prettier-plugin-tailwindcss"],
+ "pluginSearchDirs": false,
+ "printWidth": 80
+}
diff --git a/typescript/skeet-graphql/CODE_OF_CONDUCT.md b/typescript/skeet-graphql/CODE_OF_CONDUCT.md
new file mode 100755
index 000000000000..bfcc6f79b16a
--- /dev/null
+++ b/typescript/skeet-graphql/CODE_OF_CONDUCT.md
@@ -0,0 +1,133 @@
+
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+conduct@elsoul.nl.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/typescript/skeet-graphql/LICENSE.txt b/typescript/skeet-graphql/LICENSE.txt
new file mode 100755
index 000000000000..ab9e1d98a728
--- /dev/null
+++ b/typescript/skeet-graphql/LICENSE.txt
@@ -0,0 +1,67 @@
+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of this License; and
+You must cause any modified files to carry prominent notices stating that You changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+Copyright 2021 ELSOUL LABO B.V.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/typescript/skeet-graphql/README.md b/typescript/skeet-graphql/README.md
new file mode 100755
index 000000000000..d64c50fd8942
--- /dev/null
+++ b/typescript/skeet-graphql/README.md
@@ -0,0 +1,95 @@
+![Skeet Next.js + GraphQL Template](https://storage.googleapis.com/skeet-assets/imgs/frontend/skeet-next-graphql.png)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Skeet GraphQL Boilerplate
+
+- [Prisma - ORM](https://www.prisma.io/)
+- [Nexus Prisma - GraphQL plugin for Prisma](https://graphql-nexus.github.io/nexus-prisma)
+- [Apollo - GraphQL Server](https://www.apollographql.com/)
+- [Express - HTTP API](https://expressjs.com/)
+- [PostgreSQL - Relational Database](https://www.postgresql.org/)
+- [Jest - Testing framework](https://jestjs.io/)
+- [TypeScript - Type Check](https://www.typescriptlang.org/)
+- [ESLint - Linter](https://eslint.org/)
+- [Prettier - Formatter](https://prettier.io/)
+- [Firebase - Serverless Platform](https://firebase.google.com/)
+- [Google Cloud - Cloud Platform](https://cloud.google.com/)
+- [Next.js - SSG Framework](https://nextjs.org/)
+- [React - UI Framework](https://reactjs.org/)
+- [Relay - GraphQL Client](https://relay.dev/)
+- [Recoil - State Management](https://recoiljs.org/)
+- [Tailwind - CSS Framework](https://tailwindcss.com/)
+- [Next i18next - i18n Translation](https://github.com/isaachinman/next-i18next)
+
+## What's Skeet?
+
+TypeScript Serverless Framework 'Skeet'.
+
+The Skeet project was launched with the goal of reducing software development, operation, and maintenance costs.
+
+Build Serverless Apps faster.
+Powered by TypeScript GraphQL, Prisma, Jest, Prettier, and Google Cloud.
+
+## Dependency
+
+- [TypeScript](https://www.typescriptlang.org/)
+- [Node](https://nodejs.org/)
+- [Yarn](https://yarnpkg.com/)
+- [Google SDK](https://cloud.google.com/sdk/docs)
+- [Docker](https://www.docker.com/)
+- [Watchman](https://facebook.github.io/watchman/docs/install)
+
+## Usage
+
+## yarn install
+
+```bash
+$ npm i -g @skeet-framework/cli
+```
+
+### Run local
+
+After packages installed:
+
+```bash
+$ skeet yarn install
+$ skeet docker psql
+$ skeet db generate
+$ skeet db deploy
+$ skeet s
+```
+
+Now you can access;
+
+`http://localhost:3000/graphql`
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/elsoul/skeet-graphql This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
+
+## License
+
+The package is available as open source under the terms of the [Apache-2.0 License](https://www.apache.org/licenses/LICENSE-2.0).
+
+## Code of Conduct
+
+Everyone interacting in the SKEET project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/elsoul/skeet-graphql/blob/master/CODE_OF_CONDUCT.md).
diff --git a/typescript/skeet-graphql/articles/doc/en/general/motivation.md b/typescript/skeet-graphql/articles/doc/en/general/motivation.md
new file mode 100644
index 000000000000..f0f9b9360aad
--- /dev/null
+++ b/typescript/skeet-graphql/articles/doc/en/general/motivation.md
@@ -0,0 +1,45 @@
+---
+id: general-motivation
+title: Motivation
+description: Motivation for the development of Skeet, an open-source full-stack serverless framework
+---
+
+## Reduce app development and maintenance costs
+
+Many points can be improved through application development in our lives and society.
+
+However, to create and publish an application, a wide range of knowledge and skills, from server infrastructure to applications, is required, so many teams currently need help.
+
+It is always challenging to achieve both rapid development and maintainability. On top of that, it is necessary to solve complex problems during scale-up, so it takes a lot of time to obtain a reproducible development environment.
+
+Since the cost of maintaining an app that has been successfully released is steadily increasing, the development site must take on the challenge of adding and improving functions while maintaining the status quo is difficult.
+
+As the importance of apps increases in our lives and society, the shortage of IT resources is still accelerating, and development resources need to be increased at almost all sites.
+
+We want to address this issue by lowering application development and maintenance costs.
+
+Skeet allows you to get your app up and running quickly and maintain it for the long term at a low cost.
+
+## Serverless architecture powered by Firebase
+
+Firebase is an app development platform that helps you build and scale the apps and games your users love. It is a service highly trusted by many companies worldwide, supported by Google's infrastructure.
+
+Firebase provides reliable products and solutions for the entire app lifecycle. Everyone can use Firebase products to solve complex problems and optimize your app's experience. Firebase is powered by Google Cloud and lets you scale your apps to billions of users.
+
+Use cases are not limited to application development but also include API servers, scheduled task execution, function execution by event handlers, stream data pipelines, etc., and can be used in a wide range of applications, from new projects to partial applications to existing projects.
+
+## Develop and manage eco-friendly, high-performance distributed systems at low cost
+
+You can combine Firebase products to build an eco-friendly, high-performance distributed system. It utilizes server resources only as needed, allowing for a very environmentally and economically friendly design.
+
+The design and material costs of "predicting access loads in advance (design costs) and increasing server resources (material costs)" are no longer necessary, and concentrating more on things and developing is possible.
+
+Furthermore, maintenance costs are significantly reduced. Post-release apps also automatically scale up computing resources to match user usage patterns. You don't need to worry about credentials, configuration, provisioning new servers, or decommissioning old servers.
+
+Skeet is an open-source, full-stack serverless app development solution that makes Firebase and Google Cloud product combinations easier and easier to use.
+
+Skeet CLI is not only for setting, deploying, and managing Firebase products but also for application development, such as VPN network settings, domain and name server settings, load balancer management security settings using Cloud Armor, and CI & CD using GitHub Actions. We provide that with a single command. Skeet supports everything from backend construction to front-end web, iOS, and Android development.
+
+In addition, for more scalable and robust application development, we adopt tools useful for development, such as TypeScript, Jest, ESLint & Prettier by default. TypeScript is mainly used for ease of application creation, but it is also possible to build applications that partially utilize machine learning using Python.
+
+By using Skeet, you can significantly reduce app development and maintenance costs and realize more plans.
diff --git a/typescript/skeet-graphql/articles/doc/en/general/quickstart.md b/typescript/skeet-graphql/articles/doc/en/general/quickstart.md
new file mode 100644
index 000000000000..002b76dca2d2
--- /dev/null
+++ b/typescript/skeet-graphql/articles/doc/en/general/quickstart.md
@@ -0,0 +1,258 @@
+---
+id: backend-quickstart
+title: Quickstart
+description: Describes the setup for getting started with the Skeet framework.
+---
+
+## 💃 What's Skeet? 🕺
+
+⚡️ Reduce App Development and Maintenance Costs ⚡️
+
+Skeet is an Open-Source Full-stack Serverless Application Framework.
+
+Skeet was born to reduce the cost of software development and operation.
+
+Start developing and deploying serverless apps quickly.
+
+Get ready to use scalable Cloud Firestore and Cloud Functions securely right away.
+
+![https://storage.googleapis.com/skeet-assets/animation/skeet-cli-create-latest.gif](https://storage.googleapis.com/skeet-assets/animation/skeet-cli-create-latest.gif)
+
+## 🧪 Dependency 🧪
+
+- [TypeScript](https://www.typescriptlang.org/) ^5.0.0
+- [Node.js](https://nodejs.org/ja/) ^18.16.0
+- [Yarn](https://yarnpkg.com/) ^1.22.19
+- [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) ^430.0.0
+- [Firebase CLI](https://firebase.google.com/docs/cli) ^12.0.0
+- [GitHub CLI](https://cli.github.com/) ^2.29.0
+- [Java](https://www.java.com/en/download/)
+
+※ We don't write Java but we need it for mobile apps working
+
+## 📗 Usage 📗
+
+### ① Install Skeet/Firebase CLI
+
+```bash
+$ npm i -g @skeet-framework/cli
+$ npm install -g firebase-tools
+```
+
+### ② Create Skeet App
+
+```bash
+$ skeet create
+```
+
+![Skeet Create Select Template](/doc-images/cli/SkeetCreateSelectTemplate.png)
+
+You can choose a template for the frontend.
+
+- [Next.js (React)](https://nextjs.org/)
+- [Expo (React Native)](https://expo.dev/)
+
+※ This tutorial uses the Expo version, but you can use the same procedure even using the Next.js version.
+
+### ③ Run Skeet App
+
+```bash
+$ cd
+$ skeet s
+```
+
+Now you have both frontend and backend running locally ⭐️
+
+📲 Frontend(Next.js) - [http://localhost:4200/](http://localhost:4200/)
+
+📲 Frontend(Expo) - [http://localhost:19006/](http://localhost:19006/)
+
+💻 Firebase Emulator - [http://localhost:4000/](http://localhost:4000/)
+
+**⚠️ You need to finish _Activate Skeet ChatApp_ step to fully use default Skeet App ⚠️**
+
+## 🤖 Activate Skeet ChatApp 🤖
+
+### ① Create Googel Cloud Project
+
+Create Google Cloud Project
+
+- [https://console.cloud.google.com/projectcreate](https://console.cloud.google.com/projectcreate)
+
+### ② Add Firebase Project
+
+Add Firebase Project
+
+- [https://console.firebase.google.com/](https://console.firebase.google.com/)
+
+### ③ Activate Firebase Build
+
+#### - Activate Firebase Authentication
+
+- Activate Firebase Authentication
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-fb-auth.png)
+
+- Activate Google Sign-in
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/enable-fb-auth.png)
+
+#### - Activate Firebase Firestore
+
+- Activate Firestore
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-fb-firestore.png)
+
+- Select Native Mode
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-env-firestore.png)
+
+- Select Region
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-region-firestore.png)
+
+#### - Firebase Storage
+
+- Activate Firebase Storage
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-fb-storage.png)
+
+- Select Native Mode
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-env-storage.png)
+
+- Select Region
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-region-storage.png)
+
+### ④ Skeet init to setup project
+
+Run _skeet init_ command and select your GCP Project ID and Regions to setup.
+
+Then, please visit the URL to authenticate your Firebase account.
+
+```bash
+$ skeet init --only-dev
+? What's your GCP Project ID skeet-demo
+? Select Regions to deploy
+ europe-west1
+ europe-west2
+ europe-west3
+❯ europe-west6
+ northamerica-northeast1
+ southamerica-east1
+ us-central1
+
+Visit this URL on this device to log in:
+
+https://accounts.google.com/o/oauth2/auth?project...
+
+Waiting for authentication...
+```
+
+### ⑤ How to setup Secret Key
+
+#### - Upgrade to Firebase Blaze Plan
+
+Skeet Framework uses [Cloud Secret Manager](https://firebase.google.com/docs/functions/config-env?hl=en&gen=2nd) environment variables to manage sensitive information such as API keys.
+
+This command requires a Firebase Blaze or higher plan.
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/firebase-plan-en.png)
+
+From the Firebase console's bottom left menu, select _Upgrade_.
+
+- [Firebase Console](https://console.firebase.google.com/u/0/project/_/usage/details)
+
+#### - Cloud Usage of Skeet Framework
+
+Skeet Framework requires a Firebase Blaze plan or higher.
+
+Google Cloud Free Program should cover the usage fee for the development environment.
+
+The Google Cloud Free Tier has two parts:
+
+- A 90-day free trial with a $300 credit to use with any Google Cloud services.
+- Always Free, which provides limited access to many common Google Cloud resources, free of charge.
+
+[Free cloud features and trial offer](https://cloud.google.com/free/docs/free-cloud-features)
+
+[Firabse Blaze Pricing Plans](https://firebase.google.com/pricing#blaze-calculator)
+
+**⚠️ We also recommend setting things like budget alerts to avoid unexpected charges. ⚠️**
+
+- [Avoid surprise bills](https://firebase.google.com/docs/projects/billing/avoid-surprise-bills)
+
+#### - Set Secret Key in Cloud Secret Manager
+
+using the _skeet add secret _ command
+
+Set the OpenAI API key as an environment variable.
+
+```bash
+$ skeet add secret CHAT_GPT_ORG
+? Enter value for CHAT_GPT_ORG:
+```
+
+Set CHAT_GPT_KEY as well.
+
+```bash
+$ skeet add secret CHAT_GPT_KEY
+? Enter value for CHAT_GPT_KEY:
+```
+
+You can also write it in _functions/openai/.env_ to try it easily,
+This method does not translate to production environments.
+
+#### - Create OpenAI API Key
+
+- [https://beta.openai.com/](https://beta.openai.com/)
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/openai-api-key.png)
+
+📕 [OpenAI API Document](https://platform.openai.com/docs/introduction)
+
+Now you are ready to use Skeet ChatApp 🎉
+
+## 📱 User Login Auth 📱
+
+```bash
+$ skeet s
+```
+
+Run Skeet App locally and access to
+
+[http://localhost:19006/register](http://localhost:19006/register)
+
+Let's create a new user account with your email address and password.
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/user-register.png)
+
+After registration, you will see the console log like below.
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/email-validation.png)
+
+Click the link in the console log to verify your email address.
+
+```bash
+To verify the email address epics.dev@gmail.com, follow this link:
+```
+
+Successfully verified your email address.
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/email-validation-clicked.png)
+
+## ✉️ Create AI Chat Room ✉️
+
+After login, access this page to create a chat room.
+
+[http://localhost:19006/user/open-ai-chat](http://localhost:19006/user/open-ai-chat)
+
+Let's create a chat room with the following settings.
+
+OpenAI Chat Room Settings
+
+| item | description | type |
+| ---------------- | --------------------------------- | ------------------- |
+| Model | Select OpenAI API's Model | gpt3.5-turbo / gpt4 |
+| Max Tokens | Set OpenAI API's Max Tokens | number |
+| Temperature | Set OpenAI API's Temperature | number |
+| System Charactor | Set OpenAI API's System Charactor | string |
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-chatroom.png)
+
+Now you are all set 🎉
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/skeet-chat-stream.gif)
diff --git a/typescript/skeet-graphql/articles/doc/en/general/readme.md b/typescript/skeet-graphql/articles/doc/en/general/readme.md
new file mode 100644
index 000000000000..8d5c74fe37e8
--- /dev/null
+++ b/typescript/skeet-graphql/articles/doc/en/general/readme.md
@@ -0,0 +1,75 @@
+---
+id: readme
+title: Readme
+description: Skeet Next.js (React) テンプレート README
+---
+
+![Skeet Next.js Template](https://storage.googleapis.com/skeet-assets/imgs/samples/WebAppBoilerplate.png)
+
+## Skeet App Next.js テンプレート
+
+Next.js (React) 環境 for Skeet Framework
+
+[GitHub - Skeet App Next.js Template](https://github.com/elsoul/skeet-next)
+
+## 心がけ
+
+- 迅速な開発
+- ハイパフォーマンス
+- グローバルスケール(多言語化含む)
+- メンテナンスしやすいコードベース
+- SEO に強い
+
+## 技術選定
+
+- [x] [Next.js - SSG Framework](https://nextjs.org/)
+- [x] [React - UI Framework](https://reactjs.org/)
+- [x] [TypeScript - Type Check](https://www.typescriptlang.org/)
+- [x] [ESLint - Linter](https://eslint.org/)
+- [x] [Prettier - Formatter](https://prettier.io/)
+- [x] [Recoil - State Management](https://recoiljs.org/)
+- [x] [Next i18next - i18n Translation](https://github.com/isaachinman/next-i18next)
+- [x] [Firebase - Hosting & Analytics](https://firebase.google.com/)
+- [x] [Tailwind - CSS Framework](https://tailwindcss.com/)
+
+## クイックスタート
+
+```bash
+$ npm i -g firebase-tools
+$ npm i -g @skeet-framework/cli
+```
+
+```bash
+$ skeet create
+```
+
+```bash
+$ cd
+$ skeet s
+```
+
+開発中コンソールにて Auth を利用した確認も可能です:
+
+```bash
+$ skeet login
+$ export ACCESS_TOKEN=
+```
+
+**※ OpenAI API key が必要です**
+
+_./functions/openai/.env_
+
+```bash
+CHAT_GPT_KEY=your-key
+CHAT_GPT_ORG=your-org
+```
+
+テストコマンド:
+
+```bash
+$ skeet test
+```
+
+Firebase Emulator: http://localhost:4000
+
+Front-end App: http://localhost:4200
diff --git a/typescript/skeet-graphql/articles/doc/ja/general/motivation.md b/typescript/skeet-graphql/articles/doc/ja/general/motivation.md
new file mode 100644
index 000000000000..ae30c47086a7
--- /dev/null
+++ b/typescript/skeet-graphql/articles/doc/ja/general/motivation.md
@@ -0,0 +1,45 @@
+---
+id: general-motivation
+title: モチベーション
+description: オープンソースのフルスタックサーバーレスフレームワーク Skeet 開発のモチベーション
+---
+
+## アプリの開発・メンテナンスコストを下げる
+
+アプリ開発によって改善できるポイントは生活や社会の中に溢れています。
+
+しかし実際にアプリを作って公開しようとすると、サーバーインフラからアプリケーションまで広範囲に渡る知識と技術が必要になってくるため、多くのチームが苦戦を強いられているのが現状です。
+
+迅速な開発とメンテナンス性の両立はいつも難しく、その上でスケールアップ時の複雑な事象を解決する必要があるため、再現性のある開発環境を手に入れるまでには多くの時間がかかります。
+
+無事リリースを迎えたアプリのメンテナンスコストはどんどん大きくなっていくため、開発現場は現状維持すら難しい中で機能の追加や向上にチャレンジしなければなりません。
+
+生活や社会においてアプリの重要度は増していく中、今もなお IT リソース不足は加速しており、ほぼすべての現場で開発リソースが足りていません。
+
+私達はアプリケーションの開発及びメンテナンスコストを下げることでこの問題に対処したいと考えています。
+
+Skeet は素早くアプリを立ち上げ、少ないコストで長期的にメンテナンスしていくことを可能にします。
+
+## Firebase を活用したサーバーレスアーキテクチャ
+
+Firebase は、ユーザーに愛されるアプリやゲームの構築と拡大を支援するアプリ開発プラットフォームです。Google のインフラが支える、世界中の多くの企業から高い信頼を得ているサービスです。
+
+アプリのライフサイクル全体を通して信頼できるプロダクトとソリューションが提供されています。Firebase プロダクトを組み合わせて使用すれば、複雑な課題を解決したり、アプリ操作を最適化したりできます。Firebase は Google Cloud を基盤としており、アプリを数十億人規模のユーザーに拡大できます。
+
+ユースケースはアプリ開発だけでなく、API サーバー、スケジュールタスクの実行、イベントハンドラーによる関数の実行、ストリームデータパイプライン等多岐にわたり、新規プロジェクトから既存プロジェクトへの部分適用まで幅広い利用が可能です。
+
+## エコでハイパフォーマンスな分散システムをローコストで開発・管理
+
+Firebase プロダクトを組み合わせて利用すれば、エコでハイパフォーマンスな分散システムを構築することができます。それは常に必要な分だけのサーバーリソースを利用するため、環境的にも経済的にも非常に優しい設計を可能にします。
+
+「事前にアクセス負荷を予測し(設計コスト)、サーバーリソースを増強しておく(物理コスト)」という設計及び物理コストは不要になり、より物事に集中して開発を行うことが可能です。
+
+さらにメンテナンスコストも大幅に削減します。リリース後のアプリもユーザーの使用パターンに合わせてコンピューティングリソースが自動的にスケールアップされます。認証情報、サーバー構成、新規サーバーのプロビジョニング、古いサーバーのデコミッションを気にする必要はありません。
+
+Skeet は Firebase 及び Google Cloud プロダクトの組み合わせをより簡単に使いやすくする、オープンソースのフルスタックサーバーレスアプリ開発ソリューションです。
+
+Skeet CLI は Firebase プロダクトの設定・デプロイ・管理だけでなく、 VPN ネットワークの設定、ドメインやネームサーバー設定、ロードバランサーの管理、Cloud Armor によるセキュリティの設定、GitHub Actions による CI & CD など、実際にアプリを開発する上で必要な多くのことをワンコマンドで提供しています。Skeet はバックエンドの構築から、Web・iOS・Android のフロントエンド開発まで対応しています。
+
+また、よりスケールする堅牢なアプリ開発のため、TypeScript や Jest、ESLint & Prettier 等、開発に役立つツール群をデフォルトで採用しています。アプリの作りやすさからメインは TypeScript を利用していますが、部分的に Python を利用した機械学習を活用するアプリを構築することも可能になっています。
+
+Skeet を利用することにより、アプリの開発コストとメンテナンスコストを大幅に削減し、あなたはもっと多くのプランを実現できます。
diff --git a/typescript/skeet-graphql/articles/doc/ja/general/quickstart.md b/typescript/skeet-graphql/articles/doc/ja/general/quickstart.md
new file mode 100644
index 000000000000..2cfd36076fa9
--- /dev/null
+++ b/typescript/skeet-graphql/articles/doc/ja/general/quickstart.md
@@ -0,0 +1,259 @@
+---
+id: backend-quickstart
+title: クイックスタート
+description: Skeet フレームワークを使い始めるための設定について説明します。
+---
+
+## 🕺 Skeet とは? 💃
+
+オープンソースのフルスタックサーバーレスアプリケーションフレームワーク 'Skeet'。
+
+Skeet はソフトウェア開発・運用のコストを下げるために生まれました。
+
+サーバーレスアプリをすぐに開発スタート、そしてデプロイ。
+
+スケーラブルな Cloud Firestore、Cloud Functions を今すぐ安全に使い始める準備ができています。
+
+![https://storage.googleapis.com/skeet-assets/animation/skeet-cli-create-latest.gif](https://storage.googleapis.com/skeet-assets/animation/skeet-cli-create-latest.gif)
+
+## 🧪 依存パッケージ 🧪
+
+- [TypeScript](https://www.typescriptlang.org/) 5.0.4 以上
+- [Node.js](https://nodejs.org/ja/) 18.16.0 以上
+- [Yarn](https://yarnpkg.com/) 1.22.19 以上
+- [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) 430.0.0 以上
+- [Firebase CLI](https://firebase.google.com/docs/cli) 12.0.1 以上
+- [GitHub CLI](https://cli.github.com/) 2.29.0 以上
+- [Java](https://www.java.com/en/download/)
+
+※ Skeet において Java を書くことはありませんが、モバイルアプリを動かすために必要です
+
+## 📗 使い方 📗
+
+### ① パッケージのインストール
+
+```bash
+$ npm i -g @skeet-framework/cli
+$ npm install -g firebase-tools
+```
+
+### ② Skeet アプリの作成
+
+```bash
+$ skeet create
+```
+
+![Skeet Create Select Template](/doc-images/cli/SkeetCreateSelectTemplate.png)
+
+フロントエンドのテンプレートを選択できます。
+
+- [Next.js (React)](https://nextjs.org/)
+- [Expo (React Native)](https://expo.dev/)
+
+※ 本チュートリアルでは Expo 版を利用していますが、Next.js 版を利用しても同じ手順で利用可能です。
+
+### ③ ローカルで起動
+
+```bash
+$ cd
+$ skeet s
+```
+
+Skeet App フロントエンドと Firebase エミュレーターが起動します。
+
+📲 Frontend(Next.js) - [http://localhost:4200/](http://localhost:4200/)
+
+📲 Frontend(Expo) - [http://localhost:19006/](http://localhost:19006/)
+
+💻 Firebase Emulator - [http://localhost:4000/](http://localhost:4000/)
+
+** ⚠️ Skeet App を完全に使用するには、_アクティベート Skeet ChatApp_ ステップを完了する必要があります ⚠️ **
+
+## 🤖 アクティベート Skeet ChatApp 🤖
+
+### ① Googel Cloud Project の作成
+
+Create Google Cloud Project
+
+- [https://console.cloud.google.com/projectcreate](https://console.cloud.google.com/projectcreate)
+
+### ② Firebase Project の追加
+
+Add Firebase Project
+
+- [https://console.firebase.google.com/](https://console.firebase.google.com/)
+
+### ③ Firebase ビルドの有効化
+
+以下の3つの Firebase ビルドを有効化してください。
+
+#### - Firebase 認証
+
+- Firebase Authentication の有効化
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-fb-auth.png)
+
+- Google ログインの有効化
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/enable-fb-auth.png)
+
+#### - Firebase Firestore
+
+- Firestore の有効化
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-fb-firestore.png)
+
+- 環境を選択
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-env-firestore.png)
+
+- リージョンを選択
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-region-firestore.png)
+
+#### - Firebase Storage
+
+- Firebase Storage の有効化
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-fb-storage.png)
+
+- 環境を選択
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-env-storage.png)
+
+- リージョンを選択
+ ![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/select-region-storage.png)
+
+### ④ Skeet init コマンドの実行
+
+_skeet init_ コマンドに _--only-dev_ オプションを付けて実行し、
+プロジェクト ID と リージョンを選択してください。
+そして、表示された URL にアクセスし、Firebase アカウントへログインします。
+
+```bash
+$ skeet init --only-dev
+? What's your GCP Project ID skeet-demo
+? Select Regions to deploy
+ europe-west1
+ europe-west2
+ europe-west3
+❯ europe-west6
+ northamerica-northeast1
+ southamerica-east1
+ us-central1
+
+Visit this URL on this device to log in:
+
+https://accounts.google.com/o/oauth2/auth?project...
+
+Waiting for authentication...
+```
+
+### ⑤ 環境変数の設定方法
+
+#### - Firebase Blaze プランへのアップグレード
+
+Skeet Framework では環境変数を [Cloud Secret Manager](https://firebase.google.com/docs/functions/config-env?hl=ja&gen=2nd) 使って API キーなどの機密情報を管理します。
+
+このコマンドを利用するには、Firebase Blaze 以上のプランが必要です。
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/firebase-plan.png)
+
+Firebase コンソールの左下のメニューから、_アップグレード_ を選択します。
+
+- [Firebase コンソール](https://console.firebase.google.com/u/0/project/_/usage/details)
+
+#### - Skeet Framework のクラウド使用料について
+
+Skeet Framework は Firebase Blaze プラン以上のプランが必要ですが、
+通常、開発環境への使用料は以下の無料枠内で収まります。
+
+Google Cloud の無料枠には 2 つの部分があります
+
+- 90 日間の無料トライアル。Google Cloud サービスで使用できる 300 ドルのクレジットが付いています。
+- Always Free は、多くの一般的な Google Cloud リソースへの制限付きアクセスを無料で提供します。
+
+[Google Cloud の無料プログラム](https://cloud.google.com/free/docs/free-cloud-features?hl=ja)
+
+[Firabse Blaze プランの料金](https://firebase.google.com/pricing?hl=ja#blaze-calculator)
+
+**⚠️ また、想定外の請求を回避するために、予算のアラートなどを設定することをおすすめします。 ⚠️**
+
+- [想定外の請求を回避する](https://firebase.google.com/docs/projects/billing/avoid-surprise-bills)
+
+#### - シークレットキーの設定
+
+_skeet add secret _ コマンドを使って
+
+OpenAI の API キーを環境変数に設定します。
+
+```bash
+$ skeet add secret CHAT_GPT_ORG
+? Enter value for CHAT_GPT_ORG:
+```
+
+同様に CHAT_GPT_KEY も設定します。
+
+```bash
+$ skeet add secret CHAT_GPT_KEY
+? Enter value for CHAT_GPT_KEY:
+```
+
+また、簡易的に試すには、_functions/openai/.env_ に記述することもできますが、
+この方法は、本番環境には反映されません。
+
+#### - OpenAI の API Key を作成・取得
+
+- [OpenAI API](https://beta.openai.com/docs/api-reference/introduction)
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/openai-api-key.png)
+
+📕 [OpenAI API Document](https://platform.openai.com/docs/introduction)
+
+これで Skeet App を使う準備ができました 🎉
+
+## 📱 ユーザー登録・ログイン認証 📱
+
+```bash
+$ skeet s
+```
+
+ローカルで skeetApp を起動している状態で、
+
+[http://localhost:19006/register](http://localhost:19006/register)
+
+にアクセスしてください。
+
+メールアドレスとパスワードを入力してユーザー登録を行います。
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/user-register.png)
+
+作成が成功すると、コンソールログに以下のようなメッセージが表示されます。
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/email-validation.png)
+
+リンクをクリックし、メールアドレスの認証を行ってください。
+
+```bash
+To verify the email address epics.dev@gmail.com, follow this link:
+```
+
+成功すると、リンク先のページに以下のようなメッセージが表示されます。
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/email-validation-clicked.png)
+
+## ✉️ OpenAI チャットルームの作成 ✉️
+
+ログイン後、[http://localhost:19006/user/open-ai-chat](http://localhost:19006/user/open-ai-chat) にアクセスしてください。
+
+そして、チャットルームを作成します。
+
+以下の設定を選択して、チャットルームを作成してください。
+
+チャットルームの設定
+
+| 項目名 | 説明 | 型 |
+| ---------------- | --------------------------------------------- | ------------------- |
+| Model | OpenAI API のモデルを選択します。 | gpt3.5-turbo / gpt4 |
+| Max Tokens | OpenAI API の Max Tokens を設定します。 | number |
+| Temperature | OpenAI API の Temperature を設定します。 | number |
+| System Charactor | OpenAI API の System Charactor を設定します。 | string |
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/create-chatroom.png)
+
+これで、チャットルームが使えるようになりました 🎉
+
+![画像](https://storage.googleapis.com/skeet-assets/imgs/backend/skeet-chat-stream.gif)
diff --git a/typescript/skeet-graphql/articles/doc/ja/general/readme.md b/typescript/skeet-graphql/articles/doc/ja/general/readme.md
new file mode 100644
index 000000000000..d3fe8ee0d329
--- /dev/null
+++ b/typescript/skeet-graphql/articles/doc/ja/general/readme.md
@@ -0,0 +1,75 @@
+---
+id: readme
+title: Readme
+description: Skeet Next.js (React) Template README
+---
+
+![Skeet Next.js Template](https://storage.googleapis.com/skeet-assets/imgs/samples/WebAppBoilerplate.png)
+
+## Skeet App Next.js Template
+
+Next.js (React) App Environment for Skeet Framework
+
+[GitHub - Skeet App Next.js Template](https://github.com/elsoul/skeet-next)
+
+## Aiming to
+
+- Fast Development
+- High Performance
+- Global Scale
+- Maintainable Code
+- Strong SEO
+
+## Summary
+
+- [x] [Next.js - SSG Framework](https://nextjs.org/)
+- [x] [React - UI Framework](https://reactjs.org/)
+- [x] [TypeScript - Type Check](https://www.typescriptlang.org/)
+- [x] [ESLint - Linter](https://eslint.org/)
+- [x] [Prettier - Formatter](https://prettier.io/)
+- [x] [Recoil - State Management](https://recoiljs.org/)
+- [x] [Next i18next - i18n Translation](https://github.com/isaachinman/next-i18next)
+- [x] [Firebase](https://firebase.google.com/)
+- [x] [Tailwind - CSS Framework](https://tailwindcss.com/)
+
+## Usage
+
+```bash
+$ npm i -g firebase-tools
+$ npm i -g @skeet-framework/cli
+```
+
+```bash
+$ skeet create
+```
+
+```bash
+$ cd
+$ skeet s
+```
+
+Open a new terminal and run:
+
+```bash
+$ skeet login
+$ export ACCESS_TOKEN=
+```
+
+**※ You need OpenAI API key to get success for default test.**
+
+_./functions/openai/.env_
+
+```bash
+CHAT_GPT_KEY=your-key
+CHAT_GPT_ORG=your-org
+```
+
+Test your app:
+
+```bash
+$ skeet test
+```
+
+Open Firebase Emulator: http://localhost:4000
+
+Open Front-end App: http://localhost:4200
diff --git a/typescript/skeet-graphql/articles/legal/en/privacy-policy.md b/typescript/skeet-graphql/articles/legal/en/privacy-policy.md
new file mode 100644
index 000000000000..fcc52aa4978d
--- /dev/null
+++ b/typescript/skeet-graphql/articles/legal/en/privacy-policy.md
@@ -0,0 +1,139 @@
+---
+id: privacy-policy
+title: Privacy policy
+description: Privacy Policy
+---
+
+## A. Introduction
+
+1. The privacy of our users is very important to us and we are committed to protecting it. This policy describes how we handle your personal information.
+2. By agreeing to use cookies in accordance with the terms of this policy the first time you visit this website, you consent to the use of cookies each time you visit the installation.
+
+## B. Acknowledgments
+
+This document was created using the SEQ Legal (seqlegal.com) template and modified by [ELSOUL LABO (labo.elsoul.nl).](http://labo.elsoul.nl)
+
+## C. Collection of personal information
+
+The following types of personal information may be collected, stored and used.
+
+1. Information about your computer, such as IP address, geographic location, browser type and version, operating system, and more.
+2. Information about accessing and using this website, such as source, time of visit, page views, and website navigation paths.
+3. Information such as your e-mail address that you enter when registering on this site.
+4. Information to be entered when creating a profile on this site, such as name, profile picture, gender, date of birth, dating status such as married / unmarried, interests and hobbies, educational background, work history.
+5. Information such as the name and email address you enter to subscribe to emails and newsletters.
+6. Information to enter while using the service on this site.
+7. Information generated while using this site. Includes information such as when, how often, and under what circumstances.
+8. Information about what you purchased, the services you used, or the transactions you made through this site. Includes name, address, phone number, email address and credit card details.
+9. Information that customers post on this site for the purpose of publishing it on the Internet. Includes username, profile picture, and posted content.
+10. Information contained in communications you send to us via email or this site. Includes communication content and metadata.
+11. Other personal information you send to us.
+
+Before you disclose the personal information of another person to us, you must obtain that person's consent for both the disclosure and processing of that personal information in accordance with this policy.
+
+## D. Use of personal information
+
+Personal information sent to us through this site will be used for the purposes specified in this policy or related pages of the website. We may use your personal information for the following purposes.
+
+1. For management of this site and management of business.
+2. To personalize our site for you.
+3. To enable the use of services available on this site.
+4. To send products purchased from this site.
+5. To provide services purchased through this site.
+6. To send statements, invoices, payment notices and accept payments.
+7. To send communications for non-marketing purposes.
+8. To send email notifications requested by you.
+9. If you request, we will send you a newsletter by email (you can unsubscribe at any time when you no longer need the newsletter).
+10. To send marketing related to our business or the business of a carefully selected third party that you may be interested in by mail or, if you specifically agree to this, by email or similar technology. (Please contact us whenever you no longer need marketing communications).
+11. To provide statistical information about users of this site to third-party businesses (however, third-party businesses cannot identify users from that information).
+12. To respond to inquiries and complaints from customers related to this site.
+13. To keep our site safe and prevent fraud.
+14. To verify compliance with the Terms of Service governing the use of this site (including monitoring private messages sent through our private messaging services).
+15. Other uses.
+
+Privacy settings can be used to limit the disclosure of information on this site and can be adjusted using the privacy controls of the website. We will not provide your personal information to third parties or other third parties for direct marketing without your explicit consent.
+
+## E. Disclosure of personal information
+
+We may disclose personal information to employees, officers, insurance companies, professional advisors, agents, suppliers, or subcontractors if reasonably necessary for the purposes described in this policy. We may disclose personal information to members of the Group (that is, subsidiaries, ultimate holding companies, and all of them) if reasonably necessary for the purposes described in this policy.
+
+1. To the extent required by law
+2. If related to ongoing or future legal proceedings
+3. To establish, exercise, or defend our legal rights (including providing information to others to prevent fraud and reduce credit risk).
+4. To purchasers (or prospective buyers) of businesses or assets that we sell (or are considering selling)
+5. In our reasonable opinion, we reasonably believe that such courts or authorities may apply to the court or other competent authority to disclose their personal information if it may reasonably decide to disclose it. To the person
+
+We may disclose personal information.
+
+We will not provide your personal information to third parties except as provided in this policy.
+
+## F. International data communication
+
+1. The information we collect may be stored, processed and transferred in any of the countries in which we operate in order to make the information available in accordance with this policy.
+2. The information we collect may be transferred to the following countries that do not have data protection laws equivalent to those enforced in European economies: United States, Russia, Japan, China, India.
+3. Personal information published on this site or personal information sent for publication on this site may be available worldwide via the Internet. We cannot prevent the use or misuse of such information by others.
+
+## G. Retention of personal information
+
+1. Section G sets out data retention policies and procedures designed to ensure compliance with our legal obligations regarding the retention and deletion of personal information.
+2. Personal information that we process for any purpose will not be retained for longer than that purpose or the period required for those purposes.
+3. We generally remove personal information that falls into the categories specified below at the dates and times specified below without violating Section G2. 1. Personal information will be deleted after half a year.
+4. Notwithstanding the other provisions of Section G
+5. To the extent required by law
+6. If the document appears to be related to ongoing or future legal proceedings, and
+7. To establish, exercise or defend our legal rights (including providing information to others to prevent fraud and reduce credit risk). We hold documents containing personal information (including electronic documents).
+
+## H. Security of personal information
+
+1. We will take reasonable technical and systematic precautions to prevent the loss, misuse, or falsification of your personal information.
+2. All personal information provided by you is stored on a secure (password and firewall protected) server.
+3. All electronic financial transactions made through this site are protected by cryptographic technology.
+4. You acknowledge that the transmission of information over the Internet is inherently insecure and we cannot guarantee the security of the data transmitted over the Internet.
+5. You are responsible for keeping the password used to access this site confidential. We do not require your password (except when you log in to this site).
+
+## I. Fix
+
+We may update this policy from time to time by publishing a new version on this site. Please check this page regularly to understand the policy changes. We may notify you of policy changes via email or our private messaging system.
+
+## J. Customer rights
+
+You can instruct us to disclose any personal information we hold about you. The following conditions apply to the provision of such information:
+
+1. Pay the fee (50,000 yen), and
+2. Providing proper identification (a copy of the passport certified by a notary public and the original utility bill showing your current address).
+
+To the extent permitted by law, we may withhold the disclosure of personal information requested by you. You have the right to instruct us not to process your personal information for marketing purposes. In practice, we usually provide the opportunity to expressly consent in advance to the use of personal information for marketing purposes or to opt out of the use of personal information for marketing purposes.
+
+## K. Third Party Website
+
+This site contains hyperlinks to third-party websites and more information about them. We have no control over and are not responsible for third party privacy policies and conduct.
+
+## L. Information update
+
+Please let us know if we need to correct or update the personal information we hold about you.
+
+## M. cookie
+
+This site uses cookies. A cookie is a file that contains an identifier (a string of letters and numbers) that is sent by a web server to a web browser and stored by the browser. Then, every time the browser requests a page from the server, the identifier is sent back to the server. Cookies are either "persistent" cookies or "session" cookies. Persistent cookies are stored by your web browser and are valid until the set expiration date unless deleted by the user before the expiration date. Session cookies, on the other hand, expire at the end of the user session when the web browser is closed. Cookies usually do not contain any personally identifiable information about you, but they do store personal information that is stored in the cookie and may be linked to information obtained from the cookie. We use both session cookies and persistent cookies.
+
+1. The names of the cookies used on this site and their purposes are as follows.
+
+ 1. Users access websites using computers ・ Track users when navigating websites ・ Use shopping carts ・ Improve website usability ・ Analyze website usage ・ Web Manage your site-Prevent fraud and improve your website's security-Personalize your website by user-Google Analytics on your website to recognize that you're targeting ads that are of particular interest to a particular user And use AdWords.
+
+2. Most browsers allow you to refuse cookies. for example,
+
+ 1. In Internet Explorer (version 10), you can reject cookies from the cookie processing override settings by clicking Tools> Internet Options> Privacy> Advanced Settings.
+ 2. In Firefox (version 24), click Tools> Options> Privacy, select Use custom settings for history from the drop-down menu, and then select Accept cookies from your site. To do.
+ 3. In Chrome (version 29), go to the "Customize and Control" menu, click "Settings"> "Show advanced settings"> "Content settings" and select "Block sites from settings". You can block all cookies.
+
+Blocking all cookies adversely affects the usability of many websites. If you block cookies, you will not be able to use all the features of the website.
+
+3. You can delete cookies that are already stored on your computer. for example,
+
+ 1. Internet Explorer (version 10) requires you to manually delete the cookie file ( [see http://support.microsoft.com/kb/278835](http://support.microsoft.com/kb/278835) for deletion instructions).
+ 2. In Firefox (version 24), click Tools> Options> Privacy, select "Use custom settings for history", click "Show cookies", and then "Delete all". You can click to delete the cookie
+ 3. In Chrome (version 29), go to the "Customize and Control" menu, click "Settings"> "Show advanced settings"> "Clear browsing data", then click Cookies and other sites and plugin data. Select Delete and then click "Clear browsing data".
+
+4. Blocking all cookies adversely affects the usability of many websites.
+
+_Update: September 22, 2021_
diff --git a/typescript/skeet-graphql/articles/legal/ja/privacy-policy.md b/typescript/skeet-graphql/articles/legal/ja/privacy-policy.md
new file mode 100644
index 000000000000..b6add62d125a
--- /dev/null
+++ b/typescript/skeet-graphql/articles/legal/ja/privacy-policy.md
@@ -0,0 +1,140 @@
+---
+id: privacy-policy
+title: プライバシーポリシー
+description: プライバシーポリシー
+---
+
+## A. はじめに
+
+1. 当サイトの利用者のプライバシーは弊社にとって非常に重要であり、弊社はプライバシーを保護することに尽力しています。このポリシーでは、お客様の個人情報をどのように扱うかを説明しています。
+2. このウェブサイトに初めてアクセスしたときに、このポリシーの条件に従ってクッキーを使用することに同意することで、お客様が搭載にアクセスするたびにクッキーを使用することを許可したこととみなします。
+
+## B. 謝辞
+
+この文書は SEQ Legal( seqlegal.com )のテンプレートを使用して作成され、
+ELSOUL LABO( labo.elsoul.nl )によって変更されたものです。
+
+## C. 個人情報の収集
+
+次の種類の個人情報が収集、保存、及び使用される場合があります。
+
+1. IP アドレス、地理的な場所、ブラウザの種類とバージョン、オペレーティングシステムなど、コンピュータに関する情報。
+2. アクセス元、訪問時間、ページビュー、ウェブサイトのナビゲーションパスなど、このウェブサイトへのアクセスと使用に関する情報。
+3. 当サイトに登録するときに入力する、電子メールアドレスなどの情報。
+4. 当サイトでプロフィールを作成するときに入力する情報、例えば、名前、プロフィール写真、性別、生年月日、既婚・未婚などの交際状況、興味や趣味、学歴、職歴。
+5. メールやニュースレターの購読を設定するために入力する名前やメールアドレスなどの情報。
+6. 当サイトでサービスを使用している間に入力する情報。
+7. 当サイトの使用中に生成される情報。いつ、どのくらいの頻度で、どのような状況で使用するかなどの情報を含む。
+8. お客様が購入したもの、使用したサービス、または当サイトを通じて行った取引に関する情報。名前、住所、電話番号、メールアドレス、クレジットカードの詳細を含む。
+9. インターネット上に公開する目的でお客様が当サイトに投稿する情報。ユーザー名、プロフィール写真、投稿内容を含む。
+10. 電子メールまたは当サイトを通じてお客様が当社に送信する通信に含まれる情報。通信コンテンツおよびメタデータを含む。
+11. お客様が当社に送信するその他の個人情報。
+
+お客様が他人の個人情報を当社に開示する前に、このポリシーに従ってその個人情報の開示と処理の両方についてその人の同意を得る必要があります
+
+## D. 個人情報の使用について
+
+当サイトを通じて当社に送信された個人情報は、このポリシーまたはウェブサイトの関連ページで指定された目的に使用されます。以下の目的でお客様の個人情報を使用する場合があります。
+
+1. 当サイトの管理及び経営の管理のため。
+2. お客様のために当サイトをパーソナライズするため。
+3. 当サイトで利用可能なサービスの使用を可能にするため。
+4. 当サイトから購入した商品を送付するため。
+5. 当サイトを通じて購入したサービスを提供するため。
+6. 明細書、請求書、支払い通知を送信し、支払いを承るため。
+7. マーケティング目的以外の通信を送信するため。
+8. お客様が請求したメール通知を送信するため。
+9. 請求した場合、メールでニュースレターを送信するため(ニュースレターが不要になった場合、いつでも配信を停止することができます)。
+10. 当社の事業またはお客様が関心があると思われる慎重に選択された第三者の事業に関連するマーケティングを、郵送またはお客様がこれに特に同意した場合は、電子メールまたは同様の技術で送信するため(マーケティングコミュニケーションが不要になった場合はいつでも弊社にご連絡ください)。
+11. 当サイトのユーザーに関する統計情報を第三者の事業に提供するため(ただし、第三者事業はその情報からユーザーを特定することはできません)。
+12. 当サイトに関連するお客様からの問い合わせや苦情に対応するため。
+13. 当サイトを安全に保ち、詐欺を防ぐため。
+14. 当サイトの使用を管理する利用規約の順守を検証するため(当サイトのプライベートメッセージングサービスを通じて送信されるプライベートメッセージのモニタリングを含む)。
+15. その他の用途。
+
+プライバシー設定は、当サイトでの情報の公開を制限するために使用でき、ウェブサイトのプライバシー管理を使用して調整できます。
+お客様の明示的な同意がない限り、第三者または他の第三者のダイレクトマーケティングのためにお客様の個人情報を第三者に提供することはありません。
+
+## E. 個人情報の開示について
+
+このポリシーに記載されている目的に合理的に必要な場合、職員、役員、保険会社、専門アドバイザー、代理店、供給会社、または下請業者に個人情報を開示する場合があります。
+このポリシーに記載されている目的のために合理的に必要な場合、当社グループのメンバー(つまり、子会社、最終持株会社、およびそのすべての子会社)に個人情報を開示する場合があります。
+
+1. 法律で義務付けられている範囲で、
+2. 進行中または将来の法的手続きに関連する場合、
+3. 当社の法的権利を確立、行使、または防御するため(詐欺防止および信用リスクの低減を目的とした他者への情報提供を含む)、
+4. 当社が販売している(または販売を検討している)事業または資産の購入者(または購入予定者)へ、
+5. 当社の合理的な意見では、かかる裁判所または当局がその個人情報の開示を合理的に判断する可能性がある場合、裁判所またはその他の管轄当局にその個人情報の開示を申請できると合理的に信じる者に、
+
+個人情報を開示する場合があります。
+
+このポリシーで規定されている場合を除き、当社はお客様の個人情報を第三者に提供しません。
+
+## F. 国際的なデータ通信
+
+1. 当社が収集する情報は、このポリシーに従って情報を使用できるようにするために、当社が事業を展開している国のいずれかで保管、処理、および転送される場合があります。
+2. 当社が収集する情報は、欧州経済地域で施行されているデータ保護法と同等のデータ保護法を持たない次の国に転送される場合があります:米国、ロシア、日本、中国、インド。
+3. 当サイトで公開する個人情報、または当サイトで公開するために送信する個人情報は、インターネットを介して世界中で利用できる場合があります。当社は他者によるそのような情報の使用または誤用を防ぐことはできません。
+
+## G. 個人情報の保持
+
+1. 本項 G では、個人情報の保持および削除に関する法的義務を確実に順守するように設計されたデータ保持ポリシーと手順を規定します。
+2. 弊社が何らかの目的のために処理する個人情報は、その目的またはそれらの目的に必要な期間よりも長く保持されることはありません。
+3. 弊社は一般的に、項 G2 に違反することなく、以下で規定された日時に以下で規定されたカテゴリに該当する個人情報を削除します。 1.個人に関する種類の情報は半年後に削除されます。
+4. 本項 G のほかの規定にかかわらず、
+5. 法律で義務付けられている範囲で、
+6. 文書が進行中または将来の法的手続きに関連していると思われる場合、および
+7. 当社の法的権利を確立、行使、または防御するため(詐欺防止および信用リスクの低減を目的とした他者への情報提供を含む)。
+ 個人情報を含む文書(電子文書を含む)を保持します。
+
+## H. 個人情報のセキュリティ
+
+1. 弊社は、お客様の個人情報の損失、誤用、または改ざんを防ぐために、合理的な技術的および組織的な予防措置を講じます。
+2. お客様から提供されたすべての個人情報は、安全な(パスワードおよびファイアウォールで保護された)サーバーに保存されます。
+3. 当サイトを通じて行われるすべての電子金融取引は、暗号化技術によって保護されます。
+4. お客様は、インターネットを介した情報の送信は本質的に安全ではなく、インターネットを介して送信されるデータのセキュリティを保証できないことを認めます。
+5. 当サイトへのアクセスに使用するパスワードを秘密にする責任はお客様にあります。弊社がお客様のパスワードを要求することはありません(当サイトにログインする場合を除く)。
+
+## I. 修正
+
+当サイトに新しいバージョンを公開することにより、このポリシーを随時更新する場合があります。定期的にこのページを確認し、ポリシーの変更内容の理解に努めてください。ポリシーの変更については電子メールまたは当サイトのプライベートメッセージングシステムを通じて通知する場合があります。
+
+## J. お客様の権利
+
+お客様は、弊社が保持しているお客様に関する個人情報を開示するように指示することができます。そのような情報の提供には、次の条件が適用されます。
+
+1. 料金を支払うこと(五万円)、そして
+2. 適切な身分証明書を提供すること(公証人によって証明されたパスポートのコピーと現住所を示す公共料金請求書の原本)。
+
+法律で許可されている範囲で、お客様が要求する個人情報の開示を保留する場合があります。
+お客様にはマーケティング目的で個人情報を処理しないよう指示する権利があります。
+実際には、通常、マーケティング目的での個人情報の使用に事前に明示的に同意するか、マーケティング目的での個人情報の使用をオプトアウトする機会を提供します。
+
+## K. サードパーティのウェブサイト
+
+当サイトにはサードパーティのウェブサイトへのハイパーリンクとそれに関する詳細情報が掲載されています。弊社は、サードパーティのプライバシーポリシーと行いを管理することはできず、責任を負いません。
+
+## L. 情報の更新
+
+弊社がお客様に関して保持している個人情報を修正または更新する必要がある場合はお知らせください。
+
+## M. クッキー
+
+当サイトはクッキーを使用しています。クッキーは、ウェブサーバーからウェブブラウザに送信され、ブラウザによって保存される識別子(文字と数字の文字列)を含むファイルです。その後、ブラウザがサーバーからページを要求するたびに、識別子がサーバーに送り返されます。クッキーは、「永続」クッキーまたは「セッション」クッキーのいずれかです。永続クッキーは、ウェブブラウザによって保存され、有効期限前にユーザーによって削除されない限り、設定された有効期限まで有効です。一方、セッションクッキーは、Web ブラウザが閉じられるユーザーセッションの終了時に期限切れになります。クッキーには通常、ユーザーを個人的に特定する情報は含まれませんが、クッキーに保存され、クッキーから取得した情報にリンクされている可能性のある個人情報が保存されます。セッションクッキーと永続クッキーの両方を使用しています。
+
+1. 当サイトで使用するクッキーの名前、およびそれらの使用目的は以下のとおりです。
+ 1. ユーザーがコンピュータを使ってウェブサイトにアクセスする・ウェブサイトをナビゲートする際にユーザーを追跡する・買い物かごを使用する・ウェブサイトの使いやすさを改善する・ウェブサイトの利用を分析する・ウェブサイトを管理する・詐欺を防止しウェブサイトのセキュリティを改善する・ユーザーごとにウェブサイトをパーソナライズする・特定のユーザーに特に関心のある広告のターゲット化することを認識するためにウェブサイトで Google Analytics と AdWords を使用します。
+2. ほとんどのブラウザでは、クッキーを拒否できます。例えば、
+ 1. Internet Explorer(バージョン 10)では、[ツール]、[インターネットオプション]、[プライバシー]、[詳細設定]の順にクリックして、クッキー処理上書き設定からクッキーを拒否できます。
+ 2. Firefox(バージョン 24)では、[ツール]、[オプション]、[プライバシー]の順にクリックし、ドロップダウンメニューから[履歴にカスタム設定を使用する]を選択し、[サイトからの Cookie を受け入れる]を選択します。
+ 3. Chrome(バージョン 29)では、「カスタマイズと制御」メニューにアクセスし、「設定」、「詳細設定を表示」、「コンテンツ設定」の順にをクリックし、「設定からサイトをブロック」を選択して、すべての Cookie をブロックできます。
+
+すべてのクッキーをブロックすると、多くのウェブサイトのユーザビリティに悪影響が及びます。クッキーをブロックすると、ウェブサイトのすべての機能を使用できなくなります。
+
+3. コンピュータに既に保存されているクッキーを削除できます。たとえば、
+ 1. Internet Explorer(バージョン 10)では、クッキーファイルを手動で削除する必要があります(削除の手順は http://support.microsoft.com/kb/278835 を参照してください)。
+ 2. Firefox(バージョン 24)では、「ツール」、「オプション」、「プライバシー」の順にクリックし、「履歴にカスタム設定を使用する」を選択し、「Cookie を表示」をクリックしてから「すべて削除」をクリックして、クッキーを削除できます
+ 3. Chrome(バージョン 29)では、「カスタマイズと制御」メニューにアクセスし、「設定」、「詳細設定を表示」、「閲覧データを消去」の順にをクリックして、クッキーと他のサイトとプラグインデータを削除するを選択してから「閲覧データを消去」をクリックして下さい。
+4. すべてのクッキーをブロックすると、多くのウェブサイトのユーザビリティに悪影響が及びます。
+
+_更新: 2021 年 9 月 22 日_
diff --git a/typescript/skeet-graphql/articles/news/en/2023/06/13/effortless-serverless-skeet.md b/typescript/skeet-graphql/articles/news/en/2023/06/13/effortless-serverless-skeet.md
new file mode 100644
index 000000000000..e4bb1cb1c922
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/en/2023/06/13/effortless-serverless-skeet.md
@@ -0,0 +1,62 @@
+---
+id: effortless-serverless-skeet
+title: Released "Skeet", an open-source library for building zero-maintenance serverless apps on Firebase
+category: Press release
+thumbnail: /news/2023/06/13/EffortlessServerlessSkeet.png
+---
+
+ELSOUL LABO B.V. (head office: Amsterdam, Netherlands) announced on the 13th the release of "Skeet," an open-source library for building zero-maintenance serverless applications on Firebase.
+
+Skeet: https://skeet.dev/
+
+## Do more, manage less
+
+Reduce app development and operation costs and realize more plans.
+
+Skeet is an open-source full-stack app development solution.
+You can start writing app logic immediately without worrying about infrastructure.
+
+### High Performance
+
+Distributed systems leveraging various event triggers such as Auth and storage work more efficiently, and Firestore, a NoSQL database, scales flexibly and supports realtime stream.
+
+### Eco-friendly
+
+Computing resources automatically scale to match your usage patterns, so you always have as much as you need. Skeet is designed to be economically and environmentally friendly.
+
+### Developer Experience
+
+You can start writing app logic immediately without worrying about deploying or maintaining server infrastructure. TypeScript support makes apps more secure and improves development efficiency.
+
+### Security built-in
+
+In addition to SSL support, DDoS attacks and bot countermeasures using Cloud Armor, minimal endpoint exposure, and Pub/Sub messaging can be easily applied to achieve Google-scale security.
+
+## A new-age app development platform
+
+![Skeet - Full-stack Serverless Framework](https://storage.googleapis.com/skeet-assets/animation/skeet-cli-create-latest.gif)
+
+Everything you need to develop and publish your app. The Skeet framework tools are designed to make handling serverless architectures, which tend to be distributed and complicated, as easy as possible.
+
+### High performance monitor
+
+Firebase Emulator Suite makes your backend visible. The development speed will increase since you can centrally manage databases, storage, and all logs from authentication information.
+
+### Powerful CLI Tool
+
+Skeet CLI completes constructing a distributed system that combines Google Cloud's serverless products with a single command. Just answer a few simple questions, and your app is ready to deploy.
+
+### Zero-maintenance
+
+Automatic deployment with GitHub Actions and deployed resources automatically scale as needed, eliminating the need for server infrastructure maintenance.
+
+## Experience new app development
+
+![Skeet with OpenAI](https://storage.googleapis.com/skeet-assets/animation/skeet-chat-latest.gif)
+
+Experience new app development.
+You can immediately develop and publish web, iOS, and Android apps.
+
+In the Skeet tutorial, we will develop an AI chatbot app using the OpenAI API and confirm that it can be deployed and used.
+
+Skeet Tutorial: https://skeet.dev/en/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/en/2023/06/19/skeet-demo-ai-chat-app-published.md b/typescript/skeet-graphql/articles/news/en/2023/06/19/skeet-demo-ai-chat-app-published.md
new file mode 100644
index 000000000000..4fcd4687352c
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/en/2023/06/19/skeet-demo-ai-chat-app-published.md
@@ -0,0 +1,50 @@
+---
+id: skeet-demo-ai-chat-app-published
+title: Demo AI chat app created with open-source serverless framework "Skeet" launched
+category: Press release
+thumbnail: /news/2023/06/19/SkeetDemoPublished.png
+---
+
+ELSOUL LABO B.V. (Headquarters: Amsterdam, Netherlands) announced on the 19th the launch of a demo AI chat app built with "Skeet," an open-source library for building zero-maintenance serverless apps on Firebase.
+
+By using this demo, planners and developers can easily imagine what kind of applications can be created using "Skeet" in advance.
+
+Skeet Demo AI Chat App: https://skeeter.app/
+
+## What kind of apps can you create? Let's imagine using the demo AI chat app.
+
+![Skeet Demo AI Chat App](/news/2023/06/19/SkeeterAppSample16-9.png)
+
+Skeet is a full-stack serverless framework that lets you build auto-scaling apps on top of Firebase.
+
+Until now, releasing applications and publishing services required the preparation of application code and servers, and the construction and management of servers, in particular, was costly.
+
+The serverless environment provided by Google Cloud and Firebase eliminates the need for this server construction and management. All server infrastructure automatically scales with user usage, eliminating the need for detailed access forecasting and load management resource management.
+
+Skeet can build and manage these serverless products with one command for developing iOS, Android, and web apps. So Skeet developers can immediately start working on the application logic. And the deployment of the written app is guaranteed.
+
+With Skeet, you can quickly build and release applications leveraging the ChatGPT API.
+
+We have released the app as a demo after completing the Skeet tutorial.
+
+Skeet Demo AI Chat App: https://skeeter.app/
+
+Like this demo, Skeet has everything you need to develop and publish an application, such as authentication and database usage that applications generally do.
+
+TypeScript is used for both the back-end and front-end, but Python can also be used as a backend for each function if necessary, so it can be used when machine learning is required.
+
+We believe that many useful apps will be created using Skeet, and we will continue to develop and improve the Skeet framework.
+
+## Experience new app development
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+Experience new app development.
+
+You can immediately develop and publish web, iOS, and Android apps.
+
+With the Skeet tutorial, you can actually build an AI chat app like this demo and have it ready to ship.
+
+We would appreciate it if you could try it.
+
+Skeet Tutorial: https://skeet.dev/en/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/en/2023/06/23/skeet-type-safe-firestore.md b/typescript/skeet-graphql/articles/news/en/2023/06/23/skeet-type-safe-firestore.md
new file mode 100644
index 000000000000..b767b3d9366f
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/en/2023/06/23/skeet-type-safe-firestore.md
@@ -0,0 +1,65 @@
+---
+id: skeet-type-safe-firestore
+title: Released an open-source library that can handle Firestore, a NoSQL database, in a type-safe manner
+category: Press release
+thumbnail: /news/2023/06/23/SkeetTypeSafeFirestore2.png
+---
+
+ELSOUL LABO B.V. (Headquarters: Amsterdam, Netherlands) announced on the 23th the release of an open-source library "Skeet Firestore" that can handle Firestore, a serverless NoSQL database on Google Cloud, in a type-safe manner.
+
+Skeet Firestore: https://github.com/elsoul/skeet-firestore
+
+## The serverless NoSQL database "Firestore"
+
+Google Cloud's serverless NoSQL database service "Firestore" is a mobile application backend with real-time query capabilities. His four main characteristics are:
+
+- Ease of Use: You can start developing your application without spending time on preliminary database design. It also supports ACID transactions with strong consistency, unlike common NoSQL databases.
+- Fully Serverless Operation and Rapid Scaling: A fully serverless service storing data in a distributed Spanner database running in Google's data centers, enabling fast autoscaling.
+- Flexible, Efficient Real-time Queries: Data changes on the database can be notified and reflected to the client in real-time, facilitating real-time UI updates. increase.
+- Disconnected Operation: Even if the mobile device is offline, the data can be referenced and written by the local cache and will be reflected in the database when the device is online.
+
+Reference - "Firestore: The NoSQL Serverless Database for the Application Developer (2023)": https://research.google/pubs/pub52292/
+
+Skeet Firestore also uses TypeScript to make the Firestore type-safe, enabling change-resistant software development.
+
+For details, please take a look at the official Skeet document below.
+
+Skeet Official Doc (Skeet Firestore): https://skeet.dev/en/doc/plugins/skeet-firestore/
+
+## What kind of apps can you create? Let's imagine using the demo AI chat app.
+
+![Skeet Demo AI Chat App](/news/2023/06/19/SkeeterAppSample16-9.png)
+
+Skeet is a full-stack serverless framework that lets you build auto-scaling apps on top of Firebase.
+
+Until now, releasing applications and publishing services required the preparation of application code and servers, and the construction and management of servers, in particular, was costly.
+
+The serverless environment provided by Google Cloud and Firebase eliminates the need for this server construction and management. All server infrastructure automatically scales with user usage, eliminating the need for detailed access forecasting and load management resource management.
+
+Skeet can build and manage these serverless products with one command for developing iOS, Android, and web apps. So Skeet developers can immediately start working on the application logic. And the deployment of the written app is guaranteed.
+
+With Skeet, you can quickly build and release applications leveraging OpenAI's ChatGPT API.
+
+We have released the app as a demo after completing the Skeet tutorial.
+
+Skeet Demo AI Chat App: https://skeeter.app/
+
+Like this demo, Skeet has everything you need to develop and publish an application, such as authentication and database usage that applications generally do.
+
+TypeScript is used for both the back-end and front-end, but Python can also be used as a backend for each function if necessary, so it can be used when machine learning is required.
+
+We believe that many useful apps will be created using Skeet, and we will continue to develop and improve the Skeet framework.
+
+## Experience new app development
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+Experience new app development.
+
+You can immediately develop and publish web, iOS, and Android apps.
+
+With the Skeet tutorial, you can actually build an AI chat app like this demo and have it ready to ship.
+
+We would appreciate it if you could try it.
+
+Skeet Tutorial: https://skeet.dev/en/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/en/2023/06/29/skeet-tutorial-youtube-published.md b/typescript/skeet-graphql/articles/news/en/2023/06/29/skeet-tutorial-youtube-published.md
new file mode 100644
index 000000000000..76403902ab8a
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/en/2023/06/29/skeet-tutorial-youtube-published.md
@@ -0,0 +1,56 @@
+---
+id: skeet-tutorial-youtube-published
+title: Released a tutorial video to build an AI chat application using ChatGPT API using Skeet
+category: Press release
+thumbnail: /news/2023/06/29/SkeetTutorialYouTubeThumbnail2.png
+---
+
+ELSOUL LABO B.V. (head office: Amsterdam, Netherlands) has released a tutorial video on YouTube on building an AI chat app using the ChatGPT API with the full-stack serverless framework Skeet.
+
+## Let's create your original AI Chat App
+
+ChatGPT, which is currently a hot topic, allows you to create your own original AI chat application by using the API.
+
+It can also be customized for you and your organization, so there are many ways to use it.
+
+Also, it is expected that application development using AI will become popular in the future, so please take this opportunity to catch up.
+
+As you progress through the Skeet tutorial, you can create your AI chat app, deploy it on the cloud, and publish the service.
+
+Skeet Tutorial (YouTube): https://www.youtube.com/watch?v=6em68qcSsJE
+
+## What kind of apps can you create? Let's imagine using the demo AI chat app.
+
+![Skeet Demo AI Chat App](/news/2023/06/19/SkeetDemoPublished.png)
+
+Skeet is a full-stack serverless framework that lets you build auto-scaling apps on top of Firebase.
+
+Until now, releasing applications and publishing services required the preparation of application code and servers, and the construction and management of servers, in particular, was costly.
+
+The serverless environment provided by Google Cloud and Firebase eliminates the need for this server construction and management. All server infrastructure automatically scales with user usage, eliminating the need for detailed access forecasting and load management resource management.
+
+Skeet can build and manage these serverless products with one command for developing iOS, Android, and web apps. So Skeet developers can immediately start working on the application logic. And the deployment of the written app is guaranteed.
+
+With Skeet, you can quickly build and release applications leveraging OpenAI's ChatGPT API.
+
+We have released the app as a demo after completing the Skeet tutorial.
+
+Skeet Demo AI Chat App: https://skeeter.app/
+
+Like this demo, Skeet has everything you need to develop and publish an application, such as authentication and database usage that applications generally do.
+
+TypeScript is used for both the back-end and front-end, but Python can also be used as a backend for each function if necessary, so it can be used when machine learning is required.
+
+We believe that many useful apps will be created using Skeet, and we will continue to develop and improve the Skeet framework.
+
+## Experience new app development
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+You can immediately develop and publish web, iOS, and Android apps.
+
+With the Skeet tutorial, you can actually build an AI chat app like this demo and have it ready to ship.
+
+We would appreciate it if you could try it.
+
+Skeet Tutorial: https://skeet.dev/en/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/en/2023/07/10/skeet-nextjs-template-released.md b/typescript/skeet-graphql/articles/news/en/2023/07/10/skeet-nextjs-template-released.md
new file mode 100644
index 000000000000..65ce1f7d479d
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/en/2023/07/10/skeet-nextjs-template-released.md
@@ -0,0 +1,62 @@
+---
+id: skeet-nextjs-template-released
+title: Next.js (React) starter added to Skeet, a TypeScript serverless framework. A new AI Chat App demo has also been released.
+category: Press release
+thumbnail: /news/2023/07/10/NewReleaseSkeetxNextjs.png
+---
+
+ELSOUL LABO B.V. (Headquarters: Amsterdam, Netherlands) announced the release of the addition of Next.js (React) starter to Skeet, a TypeScript full-stack serverless framework. At the same time, a demo of a new AI chat app created with the same starter and ChatGPT API has also been released.
+
+Skeet AI Chat App Demo: https://skeeter.dev/
+
+## Skeet Next.js (React) starter
+
+![Skeet Next.js (React) Starter](/news/2023/07/10/WebAppBoilerplate.png)
+
+Skeet is a TypeScript full-stack serverless framework that lets you build auto-scaling apps on top of Firebase.
+
+Skeet allows you to get your app up and running quickly and maintain it for the long term at a low cost.
+
+This time, he added his highly requested Next.js (https://nextjs.org/) starter from Vercel to his Skeet framework.
+
+This gives you an even faster web app development environment.
+
+This Next.js starter uses SSG (Static Site Generation) technology to achieve strong SEO support, high performance, and reduced hosting costs.
+
+In addition, the settings essential for web development, such as multilingual support and his PWA (available by downloading the app), have already been completed, so you can start developing your application immediately without spending time preparing the development environment. Focus on your code.
+
+Skeet Next (GitHub): https://github.com/elsoul/skeet-next
+
+## New AI Chat App demo (made by Next.js)
+
+![Skeet Demo AI Chat App](/news/2023/07/10/CreateChatRoom.png)
+
+At the same time, we released a new AI chat app demo using this Next.js starter and ChatGPT API.
+
+This demo app demonstrates what an app is built when you run the skeet create command.
+
+OpenAI announced on July 7 that its natural language model "GPT-4" API is now available to all ChatGPT Plus users. This demo app also allows you to use the "GPT-4".
+
+By setting the character of the AI assistant, it is also possible to improve the quality of the response.
+
+![Skeet Demo AI Chat App](/news/2023/07/10/ChatWithCodeHighlight.png)
+
+ChatGPT has become a reliable ally for developers.
+
+This demo also supports syntax highlighting of the program code (because it is colored according to the rules, making it easier to read), and by customizing the number of tokens, etc., you can get a more accurate answer.
+
+Skeet AI Chat App Demo: https://skeeter.dev/
+
+## Experience new app development
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+You can immediately develop and publish web, iOS, and Android apps.
+
+With the Skeet tutorial, you can actually build an AI chat app that is ready to ship.
+
+Please try it and give us any feedback.
+
+Skeet Tutorial (Document): https://skeet.dev/en/doc/backend/quickstart/
+
+Skeet Tutorial (YouTube): https://www.youtube.com/watch?v=6em68qcSsJE
diff --git a/typescript/skeet-graphql/articles/news/en/2023/08/01/skeet-nextjs-graphql-template-released.md b/typescript/skeet-graphql/articles/news/en/2023/08/01/skeet-nextjs-graphql-template-released.md
new file mode 100644
index 000000000000..e0abf9c5f555
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/en/2023/08/01/skeet-nextjs-graphql-template-released.md
@@ -0,0 +1,51 @@
+---
+id: skeet-nextjs-graphql-template-released
+title: Skeet, an open-source serverless framework, now supports GraphQL and SQL (relational databases)
+category: Press release
+thumbnail: /news/2023/08/01/skeet-graphql.png
+---
+
+On August 1, 2023, ELSOUL LABO B.V. (Headquarters: Amsterdam, Netherlands) announced the major release of Skeet v1, an open-source serverless framework that makes app development fast and zero-maintenance. This release enables Skeet developers to develop using GraphQL and SQL (relational database).
+
+## Added new Next.js (React) + GraphQL API server option
+
+![Skeet Next.js (React) + GraphQL Option](/news/2023/08/01/skeet-create-got-graphql.png)
+
+Skeet is an open-source serverless framework that allows you to build zero-maintenance apps on GCP (Google Cloud) and Firebase.
+
+You can develop super fast from API to web/iOS/Android apps with TypeScript.
+
+With this update, Skeet supports application development using GraphQL and SQL (relational database).
+
+![Skeet Next.js (React) + GraphQL Starter](/news/2023/08/01/skeet-next-graphql.png)
+
+Through support for SQL (relational database), hybrid development of SQL and NoSQL (Firestore) is now possible.
+This allows developers to take advantage of both database types.
+
+Relational database excels at handling data with relationships, making it easy to maintain consistency in data retrieval and transaction processing. On the other hand, NoSQL (such as Firestore) is more flexible and scalable, making it a better choice for large amounts of data and rapid data growth.
+
+Skeet's hybrid development environment allows it to optimally manage business logic where data relationships are important in a relational database and large amounts of data, such as user data and logs, in his NoSQL. This can be a key strategy for getting the best performance within an application.
+
+## Database and API Visualization: Leveraging Prisma and Apollo
+
+With this update, you can now leverage the tools Prisma and Apollo to visualize your databases and APIs in the UI. This allows developers to understand and manipulate data structures intuitively, rather than just writing code.
+
+![Skeet Prisma Studio](/news/2023/08/01/prisma-studio.jpg)
+
+Prisma (https://www.prisma.io/) is an ORM that can easily handle SQL (relational database) with TypeScript and JavaScript. Skeet uses Prisma to provide a simple and flexible way to define your database schema and auto-generated migrations and provides TypeScript types to help with development. In addition, Prisma Studio makes it possible to operate the database with a GUI, allowing developers to easily operate and check data.
+
+![Skeet Apollo Console](/news/2023/08/01/apollo-console.png)
+
+Apollo (https://www.apollographql.com/) is a powerful tool for developing GraphQL API servers. Apollo's developer console lets you visualize your GraphQL schemas and test your APIs in real-time.
+
+In the new UI, you can generate your own GraphQL used on the front end by clicking on the GraphQL query, mutation, and data content you want to use.
+
+By leveraging these tools, Skeet provides a platform for developers to develop applications more intuitively and efficiently. With this update, developers can realize even more productivity gains.
+
+Skeet is developed as open-source to reduce development and maintenance costs and improve the developer experience for all application development sites around the world.
+
+Experience state-of-the-art application development using Skeet.
+
+Skeet Documentation: https://skeet.dev/en/
+
+Skeet (GitHub): https://github.com/elsoul/skeet-cli
diff --git a/typescript/skeet-graphql/articles/news/ja/2023/06/13/effortless-serverless-skeet.md b/typescript/skeet-graphql/articles/news/ja/2023/06/13/effortless-serverless-skeet.md
new file mode 100644
index 000000000000..b2b200b790c4
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/ja/2023/06/13/effortless-serverless-skeet.md
@@ -0,0 +1,62 @@
+---
+id: effortless-serverless-skeet
+title: Firebase上にゼロメンテナンスのサーバーレスアプリを構築するオープンソースライブラリ"Skeet"をリリース
+category: プレスリリース
+thumbnail: /news/2023/06/13/EffortlessServerlessSkeet.png
+---
+
+ELSOUL LABO B.V. (エルソウルラボ, 本社: オランダ・アムステルダム)は 13 日、Firebase 上にゼロメンテナンスのサーバーレスアプリを構築するオープンソースライブラリである"Skeet"のリリースを発表しました。
+
+Skeet: https://skeet.dev/
+
+## もっと多くを少ないコストで
+
+アプリの開発・運用コストを下げ、もっと多くのプランを実現させましょう。
+
+Skeet はオープンソースのフルスタックアプリ開発ソリューションです。
+すぐにアプリのロジックからスタートでき、インフラに関する心配は無用です。
+
+### ハイパフォーマンス
+
+Auth やストレージ等さまざまなイベントトリガーを活用した分散システムはより効率的に動作し、NoSQL データベースの Firestore は柔軟にスケール、リアルタイムストリームにも対応しています。
+
+### エコフレンドリー
+
+ユーザーの使用パターンに合わせてコンピューティングリソースが自動的にスケールするため、常に必要な分だけリソースを利用します。Skeet は経済的にも環境的にも優しい設計となっています。
+
+### 優れた開発者体験
+
+デプロイやサーバーインフラのメンテナンスの心配無しに、すぐにアプリロジックから書き始めることができます。TypeScript のサポートはアプリをより安全にし、さらに開発効率を向上させます。
+
+### 堅牢なセキュリティ
+
+SSL 対応はもちろん、Cloud Armor を活用した DDoS 攻撃及び bot 対策や、最小限のエンドポイント露出及び Pub/Sub メッセージングの活用が簡単に適用でき、Google 規模のセキュリティを実現できます。
+
+## 新時代のアプリ開発プラットフォーム
+
+![Skeet - Full-stack Serverless Framework](https://storage.googleapis.com/skeet-assets/animation/skeet-cli-create-latest.gif)
+
+アプリの開発及び公開に必要なものはすべて揃っています。Skeet フレームワークのツール群は、分散し煩雑になりがちなサーバーレスアーキテクチャをできるだけ簡単に扱えるようにと設計されています。
+
+### 高性能なモニター
+
+Firebase Emulator Suite はバックエンドを可視化します。認証情報からデータベース、ストレージやすべてのログを一元管理できるため、開発のスピードが上がります。
+
+### 強力な CLI ツール
+
+Skeet CLI は Google Cloud のサーバーレス製品を組み合わせた分散システム構築をワンコマンドで完了させます。簡単な質問に答えるだけでアプリはデプロイ可能な状態に仕上がります。
+
+### ゼロメンテナンス
+
+GitHub Actions による自動デプロイ、そしてデプロイされたリソースは必要な分だけ自動的にスケールするため、サーバーインフラのメンテナンスは基本的に必要ありません。
+
+## 新しいアプリ開発を体感してください
+
+![Skeet with OpenAI](https://storage.googleapis.com/skeet-assets/animation/skeet-chat-latest.gif)
+
+新しいアプリ開発を体感してください。
+すぐに Web・iOS・Android アプリを開発し公開できます。
+
+Skeet チュートリアルでは、OpenAI API を活用して AI チャットボットのアプリを開発し、実際にデプロイして利用できることを確認します。
+
+Skeet チュートリアル: https://skeet.dev/ja/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/ja/2023/06/19/skeet-demo-ai-chat-app-published.md b/typescript/skeet-graphql/articles/news/ja/2023/06/19/skeet-demo-ai-chat-app-published.md
new file mode 100644
index 000000000000..91b5f43963ed
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/ja/2023/06/19/skeet-demo-ai-chat-app-published.md
@@ -0,0 +1,50 @@
+---
+id: skeet-demo-ai-chat-app-published
+title: オープンソースのサーバーレスフレームワーク"Skeet"で作成したデモAIチャットアプリがローンチ
+category: プレスリリース
+thumbnail: /news/2023/06/19/SkeetDemoPublished.png
+---
+
+ELSOUL LABO B.V. (エルソウルラボ, 本社: オランダ・アムステルダム)は 19 日、Firebase 上にゼロメンテナンスのサーバーレスアプリを構築するオープンソースライブラリである"Skeet"で作成したデモ AI チャットアプリのローンチを発表しました。
+
+このデモを利用することでプランナーや開発者は予め、"Skeet"を使ってどんなアプリを作ることができるか想像しやすくなります。
+
+Skeet デモ AI チャットアプリ: https://skeeter.app/
+
+## どんなアプリがつくれるの?デモ AI チャットアプリを使って想像してみましょう
+
+![Skeet Demo AI Chat App](/news/2023/06/19/SkeeterAppSample16-9.png)
+
+Skeet は Firebase 上に自動スケールするアプリを構築できるフルスタックサーバーレスフレームワークです。
+
+今までアプリのリリースやサービス公開には、アプリケーションコードとサーバーの用意が必要で、特にサーバーの構築・管理には大きなコストがかかっていました。
+
+Google Cloud、Firebase の提供するサーバーレス環境はこのサーバー構築・管理を不要にします。すべてのサーバーインフラはユーザーの使用に合わせて自動でスケーリングするため、詳細なアクセス予想や負荷対策のリソース管理はもう必要ありません。
+
+Skeet は iOS・Android・Web アプリの開発のために、これらのサーバーレス製品をワンコマンドで構築・管理できます。そのため、Skeet 開発者はすぐにアプリケーションのロジックに取り掛かることが可能です。そして、書いたアプリのデプロイは保証されています。
+
+Skeet を使えば、ChatGPT API を活用したアプリケーションもすぐに構築しリリースすることが可能です。
+
+Skeet チュートリアルを完了させた状態のアプリをデモとして公開しています。
+
+Skeet デモ AI チャットアプリ: https://skeeter.app/
+
+このデモの様に、一般的にアプリケーションが行う認証やデータベースの利用等、アプリの開発及び公開に必要なものはすべて揃っています。
+
+バックエンド、フロントエンド共に TypeScript を利用していますが、必要に応じて Python 等もバックエンドとして関数毎に利用できるため、機械学習が必要になった場合にも対応できます。
+
+Skeet を活用してたくさんの役に立つアプリが生まれることを信じて、これからも開発・改善を続けてまいります。
+
+## 新しいアプリ開発を体感してください
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+新しいアプリ開発を体感してください。
+
+すぐに iOS・Android・Web アプリを開発し公開できます。
+
+Skeet チュートリアルでは、実際にこのデモのような AI チャットアプリを構築し、すぐにリリースすることができます。
+
+ぜひお試しいただけますと幸いです。
+
+Skeet チュートリアル: https://skeet.dev/ja/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/ja/2023/06/23/skeet-type-safe-firestore.md b/typescript/skeet-graphql/articles/news/ja/2023/06/23/skeet-type-safe-firestore.md
new file mode 100644
index 000000000000..0e27a241e659
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/ja/2023/06/23/skeet-type-safe-firestore.md
@@ -0,0 +1,65 @@
+---
+id: skeet-type-safe-firestore
+title: サーバーレスNoSQLデータベースのFirestoreを型安全に扱うことのできるオープンソースライブラリがリリース
+category: プレスリリース
+thumbnail: /news/2023/06/23/SkeetTypeSafeFirestore2.png
+---
+
+ELSOUL LABO B.V. (エルソウルラボ, 本社: オランダ・アムステルダム)は 23 日、Google Cloud の サーバーレス NoSQL データベース である Firestore を型安全に扱うことのできるオープンソースライブラリ「Skeet Firestore」のリリースを発表しました。
+
+Skeet Firestore: https://github.com/elsoul/skeet-firestore
+
+## サーバーレス NoSQL データベース "Firestore"
+
+Google Cloud の サーバーレス NoSQL データベースサービス "Firestore" はモバイルアプリケーションのバックエンドとして利用され、リアルタイムクエリー機能を備えています。その主な特徴として以下の 4 点が挙げられます。
+
+- 容易な利用方法(Ease of Use): 事前のデータベース設計に時間をかけずにアプリケーションんを開発をスタートできます。また、一般的な NoSQL データベースと異なり強い整合性を持った ACID トランザクションをサポートしています。
+- サーバーレス環境と高速なスケーリング(Fully Severless Operation and Rapid Scaling): 完全サーバーレスサービスで、Google のデータセンター内で稼働する分散データベース Spanner にデータが保存され、高速なオートスケーリングが可能です。
+- 柔軟で効率的なリアルタイムクエリー(Flexible, Efficient Real-time Queries): データベース上でのデータ変更をリアルタイムにクライアントに通知・反映することが可能で、この機能によりクライアントのリアルタイム UI 更新が容易になります。
+- オフライン操作(Disconnected Operation): モバイルデバイスがオフラインの場合でも、ローカルキャッシュによりデータの参照や書き込みが可能で、デバイスがオンラインになったタイミングでデータベースに反映されます。
+
+参考 - "Firestore: The NoSQL Serverless Database for the Application Developer (2023)": https://research.google/pubs/pub52292/
+
+Skeet Firestore は、さらに TypeScript を使ってこの Firestore を型安全に利用することで、変更に強いソフトウェア開発を可能にします。
+
+詳しくは 下記 Skeet 公式ドキュメントを御覧ください。
+
+Skeet 公式ドキュメント (Skeet Firestore): https://skeet.dev/ja/doc/plugins/skeet-firestore/
+
+## どんなアプリがつくれるの?デモ AI チャットアプリを使って想像してみましょう
+
+![Skeet Demo AI Chat App](/news/2023/06/19/SkeeterAppSample16-9.png)
+
+Skeet は Firebase 上に自動スケールするアプリを構築できるフルスタックサーバーレスフレームワークです。
+
+今までアプリのリリースやサービス公開には、アプリケーションコードとサーバーの用意が必要で、特にサーバーの構築・管理には大きなコストがかかっていました。
+
+Google Cloud、Firebase の提供するサーバーレス環境はこのサーバー構築・管理を不要にします。すべてのサーバーインフラはユーザーの使用に合わせて自動でスケーリングするため、詳細なアクセス予想や負荷対策のリソース管理はもう必要ありません。
+
+Skeet は iOS・Android・Web アプリの開発のために、これらのサーバーレス製品をワンコマンドで構築・管理できます。そのため、Skeet 開発者はすぐにアプリケーションのロジックに取り掛かることが可能です。そして、書いたアプリのデプロイは保証されています。
+
+Skeet を使えば、OpenAI の ChatGPT API を活用したアプリケーションもすぐに構築しリリースすることが可能です。
+
+Skeet チュートリアルを完了させた状態のアプリをデモとして公開しています。
+
+Skeet デモ AI チャットアプリ: https://skeeter.app/
+
+このデモの様に、一般的にアプリケーションが行う認証やデータベースの利用等、アプリの開発及び公開に必要なものはすべて揃っています。
+
+バックエンド、フロントエンド共に TypeScript を利用していますが、必要に応じて Python 等もバックエンドとして関数毎に利用できるため、機械学習が必要になった場合にも対応できます。
+
+Skeet を活用してたくさんの役に立つアプリが生まれることを信じて、これからも開発・改善を続けてまいります。
+
+## 新しいアプリ開発を体感してください
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+新しいアプリ開発を体感してください。
+
+すぐに iOS・Android・Web アプリを開発し公開できます。
+
+Skeet チュートリアルでは、実際にこのデモのような AI チャットアプリを構築し、すぐにリリースすることができます。
+
+ぜひお試しいただけますと幸いです。
+
+Skeet チュートリアル: https://skeet.dev/ja/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/ja/2023/06/29/skeet-tutorial-youtube-published.md b/typescript/skeet-graphql/articles/news/ja/2023/06/29/skeet-tutorial-youtube-published.md
new file mode 100644
index 000000000000..3a4a50f599f1
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/ja/2023/06/29/skeet-tutorial-youtube-published.md
@@ -0,0 +1,56 @@
+---
+id: skeet-tutorial-youtube-published
+title: Skeetを利用してChatGPT APIを使ったAI チャットアプリを構築するチュートリアル動画を公開
+category: プレスリリース
+thumbnail: /news/2023/06/29/SkeetTutorialYouTubeThumbnail2.png
+---
+
+ELSOUL LABO B.V. (エルソウルラボ, 本社: オランダ・アムステルダム)は、フルスタックサーバーレスフレームワークの Skeet を利用して ChatGPT API を使った AI チャットアプリを構築するチュートリアル動画を YouTube 上にて公開しました。
+
+## オリジナルの AI チャットアプリを作ってみよう
+
+今話題の ChatGPT は、API を利用することによってオリジナルの AI チャットアプリを作ることができます。
+
+あなたやあなたの組織専用にカスタマイズすることも可能なため、その活用方法は多岐にわたります。
+
+また、今後は AI を活用したアプリ開発が盛んになると予想されますので、これを機にぜひキャッチアップしてみてください。
+
+Skeet チュートリアルを進めると、オリジナルの AI チャットアプリを作成し、クラウド上にデプロイ、サービスを公開することが可能です。
+
+Skeet チュートリアル(YouTube): https://www.youtube.com/watch?v=6em68qcSsJE
+
+## どんなアプリがつくれるの?デモ AI チャットアプリを使って想像してみましょう
+
+![Skeet Demo AI Chat App](/news/2023/06/19/SkeetDemoPublished.png)
+
+Skeet は Firebase 上に自動スケールするアプリを構築できるフルスタックサーバーレスフレームワークです。
+
+今までアプリのリリースやサービス公開には、アプリケーションコードとサーバーの用意が必要で、特にサーバーの構築・管理には大きなコストがかかっていました。
+
+Google Cloud、Firebase の提供するサーバーレス環境はこのサーバー構築・管理を不要にします。すべてのサーバーインフラはユーザーの使用に合わせて自動でスケーリングするため、詳細なアクセス予想や負荷対策のリソース管理はもう必要ありません。
+
+Skeet は iOS・Android・Web アプリの開発のために、これらのサーバーレス製品をワンコマンドで構築・管理できます。そのため、Skeet 開発者はすぐにアプリケーションのロジックに取り掛かることが可能です。そして、書いたアプリのデプロイは保証されています。
+
+Skeet を使えば、OpenAI の ChatGPT API を活用したアプリケーションもすぐに構築しリリースすることが可能です。
+
+Skeet チュートリアルを完了させた状態のアプリをデモとして公開しています。
+
+Skeet デモ AI チャットアプリ: https://skeeter.app/
+
+このデモの様に、一般的にアプリケーションが行う認証やデータベースの利用等、アプリの開発及び公開に必要なものはすべて揃っています。
+
+バックエンド、フロントエンド共に TypeScript を利用していますが、必要に応じて Python 等もバックエンドとして関数毎に利用できるため、機械学習が必要になった場合にも対応できます。
+
+Skeet を活用してたくさんの役に立つアプリが生まれることを信じて、これからも開発・改善を続けてまいります。
+
+## 新しいアプリ開発を体感してください
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+Skeet を使えば、すぐに iOS・Android・Web アプリを開発し公開できます。
+
+チュートリアルでは、実際に あなたのオリジナル AI チャットアプリを構築し、すぐにリリースすることができます。
+
+ドキュメント形式のチュートリアルもございますので、ぜひお試しいただけますと幸いです。
+
+Skeet チュートリアル: https://skeet.dev/ja/doc/backend/quickstart/
diff --git a/typescript/skeet-graphql/articles/news/ja/2023/07/10/skeet-nextjs-template-released.md b/typescript/skeet-graphql/articles/news/ja/2023/07/10/skeet-nextjs-template-released.md
new file mode 100644
index 000000000000..81cf5fca6144
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/ja/2023/07/10/skeet-nextjs-template-released.md
@@ -0,0 +1,62 @@
+---
+id: skeet-nextjs-template-released
+title: TypeScript サーバーレスフレームワークのSkeetにNext.js (React) スターターを追加。新しいAIチャットアプリデモも公開。
+category: プレスリリース
+thumbnail: /news/2023/07/10/NewReleaseSkeetxNextjs.png
+---
+
+ELSOUL LABO B.V. (エルソウルラボ, 本社: オランダ・アムステルダム)は、TypeScript フルスタックサーバーレスフレームワークの Skeet に Next.js (React) スターターを追加した新しいリリースを発表しました。合わせて、同スターター及び ChatGPT API で作成された新しい AI チャットアプリのデモも公開されています。
+
+Skeet AI チャットアプリデモ: https://skeeter.dev/
+
+## Skeet Next.js (React) スターター
+
+![Skeet Next.js (React) Starter](/news/2023/07/10/WebAppBoilerplate.png)
+
+Skeet は Firebase 上に自動スケールするアプリを構築できる TypeScript 製のフルスタックサーバーレスフレームワークです。
+
+Skeet は素早くアプリを立ち上げ、少ないコストで長期的にメンテナンスしていくことを可能にします。
+
+この度は、リクエストの多かった Vercel 社の Next.js (https://nextjs.org/) スターターを Skeet フレームワークに追加いたしました。
+
+これにより、さらに高速な Web アプリ開発環境を手に入れることができます。
+
+この Next.js スターターは、SSG(スタティックサイトジェネレート) という技術を採用し、強い SEO 対応と高い性能、ホスティングコストの削減を実現しています。
+
+他にも多言語対応や PWA(アプリをダウンロードして利用可能)など、Web 開発に欠かせない設定はすでに完了しており、開発環境の準備に時間をかけず、すぐにあなたのアプリケーションコードに集中できます。
+
+Skeet Next (GitHub): https://github.com/elsoul/skeet-next
+
+## 新しい AI チャットアプリデモ (made by Next.js)
+
+![Skeet Demo AI Chat App](/news/2023/07/10/CreateChatRoom.png)
+
+同時にこの Next.js スターターと ChatGPT API を用いて新しい AI チャットアプリのデモをリリースしました。
+
+このデモアプリは、skeet create コマンドを実行すると、どのようなアプリが構築されるのかを説明しています。
+
+OpenAI は 7 月 7 日(米国時間)、同社の自然言語モデル「GPT-4」の API をすべての ChatGPT Plus ユーザーが利用可能になったことを発表しましたが、このデモアプリでも「GPT-4」の利用が可能です。
+
+AI アシスタントのキャラ設定をすることで、返答の質を向上させることも可能です。
+
+![Skeet Demo AI Chat App](/news/2023/07/10/ChatWithCodeHighlight.png)
+
+ChatGPT は開発者にとって頼れる味方となっています。
+
+今回のデモはプログラムコードのシンタックスハイライト(ルールに沿って色がつくため読みやすくなります)にも対応しており、トークン数等をカスタマイズすることでより精度の高い回答を得ることができます。
+
+Skeet AI チャットアプリデモ: https://skeeter.dev/
+
+## 新しいアプリ開発を体感してください
+
+![Skeet - Full-stack Serverless Framework for auto-scaling apps on Firebase](/news/2023/06/13/EffortlessServerlessSkeet.png)
+
+Skeet を使えば、すぐに iOS・Android・Web アプリを開発し公開できます。
+
+チュートリアルでは、実際に あなたのオリジナル AI チャットアプリを構築し、すぐにリリースすることができます。
+
+ぜひお試しいただけると嬉しいです。
+
+Skeet チュートリアル (ドキュメント): https://skeet.dev/ja/doc/backend/quickstart/
+
+Skeet チュートリアル (YouTube): https://www.youtube.com/watch?v=6em68qcSsJE
diff --git a/typescript/skeet-graphql/articles/news/ja/2023/08/01/skeet-nextjs-graphql-template-released.md b/typescript/skeet-graphql/articles/news/ja/2023/08/01/skeet-nextjs-graphql-template-released.md
new file mode 100644
index 000000000000..221ff485852a
--- /dev/null
+++ b/typescript/skeet-graphql/articles/news/ja/2023/08/01/skeet-nextjs-graphql-template-released.md
@@ -0,0 +1,51 @@
+---
+id: skeet-nextjs-graphql-template-released
+title: オープンソースのサーバーレスフレームワーク Skeet が GraphQL 及び SQL(リレーショナルデータベース) に対応しました
+category: プレスリリース
+thumbnail: /news/2023/08/01/skeet-graphql.png
+---
+
+ELSOUL LABO B.V. (エルソウルラボ, 本社: オランダ・アムステルダム) は 2023 年 8 月 1 日、アプリ開発を高速かつ低コストにするオープンソースのサーバーレスフレームワーク Skeet v1 のメジャーリリースを発表しました。本リリースによって、Skeet において GraphQL 及び SQL(リレーショナルデータベース) を利用しての開発が可能になりました。
+
+## 新しい Next.js (React) + GraphQL API サーバー オプションの追加
+
+![Skeet Next.js (React) + GraphQL Option](/news/2023/08/01/skeet-create-got-graphql.png)
+
+Skeet は GCP (Google Cloud) と Firebase 上にゼロメンテナンスアプリを構築できるオープンソースのサーバーレスフレームワークです。
+
+API から Web・iOS・Android アプリまでを TypeScript で超速開発することができます。
+
+今回のアップデートにより、GraphQL や SQL(リレーショナルデータベース) を活用したアプリケーションの開発にも対応しました。
+
+![Skeet Next.js (React) + GraphQL Starter](/news/2023/08/01/skeet-next-graphql.png)
+
+SQL(リレーショナルデータベース)への対応を通じて、SQL と NoSQL (Firestore) のハイブリッドな開発が可能になりました。
+これにより、開発者は両方のデータベースタイプのメリットを取り入れることが可能となります。
+
+リレーショナルデータベースは、関係性を持ったデータを扱うのに優れており、データ検索やトランザクション処理などでの整合性を保つことが容易です。一方で、NoSQL(Firestore など)は、柔軟性が高くスケーラビリティに優れているため、大量のデータや急速なデータ増加に対応するのに適しています。
+
+Skeet のハイブリッド開発環境により、データの関係性が重要なビジネスロジックはリレーショナルデータベースで、ユーザーデータやログなどの大量データは NoSQL でそれぞれ最適に管理することが可能となります。これは、一つのアプリケーション内で最高のパフォーマンスを引き出すための重要な戦略となり得ます。
+
+## データベースと API の可視化: Prisma と Apollo の活用
+
+このアップデートでは、Prisma と Apollo というツールを活用して、データベースや API を UI で可視化できるようになりました。これにより、開発者はコードを書くだけでなく、直感的にデータ構造を理解し、操作することができます。
+
+![Skeet Prisma Studio](/news/2023/08/01/prisma-studio.jpg)
+
+Prisma (https://www.prisma.io/) は、SQL(リレーショナルデータベース)を TypeScript や JavaScript で簡単に扱うことができる ORM です。Skeet では Prisma を用いてデータベースのスキーマをシンプルかつ柔軟に定義でき、マイグレーションは自動生成され、開発に役立つ TypeScript の型も提供されます。また、Prisma Studio はデータベースを GUI で操作することを可能にし、開発者がデータの操作や確認を容易に行うことができます。
+
+![Skeet Apollo Console](/news/2023/08/01/apollo-console.png)
+
+Apollo (https://www.apollographql.com/) は GraphQL API サーバーを開発するための強力なツールです。Apollo の開発者コンソールでは、GraphQL のスキーマを可視化し、リアルタイムで API のテストを行うことができます。
+
+新しい UI では、使いたい GraphQL Query や Mutation、データの内容をクリックするだけで、実際にフロントエンドで利用する GraphQL を生成できます。
+
+これらのツールの活用により、Skeet は開発者がより直感的かつ効率的にアプリケーションを開発するためのプラットフォームを提供します。このアップデートにより、開発者はさらなる生産性の向上を実現できます。
+
+Skeet は世界中すべてのアプリケーション開発現場の開発・メンテナンスコストを削減、開発者体験を向上させるためにオープンソースとして開発されています。
+
+Skeet を用いた最先端のアプリ開発をぜひ体験してみてください。
+
+Skeet ドキュメント: https://skeet.dev/ja/
+
+Skeet (GitHub): https://github.com/elsoul/skeet-cli
diff --git a/typescript/skeet-graphql/firebase.json b/typescript/skeet-graphql/firebase.json
new file mode 100644
index 000000000000..1da7fac8d7e8
--- /dev/null
+++ b/typescript/skeet-graphql/firebase.json
@@ -0,0 +1,45 @@
+{
+ "functions": [
+ {
+ "source": "functions/openai",
+ "codebase": "openai",
+ "ignore": [
+ "node_modules",
+ ".git",
+ "firebase-debug.log",
+ "firebase-debug.*.log"
+ ]
+ }
+ ],
+ "firestore": {
+ "rules": "firestore.rules",
+ "indexes": "firestore.indexes.json"
+ },
+ "storage": {
+ "rules": "storage.rules"
+ },
+ "emulators": {
+ "auth": {
+ "port": 9099
+ },
+ "functions": {
+ "port": 5001
+ },
+ "firestore": {
+ "port": 8080
+ },
+ "pubsub": {
+ "port": 8085
+ },
+ "hosting": {
+ "port": 8000
+ },
+ "storage": {
+ "port": 9199
+ },
+ "ui": {
+ "enabled": true
+ },
+ "singleProjectMode": true
+ }
+}
diff --git a/typescript/skeet-graphql/firestore.indexes.json b/typescript/skeet-graphql/firestore.indexes.json
new file mode 100644
index 000000000000..428b8e01d5d0
--- /dev/null
+++ b/typescript/skeet-graphql/firestore.indexes.json
@@ -0,0 +1,4 @@
+{
+ "indexes": [],
+ "fieldOverrides": []
+ }
\ No newline at end of file
diff --git a/typescript/skeet-graphql/firestore.rules b/typescript/skeet-graphql/firestore.rules
new file mode 100644
index 000000000000..547e76b9210d
--- /dev/null
+++ b/typescript/skeet-graphql/firestore.rules
@@ -0,0 +1,9 @@
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /User/{userId}/{document=**} {
+ allow read: if request.auth != null;
+ allow write: if request.auth.uid == userId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/typescript/skeet-graphql/functions/openai/.eslintignore b/typescript/skeet-graphql/functions/openai/.eslintignore
new file mode 100644
index 000000000000..8af5919ceea9
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/.eslintignore
@@ -0,0 +1,4 @@
+out
+dist
+build
+node_modules
\ No newline at end of file
diff --git a/typescript/skeet-graphql/functions/openai/.eslintrc.json b/typescript/skeet-graphql/functions/openai/.eslintrc.json
new file mode 100644
index 000000000000..bc61b1fe490e
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier"
+ ],
+ "parserOptions": {
+ "sourceType": "module",
+ "ecmaVersion": "latest"
+ },
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint"],
+ "env": {
+ "es6": true
+ },
+ "rules": {
+ "@typescript-eslint/no-unused-vars": 0,
+ "@typescript-eslint/ban-ts-comment": [
+ "off",
+ {
+ "ts-ignore": "allow-with-description"
+ }
+ ]
+ }
+}
diff --git a/typescript/skeet-graphql/functions/openai/.gcloudignore b/typescript/skeet-graphql/functions/openai/.gcloudignore
new file mode 100644
index 000000000000..5a616cba337e
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/.gcloudignore
@@ -0,0 +1,17 @@
+# This file specifies files that are *not* uploaded to Google Cloud
+# using gcloud. It follows the same syntax as .gitignore, with the addition of
+# "#!include" directives (which insert the entries of the given .gitignore-style
+# file at that point).
+#
+# For more information, run:
+# $ gcloud topic gcloudignore
+#
+.gcloudignore
+# If you would like to upload your .git directory, .gitignore file or files
+# from your .gitignore file, remove the corresponding line
+# below:
+.git
+.gitignore
+
+node_modules
+#!include:.gitignore
diff --git a/typescript/skeet-graphql/functions/openai/.gitignore b/typescript/skeet-graphql/functions/openai/.gitignore
new file mode 100644
index 000000000000..57411daba7f9
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/.gitignore
@@ -0,0 +1,12 @@
+# Compiled JavaScript files
+lib/**/*.js
+lib/**/*.js.map
+
+# TypeScript v1 declaration files
+typings/
+
+# Node.js dependency directory
+node_modules/
+.env
+*.log
+dist/
\ No newline at end of file
diff --git a/typescript/skeet-graphql/functions/openai/.prettierignore b/typescript/skeet-graphql/functions/openai/.prettierignore
new file mode 100755
index 000000000000..3e2c75a98609
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/.prettierignore
@@ -0,0 +1,4 @@
+.next
+out
+dist
+build
\ No newline at end of file
diff --git a/typescript/skeet-graphql/functions/openai/.prettierrc b/typescript/skeet-graphql/functions/openai/.prettierrc
new file mode 100755
index 000000000000..b2095be81e4e
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "semi": false,
+ "singleQuote": true
+}
diff --git a/typescript/skeet-graphql/functions/openai/build.ts b/typescript/skeet-graphql/functions/openai/build.ts
new file mode 100644
index 000000000000..4a5cf68cc2ab
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/build.ts
@@ -0,0 +1,14 @@
+import { build } from 'esbuild'
+;(async () => {
+ await build({
+ entryPoints: ['./src/index.ts'],
+ bundle: true,
+ minify: true,
+ outfile: './dist/index.js',
+ platform: 'node',
+ format: 'cjs',
+ define: {
+ 'process.env.NODE_ENV': `"production"`,
+ },
+ })
+})()
diff --git a/typescript/skeet-graphql/functions/openai/devBuild.ts b/typescript/skeet-graphql/functions/openai/devBuild.ts
new file mode 100644
index 000000000000..5de21d987107
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/devBuild.ts
@@ -0,0 +1,14 @@
+import { build } from 'esbuild'
+;(async () => {
+ await build({
+ entryPoints: ['./src/index.ts'],
+ bundle: true,
+ minify: true,
+ outfile: './dist/index.js',
+ platform: 'node',
+ define: {
+ 'process.env.NODE_ENV': `"development"`,
+ },
+ format: 'cjs',
+ })
+})()
diff --git a/typescript/skeet-graphql/functions/openai/nodemon.json b/typescript/skeet-graphql/functions/openai/nodemon.json
new file mode 100644
index 000000000000..f25686eb88a9
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/nodemon.json
@@ -0,0 +1,7 @@
+{
+ "watch": ["src"],
+ "ignore": ["src/**/*.test.ts", "node_modules"],
+ "ext": "ts,mjs,js,json",
+ "exec": "npx ts-node devBuild.ts && node ./dist/index.js",
+ "legacyWatch": true
+}
diff --git a/typescript/skeet-graphql/functions/openai/package.json b/typescript/skeet-graphql/functions/openai/package.json
new file mode 100644
index 000000000000..86e20253f2b1
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "skeet-functions",
+ "scripts": {
+ "lint": "eslint --ext .ts,.js --fix .",
+ "dev": "nodemon",
+ "dev:login": "npx ts-node -r tsconfig-paths/register --transpile-only src/scripts/login.ts",
+ "build": "npx ts-node build.ts",
+ "serve": "firebase emulators:start",
+ "shell": "yarn build && firebase functions:shell",
+ "start": "node dist/index.js",
+ "deploy": "firebase deploy --only functions",
+ "logs": "firebase functions:log",
+ "add:whiteList:production": "NODE_ENV=production npx ts-node -r tsconfig-paths/register --transpile-only src/scripts/addWhiteList.ts",
+ "add:whiteList:development": "NODE_ENV=development npx ts-node -r tsconfig-paths/register --transpile-only src/scripts/addWhiteList.ts"
+ },
+ "engines": {
+ "node": "18"
+ },
+ "main": "dist/index.js",
+ "dependencies": {
+ "@skeet-framework/pubsub": "0.1.2",
+ "@skeet-framework/utils": "^0.9.0",
+ "date-fns": "2.29.3",
+ "date-fns-tz": "2.0.0",
+ "dotenv": "16.0.3",
+ "firebase-admin": "11.9.0",
+ "firebase-functions": "4.4.1",
+ "node-fetch": "2.6.9",
+ "openai": "^3.3.0"
+ },
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "5.12.0",
+ "@typescript-eslint/parser": "5.12.0",
+ "esbuild": "0.17.14",
+ "eslint": "8.9.0",
+ "eslint-config-google": "0.14.0",
+ "eslint-plugin-import": "2.25.4",
+ "firebase": "9.21.0",
+ "nodemon": "2.0.20",
+ "prettier": "2.8.7",
+ "typescript": "5.0.4"
+ },
+ "private": true
+}
diff --git a/typescript/skeet-graphql/functions/openai/skeetOptions.json b/typescript/skeet-graphql/functions/openai/skeetOptions.json
new file mode 100644
index 000000000000..20d7ddd8184c
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/skeetOptions.json
@@ -0,0 +1,9 @@
+{
+ "projectId": "skeet-graphql",
+ "region": "asia-northeast1",
+ "appDomain": "skeet.dev",
+ "lbDomain": "sql.skeet.dev",
+ "nsDomain": "skeet.dev",
+ "fbProjectId": "skeet-graphql",
+ "name": "skeet-graphql"
+}
\ No newline at end of file
diff --git a/typescript/skeet-graphql/functions/openai/src/index.ts b/typescript/skeet-graphql/functions/openai/src/index.ts
new file mode 100644
index 000000000000..c6d5219c1d36
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/index.ts
@@ -0,0 +1,19 @@
+import admin from 'firebase-admin'
+import dotenv from 'dotenv'
+import { Request } from 'firebase-functions/v2/https'
+
+export interface TypedRequestBody extends Request {
+ body: T
+}
+
+dotenv.config()
+admin.initializeApp()
+
+export {
+ // This part is automatically generated by Skeet Framework.
+ // Please do not edit this part.
+ // Skeet Doc: https://skeet.dev
+ root,
+ authOnCreateUser,
+ createStreamChatMessage,
+} from '@/routings'
diff --git a/typescript/skeet-graphql/functions/openai/src/lib/getUserAuth.ts b/typescript/skeet-graphql/functions/openai/src/lib/getUserAuth.ts
new file mode 100644
index 000000000000..a1d9f2eb5a27
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/lib/getUserAuth.ts
@@ -0,0 +1,43 @@
+import { auth } from 'firebase-admin'
+import { Request } from 'firebase-functions/v2/https'
+import { gravatarIconUrl } from '@skeet-framework/utils'
+
+export type UserAuthType = {
+ uid: string
+ username: string
+ email: string
+ iconUrl: string
+}
+
+export const getUserAuth = async (req: Request) => {
+ try {
+ const token = req.headers.authorization
+ if (token == 'undefined' || token == null) throw new Error('Invalid token!')
+ const bearer = token.split('Bearer ')[1]
+ const user = await auth().verifyIdToken(bearer)
+ const { uid, displayName, email, photoURL } = user
+ const response: UserAuthType = {
+ uid,
+ username: displayName || email?.split('@')[0] || '',
+ email: email || '',
+ iconUrl:
+ photoURL == '' || !photoURL
+ ? gravatarIconUrl(email ?? 'info@skeet.dev')
+ : photoURL,
+ }
+ return response
+ } catch (error) {
+ throw new Error(`getUserAuth: ${error}`)
+ }
+}
+
+export const getUserBearerToken = async (req: Request) => {
+ try {
+ const token = req.headers.authorization
+ if (token == 'undefined' || token == null) throw new Error('Invalid token!')
+ const bearer = token.split('Bearer ')[1]
+ return bearer
+ } catch (error) {
+ throw new Error(`getUserBearerToken: ${error}`)
+ }
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/lib/openai/generateChatRoomTitle.ts b/typescript/skeet-graphql/functions/openai/src/lib/openai/generateChatRoomTitle.ts
new file mode 100644
index 000000000000..b2f86a0a577e
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/lib/openai/generateChatRoomTitle.ts
@@ -0,0 +1,62 @@
+import dotenv from 'dotenv'
+import { CreateChatCompletionRequest } from 'openai'
+import { chat } from './openAi'
+dotenv.config()
+
+export const generateChatRoomTitle = async (
+ content: string,
+ organization: string,
+ apiKey: string
+) => {
+ try {
+ const systemContent = systemContentJA
+ const createChatCompletionRequest: CreateChatCompletionRequest = {
+ model: 'gpt-3.5-turbo',
+ max_tokens: 500,
+ temperature: 0,
+ n: 1,
+ top_p: 1,
+ stream: false,
+ messages: [
+ {
+ role: 'system',
+ content: systemContent,
+ },
+ {
+ role: 'user',
+ content,
+ },
+ ],
+ }
+ const result = await chat(createChatCompletionRequest, organization, apiKey)
+ if (!result) throw new Error('result not found')
+ return result.content
+ } catch (error) {
+ throw new Error(`generateChatRoomTitle: ${error}`)
+ }
+}
+
+// const systemContentEN = `### Instructions ###
+// Give a title to the content of the message coming from the user. The maximum number of characters for the title is 50 characters. Please make the title as short and descriptive as possible. Do not ask users questions in interrogative sentences. Be sure to respond with only the title. Don't answer questions from users.
+// Here are some examples: Never answer user questions.
+// Example 1: User's question: [Question] I want to start learning Javascript.
+// Answer: How to start learning Javascript
+// Example 2: User's question: [Question] Can you write the code to create the file in Javascript?
+// Answer: How to create a file with JavaScript
+// Preferred response format: [content]
+// :`
+
+const systemContentJA = `### 指示 ###
+ユーザーから来るメッセージの内容にタイトルをつけます。タイトルの文字数は最大で50文字です。できるだけ短くわかりやすいタイトルをつけてください。疑問文でユーザーには質問しないでください。必ずタイトルのみをレスポンスしてください。ユーザーから来る質問には答えてはいけません。
+以下にいくつかの例を示します。絶対にユーザーの質問に答えてはいけません。すべて英語でメッセージが来た場合は英語のタイトルを付けてください。
+<重要>レスポンスはタイトルのみを返してください。
+例1: ユーザーからの質問: [質問]Javascriptの勉強を始めたいのですが、どうすればいいですか?
+ 答え: Javascriptの勉強の始め方
+例2: ユーザーからの質問: [質問]Javascriptでファイルを作成するコードを書いてくれますか?
+ 答え: JavaScriptでファイルを作成する方法
+例2: ユーザーからの質問: 今日も1日がんばるぞ!
+ 答え: 気合表明
+例3: ユーザーからの質問: あなたの今日の予定は?
+ 答え: 今日の予定
+望ましい回答フォーマット:<文章を要約したタイトル>
+<質問>:`
diff --git a/typescript/skeet-graphql/functions/openai/src/lib/openai/openAi.ts b/typescript/skeet-graphql/functions/openai/src/lib/openai/openAi.ts
new file mode 100644
index 000000000000..11437c1f704a
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/lib/openai/openAi.ts
@@ -0,0 +1,43 @@
+import { Configuration, CreateChatCompletionRequest, OpenAIApi } from 'openai'
+import { IncomingMessage } from 'http'
+
+export const chat = async (
+ createChatCompletionRequest: CreateChatCompletionRequest,
+ organization: string,
+ apiKey: string
+) => {
+ const configuration = new Configuration({
+ organization,
+ apiKey,
+ })
+ const openai = new OpenAIApi(configuration)
+ const completion = await openai.createChatCompletion(
+ createChatCompletionRequest
+ )
+ return completion.data.choices[0].message
+}
+
+export const streamChat = async (
+ createChatCompletionRequest: CreateChatCompletionRequest,
+ organization: string,
+ apiKey: string
+) => {
+ const configuration = new Configuration({
+ organization,
+ apiKey,
+ })
+ const openai = new OpenAIApi(configuration)
+ try {
+ const result = await openai.createChatCompletion(
+ createChatCompletionRequest,
+ {
+ responseType: 'stream',
+ }
+ )
+
+ const stream = result.data as unknown as IncomingMessage
+ return stream
+ } catch (error) {
+ throw new Error(`streamChat error: ${error}`)
+ }
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/models/index.ts b/typescript/skeet-graphql/functions/openai/src/models/index.ts
new file mode 100644
index 000000000000..a7dfc4b6249d
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/models/index.ts
@@ -0,0 +1 @@
+export * from './sql'
diff --git a/typescript/skeet-graphql/functions/openai/src/models/sql/index.ts b/typescript/skeet-graphql/functions/openai/src/models/sql/index.ts
new file mode 100644
index 000000000000..12f05056d3f0
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/models/sql/index.ts
@@ -0,0 +1 @@
+export * from './userModel'
diff --git a/typescript/skeet-graphql/functions/openai/src/models/sql/userModel.ts b/typescript/skeet-graphql/functions/openai/src/models/sql/userModel.ts
new file mode 100644
index 000000000000..a55dd498121d
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/models/sql/userModel.ts
@@ -0,0 +1,7 @@
+export interface User {
+ uid: string
+ email: string
+ username?: string
+ iconUrl?: string
+ iv?: string
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/auth/authOnCreateUser.ts b/typescript/skeet-graphql/functions/openai/src/routings/auth/authOnCreateUser.ts
new file mode 100644
index 000000000000..08b663ac66a7
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/auth/authOnCreateUser.ts
@@ -0,0 +1,62 @@
+import * as functions from 'firebase-functions/v1'
+import { authPublicOption } from '@/routings'
+import {
+ gravatarIconUrl,
+ sendDiscord,
+ skeetGraphql,
+} from '@skeet-framework/utils'
+import skeetConfig from '../../../skeetOptions.json'
+import { User } from '@/models'
+import { defineSecret } from 'firebase-functions/params'
+import { inspect } from 'util'
+const DISCORD_WEBHOOK_URL = defineSecret('DISCORD_WEBHOOK_URL')
+const SKEET_GRAPHQL_ENDPOINT_URL = defineSecret('SKEET_GRAPHQL_ENDPOINT_URL')
+
+const { region } = skeetConfig
+const queryName = 'createUser'
+
+export const authOnCreateUser = functions
+ .runWith({
+ ...authPublicOption,
+ secrets: [DISCORD_WEBHOOK_URL, SKEET_GRAPHQL_ENDPOINT_URL],
+ })
+ .region(region)
+ .auth.user()
+ .onCreate(async (user) => {
+ try {
+ if (!user.email) throw new Error(`no email`)
+ const { uid, email, displayName, photoURL } = user
+ const userParams: User = {
+ uid,
+ email: email,
+ username: displayName || email?.split('@')[0],
+ iconUrl:
+ photoURL == '' || !photoURL
+ ? gravatarIconUrl(email ?? 'info@skeet.dev')
+ : photoURL,
+ }
+
+ console.log({ userParams })
+ const accessToken = 'skeet-access-token'
+ const createUserResponse = await skeetGraphql(
+ accessToken,
+ SKEET_GRAPHQL_ENDPOINT_URL.value(),
+ 'mutation',
+ queryName,
+ userParams
+ )
+
+ console.log(
+ inspect(createUserResponse, false, null, true /* enable colors */)
+ )
+
+ // Send Discord message when new user is created
+ const content = `Skeet APP New user: ${userParams.username} \nemail: ${userParams.email}\niconUrl: ${userParams.iconUrl}`
+ if (process.env.NODE_ENV === 'production') {
+ await sendDiscord(content, DISCORD_WEBHOOK_URL.value())
+ }
+ console.log({ status: 'success' })
+ } catch (error) {
+ console.log({ status: 'error', message: String(error) })
+ }
+ })
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/auth/index.ts b/typescript/skeet-graphql/functions/openai/src/routings/auth/index.ts
new file mode 100644
index 000000000000..f12740fa5c97
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/auth/index.ts
@@ -0,0 +1 @@
+export * from './authOnCreateUser'
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/firestore/firestoreExample.ts b/typescript/skeet-graphql/functions/openai/src/routings/firestore/firestoreExample.ts
new file mode 100644
index 000000000000..e7877b6fc507
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/firestore/firestoreExample.ts
@@ -0,0 +1,14 @@
+import { onDocumentCreated } from 'firebase-functions/v2/firestore'
+import { firestoreDefaultOption } from '@/routings/options'
+
+export const firestoreExample = onDocumentCreated(
+ firestoreDefaultOption('User/{userId}'),
+ (event) => {
+ console.log('firestoreExample triggered')
+ try {
+ console.log(event.params)
+ } catch (error) {
+ console.log({ status: 'error', message: String(error) })
+ }
+ }
+)
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/firestore/index.ts b/typescript/skeet-graphql/functions/openai/src/routings/firestore/index.ts
new file mode 100644
index 000000000000..673633e02898
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/firestore/index.ts
@@ -0,0 +1 @@
+export * from './firestoreExample'
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/http/createStreamChatMessage.ts b/typescript/skeet-graphql/functions/openai/src/routings/http/createStreamChatMessage.ts
new file mode 100644
index 000000000000..a02da030d903
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/http/createStreamChatMessage.ts
@@ -0,0 +1,230 @@
+import { onRequest } from 'firebase-functions/v2/https'
+import { CreateChatCompletionRequest } from 'openai'
+import { streamChat } from '@/lib/openai/openAi'
+import { TypedRequestBody } from '@/index'
+import { getUserBearerToken } from '@/lib/getUserAuth'
+import { publicHttpOption } from '@/routings'
+import { defineSecret } from 'firebase-functions/params'
+import { skeetGraphql, sleep } from '@skeet-framework/utils'
+import {
+ CreateChatMessageParams,
+ CreateStreamChatMessageParams,
+ GetChatMessagesParams,
+ GetChatRoomParams,
+ UpdateChatRoomTitleParams,
+} from '@/types/http/createStreamChatMessageParams'
+import { inspect } from 'util'
+import { generateChatRoomTitle } from '@/lib/openai/generateChatRoomTitle'
+const chatGptOrg = defineSecret('CHAT_GPT_ORG')
+const chatGptKey = defineSecret('CHAT_GPT_KEY')
+const SKEET_GRAPHQL_ENDPOINT_URL = defineSecret('SKEET_GRAPHQL_ENDPOINT_URL')
+type ChatRoomParams = {
+ model: string
+ maxTokens: number
+ temperature: number
+ stream: boolean
+ title: string
+}
+
+type ChatRoomMessage = {
+ id: string
+ role: string
+ content: string
+}
+
+type ChatMessagesParams = ChatRoomMessage[]
+
+export const createStreamChatMessage = onRequest(
+ {
+ ...publicHttpOption,
+ secrets: [chatGptOrg, chatGptKey, SKEET_GRAPHQL_ENDPOINT_URL],
+ },
+ async (req: TypedRequestBody, res) => {
+ const organization = chatGptOrg.value()
+ const apiKey = chatGptKey.value()
+ if (!organization || !apiKey)
+ throw new Error(
+ `ChatGPT organization or apiKey is empty\nPlease run \`skeet add secret CHAT_GPT_ORG/CHAT_GPT_KEY\``
+ )
+
+ // Get Request Body
+ console.log(inspect(req.body, { depth: null }))
+ const body = {
+ chatRoomId: req.body.chatRoomId || '',
+ content: req.body.content,
+ }
+ if (body.chatRoomId === '') throw new Error('chatRoomId is empty')
+
+ // Get User Info from Firebase Auth
+ const token = await getUserBearerToken(req)
+ const getChatRoomBody: GetChatRoomParams = {
+ id: body.chatRoomId,
+ }
+
+ try {
+ // Get ChatRoom Info from GraphQL
+ const queryType = 'query'
+ const queryName = 'getChatRoom'
+ const chatRoom = await skeetGraphql(
+ token,
+ SKEET_GRAPHQL_ENDPOINT_URL.value(),
+ queryType,
+ queryName,
+ getChatRoomBody,
+ ['model', 'maxTokens', 'temperature', 'stream', 'title']
+ )
+ console.log(inspect(chatRoom, { depth: null }))
+
+ // Create ChatRoomMessage
+ const createMessageQueryName = 'createChatRoomMessage'
+ const params: CreateChatMessageParams = {
+ chatRoomId: body.chatRoomId,
+ role: 'user',
+ content: body.content,
+ }
+
+ const result = await skeetGraphql(
+ token,
+ SKEET_GRAPHQL_ENDPOINT_URL.value(),
+ 'mutation',
+ createMessageQueryName,
+ params,
+ ['id', 'role', 'content']
+ )
+ console.log(inspect(result, { depth: null }))
+
+ const queryName2 = 'getChatRoomMessages'
+ const params2 = {
+ chatRoomId: body.chatRoomId,
+ }
+ const chatMessages = await skeetGraphql<
+ GetChatMessagesParams,
+ ChatMessagesParams
+ >(
+ token,
+ SKEET_GRAPHQL_ENDPOINT_URL.value(),
+ queryType,
+ queryName2,
+ params2,
+ ['role', 'content']
+ )
+ console.log(inspect(chatMessages, { depth: null }))
+ const messages = chatMessages.data
+ .getChatRoomMessages as CreateChatCompletionRequest['messages']
+ // Update UserChatRoom Title
+ if (
+ chatRoom.data.getChatRoom.title == '' ||
+ !chatRoom.data.getChatRoom.title
+ ) {
+ const title = await generateChatRoomTitle(
+ body.content,
+ organization,
+ apiKey
+ )
+
+ if (title) {
+ const params: UpdateChatRoomTitleParams = {
+ id: body.chatRoomId,
+ title,
+ }
+ await skeetGraphql(
+ token,
+ SKEET_GRAPHQL_ENDPOINT_URL.value(),
+ 'mutation',
+ 'updateChatRoom',
+ params,
+ ['id', 'title']
+ )
+ }
+ }
+
+ // Send Request to OpenAI
+ const openAiBody: CreateChatCompletionRequest = {
+ model: String(chatRoom.data.getChatRoom.model),
+ max_tokens: Number(chatRoom.data.getChatRoom.maxTokens),
+ temperature: Number(chatRoom.data.getChatRoom.temperature),
+ n: 1,
+ top_p: 1,
+ stream: Boolean(chatRoom.data.getChatRoom.stream),
+ messages,
+ }
+ console.log('openAiBody')
+ console.log(inspect(openAiBody, { depth: null }))
+
+ // Get OpenAI Stream
+ const stream = await streamChat(openAiBody, organization, apiKey)
+ const messageResults: string[] = []
+ let streamClosed = false
+
+ res.once('error', () => (streamClosed = true))
+ res.once('close', () => (streamClosed = true))
+ stream.on('data', async (chunk: Buffer) => {
+ const payloads = chunk.toString().split('\n\n')
+ for await (const payload of payloads) {
+ if (payload.includes('[DONE]')) return
+ if (payload.startsWith('data:')) {
+ const data = payload.replaceAll(/(\n)?^data:\s*/g, '')
+ try {
+ const delta = JSON.parse(data.trim())
+ const message = delta.choices[0].delta?.content
+ if (message == undefined) continue
+
+ // Log Message
+ console.log(message)
+ messageResults.push(message)
+
+ while (!streamClosed && res.writableLength > 0) {
+ await sleep(10)
+ }
+
+ // Send Message to Client
+ res.write(JSON.stringify({ text: message }))
+ } catch (error) {
+ console.log(`Error with JSON.parse and ${payload}.\n${error}`)
+ }
+ }
+ }
+ if (streamClosed) res.end('Stream disconnected')
+ })
+
+ // Stream End
+ stream.on('end', async () => {
+ const message = messageResults.join('')
+ // Send Message to Client
+
+ const params: CreateChatMessageParams = {
+ chatRoomId: body.chatRoomId,
+ role: 'assistant',
+ content: message,
+ }
+
+ const result = await skeetGraphql(
+ token,
+ SKEET_GRAPHQL_ENDPOINT_URL.value(),
+ 'mutation',
+ createMessageQueryName,
+ params,
+ ['id', 'role', 'content']
+ )
+ console.log('got result')
+ console.log(inspect(result, { depth: null }))
+ res.end('Stream done')
+ })
+ stream.on('error', (e: Error) => console.error(e))
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes('Please ask to join the whitelist.') &&
+ !error.message.includes('userChatRoomId is empty') &&
+ !error.message.includes('stream must be true') &&
+ !error.message.includes(
+ `ChatGPT organization or apiKey is empty\nPlease run \`skeet add secret CHAT_GPT_ORG/CHAT_GPT_KEY\``
+ )
+ ) {
+ console.error(`OpenAI API Connection Error - ${error}`)
+ }
+
+ res.status(500).json({ status: 'error', message: String(error) })
+ }
+ }
+)
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/http/index.ts b/typescript/skeet-graphql/functions/openai/src/routings/http/index.ts
new file mode 100644
index 000000000000..4b53320d1266
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/http/index.ts
@@ -0,0 +1,2 @@
+export * from './root'
+export * from './createStreamChatMessage'
\ No newline at end of file
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/http/root.ts b/typescript/skeet-graphql/functions/openai/src/routings/http/root.ts
new file mode 100644
index 000000000000..06328d81a867
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/http/root.ts
@@ -0,0 +1,20 @@
+import { onRequest } from 'firebase-functions/v2/https'
+import { publicHttpOption } from '@/routings/options'
+import { TypedRequestBody } from '@/index'
+import { RootParams } from '@/types/http/rootParams'
+
+export const root = onRequest(
+ publicHttpOption,
+ async (req: TypedRequestBody, res) => {
+ try {
+ const body = req.body
+ res.json({
+ status: 'success',
+ message: 'Skeet Backend is running!',
+ body,
+ })
+ } catch (error) {
+ res.status(500).json({ status: 'error', message: String(error) })
+ }
+ }
+)
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/index.ts b/typescript/skeet-graphql/functions/openai/src/routings/index.ts
new file mode 100644
index 000000000000..99cb0f2e15ea
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/index.ts
@@ -0,0 +1,6 @@
+export * from './http'
+export * from './pubsub'
+export * from './schedule'
+export * from './options'
+export * from './auth'
+export * from './firestore'
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/options/authOptions.ts b/typescript/skeet-graphql/functions/openai/src/routings/options/authOptions.ts
new file mode 100644
index 000000000000..793ffa3f6c8b
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/options/authOptions.ts
@@ -0,0 +1,32 @@
+import { RuntimeOptions } from 'firebase-functions/v1'
+import skeetOptions from '../../../skeetOptions.json'
+
+const appName = skeetOptions.name
+const project = skeetOptions.projectId
+
+const serviceAccount = `${appName}@${project}.iam.gserviceaccount.com`
+const vpcConnector = `${appName}-con`
+
+export const authPublicOption: RuntimeOptions = {
+ memory: '1GB',
+ maxInstances: 100,
+ minInstances: 0,
+ timeoutSeconds: 300,
+ labels: {
+ skeet: 'auth',
+ },
+}
+
+export const authPrivateOption: RuntimeOptions = {
+ memory: '1GB',
+ maxInstances: 100,
+ minInstances: 0,
+ timeoutSeconds: 300,
+ serviceAccount,
+ ingressSettings: 'ALLOW_INTERNAL_ONLY',
+ vpcConnector,
+ vpcConnectorEgressSettings: 'PRIVATE_RANGES_ONLY',
+ labels: {
+ skeet: 'auth',
+ },
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/options/firestoreOptions.ts b/typescript/skeet-graphql/functions/openai/src/routings/options/firestoreOptions.ts
new file mode 100644
index 000000000000..739c9bf331fd
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/options/firestoreOptions.ts
@@ -0,0 +1,26 @@
+import { DocumentOptions } from 'firebase-functions/v2/firestore'
+import skeetOptions from '../../../skeetOptions.json'
+
+const appName = skeetOptions.name
+const project = skeetOptions.projectId
+const region = skeetOptions.region
+const serviceAccount = `${appName}@${project}.iam.gserviceaccount.com`
+const vpcConnector = `${appName}-con`
+
+export const firestoreDefaultOption = (document: string): DocumentOptions => ({
+ document,
+ region,
+ cpu: 1,
+ memory: '1GiB',
+ maxInstances: 100,
+ minInstances: 0,
+ concurrency: 1,
+ serviceAccount,
+ ingressSettings: 'ALLOW_INTERNAL_ONLY',
+ vpcConnector,
+ vpcConnectorEgressSettings: 'PRIVATE_RANGES_ONLY',
+ timeoutSeconds: 540,
+ labels: {
+ skeet: 'firestore',
+ },
+})
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/options/httpOptions.ts b/typescript/skeet-graphql/functions/openai/src/routings/options/httpOptions.ts
new file mode 100644
index 000000000000..9a65d32b94c8
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/options/httpOptions.ts
@@ -0,0 +1,40 @@
+import { HttpsOptions } from 'firebase-functions/v2/https'
+import skeetOptions from '../../../skeetOptions.json'
+
+const appName = skeetOptions.name
+const project = skeetOptions.projectId
+const region = skeetOptions.region
+const serviceAccount = `${appName}@${project}.iam.gserviceaccount.com`
+const vpcConnector = `${appName}-con`
+const cors = true
+
+export const publicHttpOption: HttpsOptions = {
+ region,
+ cpu: 1,
+ memory: '1GiB',
+ maxInstances: 100,
+ minInstances: 0,
+ concurrency: 1,
+ timeoutSeconds: 540,
+ labels: {
+ skeet: 'http',
+ },
+}
+
+export const privateHttpOption: HttpsOptions = {
+ region,
+ cpu: 1,
+ memory: '2GiB',
+ maxInstances: 100,
+ minInstances: 0,
+ concurrency: 80,
+ serviceAccount,
+ ingressSettings: 'ALLOW_INTERNAL_AND_GCLB',
+ vpcConnector,
+ vpcConnectorEgressSettings: 'PRIVATE_RANGES_ONLY',
+ cors,
+ timeoutSeconds: 540,
+ labels: {
+ skeet: 'http',
+ },
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/options/index.ts b/typescript/skeet-graphql/functions/openai/src/routings/options/index.ts
new file mode 100644
index 000000000000..bcf98cb99851
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/options/index.ts
@@ -0,0 +1,5 @@
+export * from './httpOptions'
+export * from './pubsubOptions'
+export * from './firestoreOptions'
+export * from './scheduleOptions'
+export * from './authOptions'
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/options/pubsubOptions.ts b/typescript/skeet-graphql/functions/openai/src/routings/options/pubsubOptions.ts
new file mode 100644
index 000000000000..a6497bcdec74
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/options/pubsubOptions.ts
@@ -0,0 +1,26 @@
+import { PubSubOptions } from 'firebase-functions/v2/pubsub'
+import skeetOptions from '../../../skeetOptions.json'
+
+const appName = skeetOptions.name
+const project = skeetOptions.projectId
+const region = skeetOptions.region
+const serviceAccount = `${appName}@${project}.iam.gserviceaccount.com`
+const vpcConnector = `${appName}-con`
+
+export const pubsubDefaultOption = (topic: string): PubSubOptions => ({
+ topic,
+ region,
+ cpu: 1,
+ memory: '1GiB',
+ maxInstances: 100,
+ minInstances: 0,
+ concurrency: 1,
+ serviceAccount,
+ ingressSettings: 'ALLOW_INTERNAL_ONLY',
+ vpcConnector,
+ vpcConnectorEgressSettings: 'PRIVATE_RANGES_ONLY',
+ timeoutSeconds: 540,
+ labels: {
+ skeet: 'pubsub',
+ },
+})
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/options/scheduleOptions.ts b/typescript/skeet-graphql/functions/openai/src/routings/options/scheduleOptions.ts
new file mode 100644
index 000000000000..49b89cdd2cea
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/options/scheduleOptions.ts
@@ -0,0 +1,26 @@
+import { ScheduleOptions } from 'firebase-functions/v2/scheduler'
+import skeetOptions from '../../../skeetOptions.json'
+
+const appName = skeetOptions.name
+const project = skeetOptions.projectId
+const region = skeetOptions.region
+const serviceAccount = `${appName}@${project}.iam.gserviceaccount.com`
+const vpcConnector = `${appName}-con`
+
+export const scheduleDefaultOption: ScheduleOptions = {
+ region,
+ schedule: 'every 1 hours',
+ timeZone: 'UTC',
+ retryCount: 3,
+ maxRetrySeconds: 60,
+ minBackoffSeconds: 1,
+ maxBackoffSeconds: 10,
+ serviceAccount,
+ ingressSettings: 'ALLOW_INTERNAL_ONLY',
+ vpcConnector,
+ vpcConnectorEgressSettings: 'PRIVATE_RANGES_ONLY',
+ timeoutSeconds: 540,
+ labels: {
+ skeet: 'schedule',
+ },
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/pubsub/index.ts b/typescript/skeet-graphql/functions/openai/src/routings/pubsub/index.ts
new file mode 100644
index 000000000000..8ccbdd2e66e5
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/pubsub/index.ts
@@ -0,0 +1 @@
+export * from './pubsubExample'
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/pubsub/pubsubExample.ts b/typescript/skeet-graphql/functions/openai/src/routings/pubsub/pubsubExample.ts
new file mode 100644
index 000000000000..bd2ed2302737
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/pubsub/pubsubExample.ts
@@ -0,0 +1,23 @@
+import { onMessagePublished } from 'firebase-functions/v2/pubsub'
+import { pubsubDefaultOption } from '@/routings/options'
+import { parsePubSubMessage } from '@skeet-framework/pubsub'
+import { PubsubExampleParams } from '@/types/pubsub/pubsubExampleParams'
+
+export const pubsubTopic = 'pubsubExample'
+
+export const pubsubExample = onMessagePublished(
+ pubsubDefaultOption(pubsubTopic),
+ async (event) => {
+ try {
+ const pubsubObject = parsePubSubMessage(event)
+ console.log({
+ status: 'success',
+ topic: pubsubTopic,
+ event,
+ pubsubObject,
+ })
+ } catch (error) {
+ console.error({ status: 'error', message: String(error) })
+ }
+ }
+)
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/schedule/index.ts b/typescript/skeet-graphql/functions/openai/src/routings/schedule/index.ts
new file mode 100644
index 000000000000..19f22d4089b2
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/schedule/index.ts
@@ -0,0 +1 @@
+export * from './scheduleExample'
diff --git a/typescript/skeet-graphql/functions/openai/src/routings/schedule/scheduleExample.ts b/typescript/skeet-graphql/functions/openai/src/routings/schedule/scheduleExample.ts
new file mode 100644
index 000000000000..7bebf9f6147c
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/routings/schedule/scheduleExample.ts
@@ -0,0 +1,13 @@
+import { onSchedule } from 'firebase-functions/v2/scheduler'
+import { scheduleDefaultOption } from '@/routings/options'
+
+export const scheduleExample = onSchedule(
+ scheduleDefaultOption,
+ async (event) => {
+ try {
+ console.log({ status: 'success' })
+ } catch (error) {
+ console.log({ status: 'error', message: String(error) })
+ }
+ }
+)
diff --git a/typescript/skeet-graphql/functions/openai/src/types/http/createStreamChatMessageParams.ts b/typescript/skeet-graphql/functions/openai/src/types/http/createStreamChatMessageParams.ts
new file mode 100644
index 000000000000..03890da32bd4
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/types/http/createStreamChatMessageParams.ts
@@ -0,0 +1,23 @@
+export type CreateStreamChatMessageParams = {
+ chatRoomId: string
+ content: string
+}
+
+export type GetChatRoomParams = {
+ id: string
+}
+
+export type GetChatMessagesParams = {
+ chatRoomId: string
+}
+
+export type CreateChatMessageParams = {
+ role: string
+ content: string
+ chatRoomId: string
+}
+
+export type UpdateChatRoomTitleParams = {
+ id: string
+ title: string
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/types/http/rootParams.ts b/typescript/skeet-graphql/functions/openai/src/types/http/rootParams.ts
new file mode 100644
index 000000000000..843ede34c3d5
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/types/http/rootParams.ts
@@ -0,0 +1,3 @@
+export type RootParams = {
+ name?: string
+}
diff --git a/typescript/skeet-graphql/functions/openai/src/types/pubsub/pubsubExampleParams.ts b/typescript/skeet-graphql/functions/openai/src/types/pubsub/pubsubExampleParams.ts
new file mode 100644
index 000000000000..bc5f18d313bd
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/src/types/pubsub/pubsubExampleParams.ts
@@ -0,0 +1,3 @@
+export type PubsubExampleParams = {
+ name?: string
+}
diff --git a/typescript/skeet-graphql/functions/openai/tsconfig.json b/typescript/skeet-graphql/functions/openai/tsconfig.json
new file mode 100644
index 000000000000..66bbc0137d02
--- /dev/null
+++ b/typescript/skeet-graphql/functions/openai/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "noImplicitReturns": true,
+ "noUnusedLocals": true,
+ "outDir": "dist",
+ "target": "ESNext",
+ "rootDir": ".",
+ "strict": true,
+ "moduleResolution": "node",
+ "baseUrl": ".",
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "lib": ["esnext"],
+ "sourceMap": true,
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "compileOnSave": true,
+ "include": ["src/*", "src/**/*"]
+}
diff --git a/typescript/skeet-graphql/github/workflows/firebase-rules.yml b/typescript/skeet-graphql/github/workflows/firebase-rules.yml
new file mode 100644
index 000000000000..02792f4f12c6
--- /dev/null
+++ b/typescript/skeet-graphql/github/workflows/firebase-rules.yml
@@ -0,0 +1,32 @@
+name: FirebaseRules
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'firestore.rules'
+ - 'firestore.indexes.json'
+ - 'storage.rules'
+ - '.github/workflows/firebase-rules.yml'
+
+jobs:
+ deploy:
+ name: Deploy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v2
+ - name: Install Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '18.16.0'
+ - id: auth
+ uses: google-github-actions/auth@v0
+ with:
+ credentials_json: ${{ secrets.SKEET_GCP_SA_KEY }}
+ - name: Install firebase tools
+ run: npm i -g npm firebase-tools
+ - name: GitHub repository setting
+ run: git config --global url."https://github.com".insteadOf ssh://git@github.com
+ - name: Deploy rules to Firebase
+ run: firebase deploy --only firestore:rules,storage
diff --git a/typescript/skeet-graphql/github/workflows/functions-openai.yml b/typescript/skeet-graphql/github/workflows/functions-openai.yml
new file mode 100644
index 000000000000..9b9b392cee81
--- /dev/null
+++ b/typescript/skeet-graphql/github/workflows/functions-openai.yml
@@ -0,0 +1,34 @@
+name: Openai
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'functions/skeet/**'
+ - '.github/workflows/functions-openai.yml'
+
+jobs:
+ deploy:
+ name: Deploy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v2
+ - name: Install Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '18.16.0'
+ - id: auth
+ uses: google-github-actions/auth@v0
+ with:
+ credentials_json: ${{ secrets.SKEET_GCP_SA_KEY }}
+ - name: Install yarn and firebase tools
+ run: npm i -g npm yarn firebase-tools
+ - name: GitHub repository setting
+ run: git config --global url."https://github.com".insteadOf ssh://git@github.com
+ - name: Install dependencies
+ run: cd ./functions/openai && yarn install --frozen-lockfile
+ - name: Build App
+ run: cd ./functions/openai && yarn build
+ - name: Deploy to Firebase
+ run: firebase deploy --only functions:openai
diff --git a/typescript/skeet-graphql/github/workflows/webapp.yml b/typescript/skeet-graphql/github/workflows/webapp.yml
new file mode 100644
index 000000000000..d71f0009a4ca
--- /dev/null
+++ b/typescript/skeet-graphql/github/workflows/webapp.yml
@@ -0,0 +1,42 @@
+name: WebApp
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'src/**'
+ - 'assets/**'
+ - 'lib/**'
+ - '.github/workflows/webapp.yml'
+ - 'package.json'
+ - 'firebase.json'
+ - '.firebaserc'
+ - 'app.json'
+ - 'metro.config.js'
+ - 'babel.config.js'
+
+jobs:
+ deploy:
+ name: Deploy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v2
+ - name: Install Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '18.16.0'
+ - id: auth
+ uses: google-github-actions/auth@v0
+ with:
+ credentials_json: ${{ secrets.SKEET_GCP_SA_KEY }}
+ - name: Install yarn and firebase tools
+ run: npm i -g npm yarn firebase-tools
+ - name: GitHub repository setting
+ run: git config --global url."https://github.com".insteadOf ssh://git@github.com
+ - name: Install dependencies
+ run: yarn install --frozen-lockfile
+ - name: Build App
+ run: yarn build:production:webapp
+ - name: Deploy to Firebase
+ run: firebase deploy --only hosting
diff --git a/typescript/skeet-graphql/graphql/.eslintignore b/typescript/skeet-graphql/graphql/.eslintignore
new file mode 100755
index 000000000000..43c5ccae9b7b
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/.eslintignore
@@ -0,0 +1,6 @@
+.next
+out
+dist
+build
+src/__generated__
+src/schema.graphql
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/.eslintrc.json b/typescript/skeet-graphql/graphql/.eslintrc.json
new file mode 100755
index 000000000000..ec0429895ce6
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/.eslintrc.json
@@ -0,0 +1,28 @@
+{
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier"
+ ],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint"],
+ "env": {
+ "es6": true
+ },
+ "rules": {
+ "@typescript-eslint/no-explicit-any": 0,
+ "@typescript-eslint/no-var-requires": 0,
+ "@typescript-eslint/no-unused-vars": 0,
+ "@typescript-eslint/no-empty-function": 0,
+ "@typescript-eslint/ban-ts-comment": [
+ "off",
+ {
+ "ts-ignore": "allow-with-description"
+ }
+ ]
+ }
+}
diff --git a/typescript/skeet-graphql/graphql/.gitignore b/typescript/skeet-graphql/graphql/.gitignore
new file mode 100755
index 000000000000..48b8560b6b71
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/.gitignore
@@ -0,0 +1,50 @@
+# 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
+/dist
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+.env
+.env.build
+.env.production
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+
+# key
+keyfile.json
+
+#firebase
+.firebase
+firebase-debug.log
+
+#coverage
+/coverage_dir
+
diff --git a/typescript/skeet-graphql/graphql/.prettierignore b/typescript/skeet-graphql/graphql/.prettierignore
new file mode 100755
index 000000000000..43c5ccae9b7b
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/.prettierignore
@@ -0,0 +1,6 @@
+.next
+out
+dist
+build
+src/__generated__
+src/schema.graphql
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/.prettierrc b/typescript/skeet-graphql/graphql/.prettierrc
new file mode 100755
index 000000000000..8760b0fa226e
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/.prettierrc
@@ -0,0 +1,14 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "overrides": [
+ {
+ "files": "*.prisma",
+ "options": {
+ "parser": "prisma",
+ "printWidth": 100,
+ "endOfLine": "auto"
+ }
+ }
+ ]
+}
diff --git a/typescript/skeet-graphql/graphql/Dockerfile b/typescript/skeet-graphql/graphql/Dockerfile
new file mode 100755
index 000000000000..b37c1f6737b6
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/Dockerfile
@@ -0,0 +1,24 @@
+FROM node:18.14-alpine AS build
+
+WORKDIR /app
+
+COPY package* yarn.lock ./
+COPY prisma ./prisma/
+COPY tsconfig.json ./
+COPY . .
+RUN yarn install --frozen-lockfile
+RUN npx prisma generate
+RUN yarn build
+
+FROM node:18.14-alpine
+
+WORKDIR /app
+
+RUN apk add openssl
+COPY package.json yarn.lock ./
+RUN yarn install --frozen-lockfile --production && yarn cache clean
+COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma
+COPY --from=build /app/prisma /app/prisma
+COPY --from=build /app/dist /app/dist
+RUN npx prisma generate
+CMD ["yarn", "start"]
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/build.ts b/typescript/skeet-graphql/graphql/build.ts
new file mode 100644
index 000000000000..46acd57efd03
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/build.ts
@@ -0,0 +1,15 @@
+import { build } from 'esbuild'
+;(async () => {
+ const res = await build({
+ entryPoints: ['./src/index.ts'],
+ bundle: true,
+ minify: true,
+ outfile: './dist/index.js',
+ platform: 'node',
+ format: 'cjs',
+ define: {
+ 'process.env.NODE_ENV': `"production"`,
+ },
+ external: ['graphql', '@prisma/client'],
+ })
+})()
diff --git a/typescript/skeet-graphql/graphql/devBuild.ts b/typescript/skeet-graphql/graphql/devBuild.ts
new file mode 100644
index 000000000000..cbfbe04c4616
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/devBuild.ts
@@ -0,0 +1,15 @@
+import { build } from 'esbuild'
+;(async () => {
+ const res = await build({
+ entryPoints: ['./src/index.ts'],
+ bundle: true,
+ minify: true,
+ outfile: './dist/index.js',
+ platform: 'node',
+ define: {
+ 'process.env.NODE_ENV': `"development"`,
+ },
+ format: 'cjs',
+ external: ['graphql', '@prisma/client'],
+ })
+})()
diff --git a/typescript/skeet-graphql/graphql/env.sample b/typescript/skeet-graphql/graphql/env.sample
new file mode 100644
index 000000000000..d58a51255433
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/env.sample
@@ -0,0 +1 @@
+DATABASE_URL=postgresql://skeeter:rabbit@127.0.0.1:5432/skeet-graphql-dev?schema=public
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/jest.config.js b/typescript/skeet-graphql/graphql/jest.config.js
new file mode 100755
index 000000000000..2f35f558b5f8
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/jest.config.js
@@ -0,0 +1,16 @@
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ roots: ['/tests', '/src'],
+ collectCoverage: true,
+ collectCoverageFrom: ['**/*.ts', '!**/node_modules/**'],
+ coverageDirectory: 'coverage_dir',
+ coverageReporters: ['html'],
+ moduleNameMapper: {
+ '^@/(.*)$': '/src/$1',
+ },
+ setupFilesAfterEnv: ['./tests/jest.setup.ts'],
+ reporters: ['default', 'github-actions'],
+}
diff --git a/typescript/skeet-graphql/graphql/nodemon.json b/typescript/skeet-graphql/graphql/nodemon.json
new file mode 100644
index 000000000000..208664523d79
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/nodemon.json
@@ -0,0 +1,7 @@
+{
+ "watch": ["src"],
+ "ignore": ["src/**/*.test.ts", "node_modules"],
+ "ext": "ts,mjs,js,json,graphql",
+ "exec": "yarn s && npx ts-node devBuild.ts && node ./dist/index.js",
+ "legacyWatch": true
+}
diff --git a/typescript/skeet-graphql/graphql/package.json b/typescript/skeet-graphql/graphql/package.json
new file mode 100755
index 000000000000..1d8661fcf9b3
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "skeet-graphql",
+ "version": "0.6.6",
+ "description": "Skeet GraphQL - TypeScript Serverless Framework",
+ "main": "dist/index.js",
+ "repository": "https://github.com/elsoul/skeet-graphql.git",
+ "author": "POPPIN-FUMI",
+ "license": "Apache-2.0",
+ "private": false,
+ "scripts": {
+ "test": "jest --coverage=false --detectOpenHandles --maxWorkers=1",
+ "update:packages": "ncu -u && yarn",
+ "build": "npx ts-node build.ts",
+ "publish": "npm publish",
+ "s": "npx ts-node -r tsconfig-paths/register --transpile-only src/index.ts hey",
+ "dev": "nodemon",
+ "start": "node dist/index.js",
+ "db:dev": "npx prisma migrate dev",
+ "db:deploy": "npx prisma migrate deploy",
+ "db:seed": "npx prisma db seed"
+ },
+ "prisma": {
+ "seed": "npx ts-node --transpile-only prisma/seed.ts"
+ },
+ "dependencies": {
+ "@apollo/server": "4.7.5",
+ "@jcm/nexus-plugin-relay-global-id": "0.2.0",
+ "@jcm/nexus-plugin-relay-node-interface": "0.2.0",
+ "@prisma/client": "5.0.0",
+ "@skeet-framework/utils": "^0.9.0",
+ "bs58": "5.0.0",
+ "dotenv": "16.3.1",
+ "firebase-admin": "11.4.1",
+ "graphql": "^16.7.1",
+ "graphql-depth-limit": "1.1.0",
+ "graphql-middleware": "6.1.35",
+ "graphql-query-complexity": "0.12.0",
+ "graphql-relay": "0.10.0",
+ "graphql-shield": "7.6.5",
+ "jsonwebtoken": "9.0.0",
+ "nexus": "1.3.0",
+ "nexus-prisma": "1.0.10",
+ "node-fetch": "2.6.9",
+ "prisma": "5.0.0"
+ },
+ "devDependencies": {
+ "@types/cors": "2.8.13",
+ "@types/express": "4.17.17",
+ "@types/graphql-depth-limit": "1.1.3",
+ "@types/jest": "29.5.3",
+ "@types/node": "20.4.2",
+ "@types/node-fetch": "2.6.4",
+ "@types/superagent": "4.1.18",
+ "@types/supertest": "2.0.12",
+ "babel-loader": "9.1.3",
+ "esbuild": "0.18.14",
+ "eslint": "8.45.0",
+ "eslint-config-prettier": "8.8.0",
+ "jest": "29.6.1",
+ "nodemon": "3.0.1",
+ "npm-check-updates": "16.10.16",
+ "prettier": "2.8.8",
+ "prettier-plugin-prisma": "4.17.0",
+ "superagent": "8.0.9",
+ "supertest": "6.3.3",
+ "ts-jest": "29.1.1",
+ "ts-loader": "9.4.4",
+ "tsconfig-paths": "4.2.0",
+ "typescript": "5.1.6"
+ }
+}
diff --git a/typescript/skeet-graphql/graphql/prisma/schema.prisma b/typescript/skeet-graphql/graphql/prisma/schema.prisma
new file mode 100644
index 000000000000..674101094d16
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/prisma/schema.prisma
@@ -0,0 +1,71 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+generator nexusPrisma {
+ provider = "nexus-prisma"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+enum Role {
+ USER
+ ADMIN
+ MASTER
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ uid String @unique
+ username String?
+ email String @unique
+ iconUrl String?
+ role Role @default(USER)
+ iv String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ chatRoomMessages ChatRoomMessage[]
+ userChatRooms UserChatRoom[]
+
+ @@index([username])
+}
+
+model ChatRoom {
+ id Int @id @default(autoincrement())
+ name String?
+ title String?
+ model String @default("gpt4")
+ maxTokens Int @default(500)
+ temperature Int @default(0)
+ stream Boolean @default(false)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ chatRoomMessages ChatRoomMessage[]
+ userChatRooms UserChatRoom[]
+}
+
+model ChatRoomMessage {
+ id Int @id @default(autoincrement())
+ role String
+ content String
+ userId Int
+ chatRoomId Int
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ User User @relation(fields: [userId], references: [id])
+ ChatRoom ChatRoom @relation(fields: [chatRoomId], references: [id])
+}
+
+model UserChatRoom {
+ userId Int
+ chatRoomId Int
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ User User @relation(fields: [userId], references: [id])
+ ChatRoom ChatRoom @relation(fields: [chatRoomId], references: [id])
+
+ @@id([userId, chatRoomId])
+}
diff --git a/typescript/skeet-graphql/graphql/prisma/seed.ts b/typescript/skeet-graphql/graphql/prisma/seed.ts
new file mode 100644
index 000000000000..faa5446a1742
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/prisma/seed.ts
@@ -0,0 +1 @@
+console.log(`👷 Nothing to create seed data for now!`)
diff --git a/typescript/skeet-graphql/graphql/src/graphql/authManager/index.ts b/typescript/skeet-graphql/graphql/src/graphql/authManager/index.ts
new file mode 100644
index 000000000000..325ce3f48a00
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/authManager/index.ts
@@ -0,0 +1 @@
+export * from './me'
diff --git a/typescript/skeet-graphql/graphql/src/graphql/authManager/me.ts b/typescript/skeet-graphql/graphql/src/graphql/authManager/me.ts
new file mode 100644
index 000000000000..7b81c3044feb
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/authManager/me.ts
@@ -0,0 +1,31 @@
+import { toPrismaId } from '@skeet-framework/utils'
+import { extendType } from 'nexus'
+import { User } from 'nexus-prisma'
+
+export const MeQuery = extendType({
+ type: 'Query',
+ definition(t) {
+ t.field('me', {
+ type: User.$name,
+ args: {},
+ async resolve(_, __, ctx) {
+ if (!ctx.user.id || ctx.user.id == '') {
+ return {
+ uid: '',
+ username: '',
+ iconUrl: '',
+ email: '',
+ }
+ }
+ return await ctx.prisma.user.findUnique({
+ where: {
+ id:
+ process.env.NODE_ENV === 'production'
+ ? toPrismaId(ctx.user.id)
+ : 1,
+ },
+ })
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/enums.ts b/typescript/skeet-graphql/graphql/src/graphql/enums.ts
new file mode 100644
index 000000000000..c80b383586e9
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/enums.ts
@@ -0,0 +1,4 @@
+import { enumType } from 'nexus'
+import { Role } from 'nexus-prisma'
+
+export const roleEnum = enumType(Role)
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/graphql/index.ts b/typescript/skeet-graphql/graphql/src/graphql/index.ts
new file mode 100644
index 000000000000..f056e6ba6d2e
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/index.ts
@@ -0,0 +1,4 @@
+export * from './taskManager'
+export * from './modelManager'
+export * from './authManager'
+export * from './responseManager'
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/index.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/index.ts
new file mode 100644
index 000000000000..37c0fa5b9699
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/index.ts
@@ -0,0 +1,3 @@
+export * from './model'
+export * from './query'
+export * from './mutation'
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/model.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/model.ts
new file mode 100644
index 000000000000..f6368ed17775
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/model.ts
@@ -0,0 +1,20 @@
+import { objectType } from 'nexus'
+import { ChatRoom } from 'nexus-prisma'
+
+export const ChatRoomObject = objectType({
+ name: ChatRoom.$name,
+ description: ChatRoom.$description,
+ definition(t) {
+ t.relayGlobalId('id', {})
+ t.field(ChatRoom.name)
+ t.field(ChatRoom.title)
+ t.field(ChatRoom.model)
+ t.field(ChatRoom.maxTokens)
+ t.field(ChatRoom.temperature)
+ t.field(ChatRoom.stream)
+ t.field(ChatRoom.createdAt)
+ t.field(ChatRoom.updatedAt)
+ t.field(ChatRoom.chatRoomMessages)
+ t.field(ChatRoom.userChatRooms)
+ },
+})
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/mutation.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/mutation.ts
new file mode 100644
index 000000000000..63764799ec0a
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/mutation.ts
@@ -0,0 +1,121 @@
+import { extendType, stringArg, intArg, floatArg, booleanArg } from 'nexus'
+import { toPrismaId } from '@skeet-framework/utils'
+import { ChatRoom } from 'nexus-prisma'
+import { PrismaClient } from '@prisma/client'
+import { CurrentUser } from '@/index'
+import { GraphQLError } from 'graphql'
+
+export const ChatRoomMutation = extendType({
+ type: 'Mutation',
+ definition(t) {
+ t.field('createChatRoom', {
+ type: ChatRoom.$name,
+ args: {
+ name: stringArg(),
+ title: stringArg(),
+ model: stringArg(),
+ maxTokens: intArg(),
+ temperature: intArg(),
+ stream: booleanArg(),
+ systemContent: stringArg(),
+ },
+ async resolve(
+ _,
+ { name, title, model, maxTokens, temperature, stream, systemContent },
+ ctx
+ ) {
+ try {
+ const data = {
+ name: name || 'default room',
+ title,
+ model: model || 'gpt-3.5-turbo',
+ maxTokens: maxTokens || 420,
+ temperature: temperature || 0,
+ stream: !!stream,
+ }
+ const user: CurrentUser = ctx.user
+ console.log({ user: user.id })
+ if (user.id === '') throw new Error('You are not logged in!')
+ const prismaClient = ctx.prisma as PrismaClient
+ const result = await prismaClient.$transaction(async (tx) => {
+ const userId = toPrismaId(user.id)
+ // ChatRoomを作成
+ const createdChatRoom = await tx.chatRoom.create({
+ data,
+ })
+
+ // UserChatRoomに関連付けを作成
+ await tx.userChatRoom.create({
+ data: {
+ userId,
+ chatRoomId: createdChatRoom.id,
+ },
+ })
+
+ // ChatRoomMessageを作成
+ await tx.chatRoomMessage.create({
+ data: {
+ role: 'system',
+ content:
+ systemContent ||
+ 'This is a great chatbot. This Assistant is very kind and helpful.',
+ userId,
+ chatRoomId: createdChatRoom.id,
+ },
+ })
+
+ return createdChatRoom
+ })
+ return result
+ } catch (error) {
+ throw new GraphQLError(`${error}`)
+ }
+ },
+ })
+ t.field('updateChatRoom', {
+ type: ChatRoom.$name,
+ args: {
+ id: stringArg(),
+ name: stringArg(),
+ title: stringArg(),
+ model: stringArg(),
+ stream: booleanArg(),
+ },
+ async resolve(_, args, ctx) {
+ try {
+ if (!args.id) throw new Error(`no id`)
+ const id = toPrismaId(args.id)
+ const data = JSON.parse(JSON.stringify(args))
+ delete data.id
+ return await ctx.prisma.chatRoom.update({
+ where: {
+ id,
+ },
+ data,
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ t.field('deleteChatRoom', {
+ type: ChatRoom.$name,
+ args: {
+ id: stringArg(),
+ },
+ async resolve(_, { id }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.chatRoom.delete({
+ where: {
+ id: toPrismaId(id),
+ },
+ })
+ } catch (error) {
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/query.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/query.ts
new file mode 100644
index 000000000000..c20a65175a74
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoom/query.ts
@@ -0,0 +1,64 @@
+import { extendType, stringArg } from 'nexus'
+import { toPrismaId, connectionFromArray } from '@skeet-framework/utils'
+import { ChatRoom } from 'nexus-prisma'
+import { CurrentUser } from '@/index'
+import { UserChatRoom } from '@prisma/client'
+
+export const ChatRoomsQuery = extendType({
+ type: 'Query',
+ definition(t) {
+ t.connectionField('chatRoomConnection', {
+ type: ChatRoom.$name,
+ async resolve(_, args, ctx, info) {
+ const user: CurrentUser = ctx.user
+ const userChatRooms = await ctx.prisma.userChatRoom.findMany({
+ where: {
+ userId: toPrismaId(user.id),
+ },
+ })
+ const chatRoomIds = userChatRooms.map(
+ (userChatRoom: UserChatRoom) => userChatRoom.chatRoomId
+ )
+ return connectionFromArray(
+ await ctx.prisma.chatRoom.findMany({
+ where: {
+ id: {
+ in: chatRoomIds,
+ },
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ }),
+ args
+ )
+ },
+ extendConnection(t) {
+ t.int('totalCount', {
+ async resolve(source, args, ctx) {
+ return ctx.prisma.chatRoom.count()
+ },
+ })
+ },
+ })
+ t.field('getChatRoom', {
+ type: ChatRoom.$name,
+ args: {
+ id: stringArg(),
+ },
+ async resolve(_, { id }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.chatRoom.findUnique({
+ where: {
+ id: toPrismaId(id),
+ },
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/index.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/index.ts
new file mode 100644
index 000000000000..37c0fa5b9699
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/index.ts
@@ -0,0 +1,3 @@
+export * from './model'
+export * from './query'
+export * from './mutation'
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/model.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/model.ts
new file mode 100644
index 000000000000..e7313318e9ec
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/model.ts
@@ -0,0 +1,16 @@
+import { objectType } from 'nexus'
+import { ChatRoomMessage } from 'nexus-prisma'
+
+export const ChatRoomMessageObject = objectType({
+ name: ChatRoomMessage.$name,
+ description: ChatRoomMessage.$description,
+ definition(t) {
+ t.relayGlobalId('id', {})
+ t.field(ChatRoomMessage.role)
+ t.field(ChatRoomMessage.content)
+ t.relayGlobalId('userId', {})
+ t.relayGlobalId('chatRoomId', {})
+ t.field(ChatRoomMessage.createdAt)
+ t.field(ChatRoomMessage.updatedAt)
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/mutation.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/mutation.ts
new file mode 100644
index 000000000000..aa4f6d642b98
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/mutation.ts
@@ -0,0 +1,83 @@
+import { extendType, stringArg, intArg } from 'nexus'
+import { toPrismaId } from '@skeet-framework/utils'
+import { ChatRoomMessage } from 'nexus-prisma'
+import { CurrentUser } from '@/index'
+
+export const ChatRoomMessageMutation = extendType({
+ type: 'Mutation',
+ definition(t) {
+ t.field('createChatRoomMessage', {
+ type: ChatRoomMessage.$name,
+ args: {
+ role: stringArg(),
+ content: stringArg(),
+ chatRoomId: stringArg(),
+ },
+ async resolve(_, { role, content, chatRoomId }, ctx) {
+ try {
+ const user: CurrentUser = ctx.user
+ console.log(user)
+ if (user.uid === '') throw new Error(`You are not logged in!`)
+ if (!role || !content || !chatRoomId)
+ throw new Error(`not enough args`)
+ const data = {
+ role,
+ content,
+ userId: toPrismaId(user.id),
+ chatRoomId: toPrismaId(chatRoomId),
+ }
+ return await ctx.prisma.chatRoomMessage.create({
+ data,
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`createChatRoomMessage: ${error}`)
+ }
+ },
+ })
+ t.field('updateChatRoomMessage', {
+ type: ChatRoomMessage.$name,
+ args: {
+ id: stringArg(),
+ content: stringArg(),
+ userId: intArg(),
+ chatRoomId: intArg(),
+ },
+ async resolve(_, args, ctx) {
+ try {
+ if (!args.id) throw new Error(`no id`)
+ const id = toPrismaId(args.id)
+ const data = JSON.parse(JSON.stringify(args))
+ delete data.id
+ return await ctx.prisma.chatRoomMessage.update({
+ where: {
+ id,
+ },
+ data,
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ t.field('deleteChatRoomMessage', {
+ type: ChatRoomMessage.$name,
+ args: {
+ id: stringArg(),
+ },
+ async resolve(_, { id }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.chatRoomMessage.delete({
+ where: {
+ id: toPrismaId(id),
+ },
+ })
+ } catch (error) {
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/query.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/query.ts
new file mode 100644
index 000000000000..3f86a5f1203b
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/ChatRoomMessage/query.ts
@@ -0,0 +1,110 @@
+import { extendType, stringArg } from 'nexus'
+import {
+ toPrismaId,
+ connectionFromArray,
+ GraphQLError,
+} from '@skeet-framework/utils'
+import { ChatRoomMessage } from 'nexus-prisma'
+import { CurrentUser } from '@/index'
+
+export const ChatRoomMessagesQuery = extendType({
+ type: 'Query',
+ definition(t) {
+ t.connectionField('chatRoomMessageConnection', {
+ type: ChatRoomMessage.$name,
+ additionalArgs: {
+ chatRoomId: stringArg(),
+ },
+ async resolve(_, { chatRoomId, ...args }, ctx) {
+ const user: CurrentUser = ctx.user
+ const prismaChatRoomId = toPrismaId(chatRoomId || '')
+ const userChatRooms = await ctx.prisma.userChatRoom.findMany({
+ where: {
+ userId: toPrismaId(user.id),
+ chatRoomId: prismaChatRoomId,
+ },
+ })
+ if (userChatRooms.length === 0)
+ throw new Error(`You are not a member of this chat room!`)
+
+ ctx.chatRoomId = prismaChatRoomId
+
+ return connectionFromArray(
+ await ctx.prisma.chatRoomMessage.findMany({
+ where: {
+ chatRoomId: prismaChatRoomId,
+ },
+ orderBy: {
+ createdAt: 'asc',
+ },
+ }),
+ args
+ )
+ },
+ extendConnection(t) {
+ t.int('totalCount', {
+ async resolve(_, __, ctx) {
+ // Retrieve chatRoomId from the context object
+ const prismaChatRoomId = ctx.chatRoomId
+ return ctx.prisma.chatRoomMessage.count({
+ where: {
+ chatRoomId: prismaChatRoomId,
+ },
+ })
+ },
+ })
+ },
+ })
+ t.list.field('getChatRoomMessages', {
+ type: ChatRoomMessage.$name,
+ args: {
+ chatRoomId: stringArg(),
+ },
+ async resolve(_, { chatRoomId }, ctx) {
+ try {
+ if (!chatRoomId) throw new Error(`no chatRoomId`)
+ const user: CurrentUser = ctx.user
+ if (user.id === '') throw new Error('You are not logged in!')
+ // Fetch the most recent 5 messages
+ const chatRoomMessages = await ctx.prisma.chatRoomMessage.findMany({
+ where: {
+ chatRoomId: toPrismaId(chatRoomId),
+ userId: toPrismaId(user.id),
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ take: 5,
+ })
+ const reversedChatRoomMessages = chatRoomMessages.reverse()
+
+ // Fetch the first created system message
+ const firstMessage: ChatRoomMessage | null =
+ await ctx.prisma.chatRoomMessage.findFirst({
+ where: {
+ chatRoomId: toPrismaId(chatRoomId),
+ userId: toPrismaId(user.id),
+ },
+ orderBy: {
+ createdAt: 'asc',
+ },
+ })
+
+ // If the first created message is not included in the most recent messages, append it
+ if (
+ firstMessage &&
+ !reversedChatRoomMessages.some(
+ (msg: any) => msg.id === firstMessage.id
+ )
+ ) {
+ reversedChatRoomMessages.unshift(firstMessage)
+ }
+
+ return chatRoomMessages
+ } catch (error) {
+ throw new GraphQLError(`getChatRoomMessage: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/index.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/index.ts
new file mode 100644
index 000000000000..37c0fa5b9699
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/index.ts
@@ -0,0 +1,3 @@
+export * from './model'
+export * from './query'
+export * from './mutation'
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/model.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/model.ts
new file mode 100644
index 000000000000..d955cbae5896
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/model.ts
@@ -0,0 +1,19 @@
+import { objectType } from 'nexus'
+import { User } from 'nexus-prisma'
+import { roleEnum } from '../enums'
+
+export const UserObject = objectType({
+ name: User.$name,
+ description: User.$description,
+ definition(t) {
+ t.relayGlobalId('id', {})
+ t.field(User.uid)
+ t.field(User.username)
+ t.field(User.email)
+ t.field(User.iconUrl)
+ t.field(User.role.name, { type: roleEnum })
+ t.field(User.iv)
+ t.field(User.createdAt)
+ t.field(User.updatedAt)
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/mutation.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/mutation.ts
new file mode 100644
index 000000000000..a4ceadfac9d2
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/mutation.ts
@@ -0,0 +1,83 @@
+import { Prisma } from '@prisma/client'
+import { generateIv, toPrismaId } from '@skeet-framework/utils'
+import { objectType, stringArg } from 'nexus'
+import { User } from 'nexus-prisma'
+
+export const UserMutation = objectType({
+ name: 'Mutation',
+ definition(t) {
+ t.field('createUser', {
+ type: User.$name,
+ args: {
+ uid: stringArg(),
+ username: stringArg(),
+ email: stringArg(),
+ iconUrl: stringArg(),
+ },
+ async resolve(_, args, ctx) {
+ try {
+ if (!args.uid || !args.email) throw new Error(`no uid or email`)
+ const { uid, username, email, iconUrl } = args
+ const userParams: Prisma.UserCreateInput = {
+ uid: uid,
+ username,
+ email: email,
+ iconUrl,
+ iv: generateIv(),
+ }
+ return await ctx.prisma.user.create({
+ data: userParams,
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ t.field('updateUser', {
+ type: User.$name,
+ args: {
+ id: stringArg(),
+ uid: stringArg(),
+ username: stringArg(),
+ email: stringArg(),
+ iconUrl: stringArg(),
+ },
+ async resolve(_, { id, username, iconUrl }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.user.update({
+ where: {
+ id: toPrismaId(id),
+ },
+ data: {
+ username,
+ iconUrl,
+ },
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ t.field('deleteUser', {
+ type: User.$name,
+ args: {
+ id: stringArg(),
+ },
+ async resolve(_, { id }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.user.delete({
+ where: {
+ id: toPrismaId(id),
+ },
+ })
+ } catch (error) {
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/query.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/query.ts
new file mode 100644
index 000000000000..30958bae62c8
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/User/query.ts
@@ -0,0 +1,42 @@
+import { extendType, stringArg } from 'nexus'
+import { connectionFromArray } from 'graphql-relay'
+import { User } from 'nexus-prisma'
+import { toPrismaId } from '@skeet-framework/utils'
+
+export const UsersQuery = extendType({
+ type: 'Query',
+ definition(t) {
+ t.connectionField('userConnection', {
+ type: User.$name,
+ async resolve(_, args, ctx, info) {
+ return connectionFromArray(await ctx.prisma.user.findMany(), args)
+ },
+ extendConnection(t) {
+ t.int('totalCount', {
+ async resolve(source, args, ctx) {
+ return ctx.prisma.user.count()
+ },
+ })
+ },
+ })
+ t.field('getUser', {
+ type: User.$name,
+ args: {
+ id: stringArg(),
+ },
+ async resolve(_, { id }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.user.findUnique({
+ where: {
+ id: toPrismaId(id),
+ },
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/index.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/index.ts
new file mode 100644
index 000000000000..37c0fa5b9699
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/index.ts
@@ -0,0 +1,3 @@
+export * from './model'
+export * from './query'
+export * from './mutation'
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/model.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/model.ts
new file mode 100644
index 000000000000..3306ce8f7475
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/model.ts
@@ -0,0 +1,14 @@
+import { objectType } from 'nexus'
+import { UserChatRoom } from 'nexus-prisma'
+
+export const UserChatRoomObject = objectType({
+ name: UserChatRoom.$name,
+ description: UserChatRoom.$description,
+ definition(t) {
+ t.relayGlobalId('id', {})
+ t.field(UserChatRoom.userId)
+ t.field(UserChatRoom.chatRoomId)
+ t.field(UserChatRoom.createdAt)
+ t.field(UserChatRoom.updatedAt)
+ },
+})
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/mutation.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/mutation.ts
new file mode 100644
index 000000000000..b603c7977792
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/mutation.ts
@@ -0,0 +1,70 @@
+import { extendType, stringArg, intArg } from 'nexus'
+import { toPrismaId } from '@skeet-framework/utils'
+import { UserChatRoom } from 'nexus-prisma'
+
+export const UserChatRoomMutation = extendType({
+ type: 'Mutation',
+ definition(t) {
+ t.field('createUserChatRoom', {
+ type: UserChatRoom.$name,
+ args: {
+ userId: intArg(),
+ chatRoomId: intArg(),
+ },
+ async resolve(_, args, ctx) {
+ try {
+ if (!args.userId || !args.chatRoomId)
+ throw new Error(`not enough args`)
+ return await ctx.prisma.userChatRoom.create({
+ data: args,
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ t.field('updateUserChatRoom', {
+ type: UserChatRoom.$name,
+ args: {
+ id: stringArg(),
+ chatRoomId: intArg(),
+ },
+ async resolve(_, args, ctx) {
+ try {
+ if (!args.id) throw new Error(`no id`)
+ const id = toPrismaId(args.id)
+ const data = JSON.parse(JSON.stringify(args))
+ delete data.id
+ return await ctx.prisma.userChatRoom.update({
+ where: {
+ id,
+ },
+ data,
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ t.field('deleteUserChatRoom', {
+ type: UserChatRoom.$name,
+ args: {
+ id: stringArg(),
+ },
+ async resolve(_, { id }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.userChatRoom.delete({
+ where: {
+ id: toPrismaId(id),
+ },
+ })
+ } catch (error) {
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/query.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/query.ts
new file mode 100644
index 000000000000..6819ddbd59e9
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/UserChatRoom/query.ts
@@ -0,0 +1,44 @@
+import { extendType, stringArg } from 'nexus'
+import { toPrismaId, connectionFromArray } from '@skeet-framework/utils'
+import { UserChatRoom } from 'nexus-prisma'
+
+export const UserChatRoomsQuery = extendType({
+ type: 'Query',
+ definition(t) {
+ t.connectionField('userChatRoomConnection', {
+ type: UserChatRoom.$name,
+ async resolve(_, args, ctx, info) {
+ return connectionFromArray(
+ await ctx.prisma.userChatRoom.findMany(),
+ args
+ )
+ },
+ extendConnection(t) {
+ t.int('totalCount', {
+ async resolve(source, args, ctx) {
+ return ctx.prisma.userChatRoom.count()
+ },
+ })
+ },
+ })
+ t.field('getUserChatRoom', {
+ type: UserChatRoom.$name,
+ args: {
+ id: stringArg(),
+ },
+ async resolve(_, { id }, ctx) {
+ try {
+ if (!id) throw new Error(`no id`)
+ return await ctx.prisma.userChatRoom.findUnique({
+ where: {
+ id: toPrismaId(id),
+ },
+ })
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/enums.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/enums.ts
new file mode 100644
index 000000000000..c80b383586e9
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/enums.ts
@@ -0,0 +1,4 @@
+import { enumType } from 'nexus'
+import { Role } from 'nexus-prisma'
+
+export const roleEnum = enumType(Role)
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/graphql/modelManager/index.ts b/typescript/skeet-graphql/graphql/src/graphql/modelManager/index.ts
new file mode 100644
index 000000000000..540f512b428e
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/modelManager/index.ts
@@ -0,0 +1,4 @@
+export * from './ChatRoom'
+export * from './ChatRoomMessage'
+export * from './User'
+export * from './UserChatRoom'
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/graphql/responseManager/index.ts b/typescript/skeet-graphql/graphql/src/graphql/responseManager/index.ts
new file mode 100644
index 000000000000..336ce12bb910
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/responseManager/index.ts
@@ -0,0 +1 @@
+export {}
diff --git a/typescript/skeet-graphql/graphql/src/graphql/taskManager/index.ts b/typescript/skeet-graphql/graphql/src/graphql/taskManager/index.ts
new file mode 100644
index 000000000000..85bf0faaf91e
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/taskManager/index.ts
@@ -0,0 +1 @@
+export * from './postTweet'
diff --git a/typescript/skeet-graphql/graphql/src/graphql/taskManager/postTweet.ts b/typescript/skeet-graphql/graphql/src/graphql/taskManager/postTweet.ts
new file mode 100644
index 000000000000..b4f3c9ea6a9d
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/graphql/taskManager/postTweet.ts
@@ -0,0 +1,23 @@
+import { extendType, stringArg } from 'nexus'
+
+export const postTweet = extendType({
+ type: 'Query',
+ definition(t) {
+ t.field('postTweet', {
+ type: 'Boolean',
+ args: {
+ id: stringArg(),
+ text: stringArg(),
+ },
+ async resolve(_, { id, text }, ctx) {
+ try {
+ if (!id || !text) throw new Error(`no id`)
+ return true
+ } catch (error) {
+ console.log(error)
+ throw new Error(`error: ${error}`)
+ }
+ },
+ })
+ },
+})
diff --git a/typescript/skeet-graphql/graphql/src/index.ts b/typescript/skeet-graphql/graphql/src/index.ts
new file mode 100755
index 000000000000..840b18a9c0a6
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/index.ts
@@ -0,0 +1,122 @@
+import { PrismaClient, User } from '@prisma/client'
+import cors from 'cors'
+import express from 'express'
+import { json } from 'body-parser'
+import http from 'http'
+import { GraphQLError } from 'graphql'
+import bodyParser from 'body-parser'
+import { ApolloServer } from '@apollo/server'
+import { expressMiddleware } from '@apollo/server/express4'
+import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
+import { schema, permissions } from '@/schema'
+import { applyMiddleware } from 'graphql-middleware'
+import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache'
+import depthLimit from 'graphql-depth-limit'
+import queryComplexity, { simpleEstimator } from 'graphql-query-complexity'
+import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'
+import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled'
+import { getLoginUser } from './lib/getLoginUser'
+import { sleep } from '@skeet-framework/utils'
+
+export type CurrentUser = Omit & { id: string }
+
+interface Context {
+ user?: CurrentUser
+}
+
+const prisma = new PrismaClient()
+
+const PORT = process.env.PORT || 3000
+const skeetEnv = process.env.NODE_ENV || 'development'
+
+const queryComplexityRule = queryComplexity({
+ maximumComplexity: 1000,
+ variables: {},
+ // eslint-disable-next-line no-console
+ createError: (max: number, actual: number) =>
+ new GraphQLError(
+ `Query is too complex: ${actual}. Maximum allowed complexity: ${max}`
+ ),
+ estimators: [
+ simpleEstimator({
+ defaultComplexity: 1,
+ }),
+ ],
+})
+
+const allowedOrigins: string[] = []
+if (process.env.NODE_ENV === 'production') {
+ allowedOrigins.push('https://next-graphql.skeet.dev')
+ allowedOrigins.push('https://skeet-graphql.web.app')
+} else {
+ allowedOrigins.push('http://localhost:3000')
+ allowedOrigins.push('http://localhost:4200')
+ new Array(10).fill(0).forEach((_, i) => {
+ allowedOrigins.push(`http://localhost:1900${i}`)
+ })
+}
+
+const corsOptions: cors.CorsOptions = {
+ origin: (origin, callback) => {
+ if (!origin || allowedOrigins.includes(origin)) {
+ callback(null, origin)
+ } else {
+ callback(new Error('Not allowed by CORS'))
+ }
+ },
+}
+
+const endpoint = process.env.NODE_ENV === 'production' ? '/' : '/graphql'
+
+const app = express()
+const httpServer = http.createServer(app)
+app.use(bodyParser.json())
+app.use(cors(corsOptions))
+app.get('/root', (req, res) => {
+ res.send('Skeet App is Running!')
+})
+
+export const server = new ApolloServer({
+ schema: applyMiddleware(schema, permissions),
+ cache: new InMemoryLRUCache({
+ maxSize: Math.pow(2, 20) * 100,
+ ttl: 300_000,
+ }),
+ plugins: [
+ ApolloServerPluginDrainHttpServer({ httpServer }),
+ process.env.NODE_ENV === 'production'
+ ? ApolloServerPluginLandingPageDisabled()
+ : ApolloServerPluginLandingPageLocalDefault({ footer: false }),
+ ],
+ validationRules: [depthLimit(7), queryComplexityRule],
+ introspection: true,
+})
+
+export const startApolloServer = async () => {
+ await server.start()
+ app.use(
+ endpoint,
+ cors(corsOptions),
+ json(),
+ expressMiddleware(server, {
+ context: async ({ req }) => ({
+ user: await getLoginUser(
+ String(req.headers.authorization),
+ prisma
+ ),
+ prisma,
+ }),
+ })
+ )
+}
+
+export const expressServer = httpServer.listen(PORT, async () => {
+ await startApolloServer()
+ if (process.argv[2]) {
+ await sleep(1000)
+ process.exit()
+ }
+ console.log(
+ `🚀 [API:${skeetEnv}]Server ready at http://localhost:${PORT}/graphql`
+ )
+})
diff --git a/typescript/skeet-graphql/graphql/src/lib/firebaseConfig.ts b/typescript/skeet-graphql/graphql/src/lib/firebaseConfig.ts
new file mode 100644
index 000000000000..94d5f6d5b811
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/lib/firebaseConfig.ts
@@ -0,0 +1,10 @@
+const firebaseConfig = {
+ projectId: 'skeet-graphql',
+ appId: '1:818638014982:web:2b634dc809a351340a9187',
+ storageBucket: 'skeet-graphql.appspot.com',
+ apiKey: 'AIzaSyAxTADDsXI9QENVObjIb7HVAj5LMMy3L0o',
+ authDomain: 'skeet-graphql.firebaseapp.com',
+ messagingSenderId: '818638014982',
+ measurementId: 'G-9TE9K1RNSW',
+}
+export default firebaseConfig
diff --git a/typescript/skeet-graphql/graphql/src/lib/getLoginUser.ts b/typescript/skeet-graphql/graphql/src/lib/getLoginUser.ts
new file mode 100644
index 000000000000..9a1ed7296f81
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/lib/getLoginUser.ts
@@ -0,0 +1,55 @@
+import { DecodedIdToken } from 'firebase-admin/lib/auth/token-verifier'
+import { auth } from 'firebase-admin'
+import { toGlobalId } from '@skeet-framework/utils'
+import admin from 'firebase-admin'
+import { PrismaClient } from '@prisma/client'
+admin.initializeApp()
+
+const skeetEnv = process.env.NODE_ENV || 'development'
+
+export type UnknownUser = {
+ id: string
+ uid: string
+ name: string
+ email: string
+ iconUrl: string
+}
+
+export const unknownUser: UnknownUser = {
+ id: '',
+ uid: '',
+ name: '',
+ email: '',
+ iconUrl: '',
+}
+
+export const getLoginUser = async (token: string, prisma: PrismaClient) => {
+ if (token == 'undefined' || token == null) return unknownUser
+
+ const bearer = token.split('Bearer ')[1]
+ try {
+ if (!bearer) return unknownUser
+ const decodedUser: DecodedIdToken = await auth().verifyIdToken(bearer)
+ const user = await prisma.user.findUnique({
+ where: {
+ uid: decodedUser.uid,
+ },
+ })
+ if (!user) return unknownUser
+ const response = { ...user, id: toGlobalId('User', user.id) } as T
+ if (response) return response
+ return response
+ } catch (error) {
+ if (skeetEnv === 'development') {
+ const user = await prisma.user.findUnique({
+ where: {
+ id: 1,
+ },
+ })
+ if (!user) return unknownUser
+ console.log(`This is development mode - ctx.user returns user id: 1`)
+ return { ...user, id: toGlobalId('User', 1) } as T
+ }
+ return unknownUser
+ }
+}
diff --git a/typescript/skeet-graphql/graphql/src/schema/Node.ts b/typescript/skeet-graphql/graphql/src/schema/Node.ts
new file mode 100644
index 000000000000..e57a15858a53
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/schema/Node.ts
@@ -0,0 +1,21 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { RelayNodeInterfacePluginConfig } from '@jcm/nexus-plugin-relay-node-interface'
+
+const idFetcher: RelayNodeInterfacePluginConfig['idFetcher'] = (
+ { id, type },
+ ctx,
+ _info,
+) => {
+ if (type === 'User') return []
+ return null
+}
+
+const resolveType: RelayNodeInterfacePluginConfig['resolveType'] = (o) => {
+ if ('isAdmin' in o) return 'User'
+ return 'Boolean'
+}
+
+export const relayNodeInterfacePluginConfig = {
+ idFetcher,
+ resolveType,
+}
diff --git a/typescript/skeet-graphql/graphql/src/schema/index.ts b/typescript/skeet-graphql/graphql/src/schema/index.ts
new file mode 100644
index 000000000000..61900760ee8c
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/schema/index.ts
@@ -0,0 +1,2 @@
+export * from './permissions'
+export * from './schema'
diff --git a/typescript/skeet-graphql/graphql/src/schema/nexus-typegen.ts b/typescript/skeet-graphql/graphql/src/schema/nexus-typegen.ts
new file mode 100644
index 000000000000..8b957205cd5a
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/schema/nexus-typegen.ts
@@ -0,0 +1,583 @@
+/**
+ * This file was generated by Nexus Schema
+ * Do not make changes to this file directly
+ */
+
+
+import type { core, connectionPluginCore } from "nexus"
+import type { RelayGlobalIdNexusFieldConfig } from "@jcm/nexus-plugin-relay-global-id"
+import type { QueryComplexity } from "nexus/dist/plugins/queryComplexityPlugin"
+declare global {
+ interface NexusGenCustomInputMethods {
+ /**
+ * A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.
+ */
+ datetime(fieldName: FieldName, opts?: core.CommonInputFieldConfig): void // "DateTime";
+ }
+}
+declare global {
+ interface NexusGenCustomOutputMethods {
+ /**
+ * A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.
+ */
+ datetime(fieldName: FieldName, ...opts: core.ScalarOutSpread): void // "DateTime";
+ /**
+ * Adds a Relay-style connection to the type, with numerous options for configuration
+ *
+ * @see https://nexusjs.org/docs/plugins/connection
+ */
+ connectionField(
+ fieldName: FieldName,
+ config: connectionPluginCore.ConnectionFieldConfig & { totalCount?: connectionPluginCore.ConnectionFieldResolver }
+ ): void
+ relayGlobalId(
+ fieldName: FieldName,
+ config: RelayGlobalIdNexusFieldConfig
+ ): void
+ }
+}
+
+
+declare global {
+ interface NexusGen extends NexusGenTypes {}
+}
+
+export interface NexusGenInputs {
+}
+
+export interface NexusGenEnums {
+ Role: "ADMIN" | "MASTER" | "USER"
+}
+
+export interface NexusGenScalars {
+ String: string
+ Int: number
+ Float: number
+ Boolean: boolean
+ ID: string
+ DateTime: Date
+}
+
+export interface NexusGenObjects {
+ ChatRoom: { // root type
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ maxTokens: number; // Int!
+ model: string; // String!
+ name?: string | null; // String
+ stream: boolean; // Boolean!
+ temperature: number; // Int!
+ title?: string | null; // String
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ }
+ ChatRoomEdge: { // root type
+ cursor: string; // String!
+ node?: NexusGenRootTypes['ChatRoom'] | null; // ChatRoom
+ }
+ ChatRoomMessage: { // root type
+ content: string; // String!
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ role: string; // String!
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ }
+ ChatRoomMessageEdge: { // root type
+ cursor: string; // String!
+ node?: NexusGenRootTypes['ChatRoomMessage'] | null; // ChatRoomMessage
+ }
+ Mutation: {};
+ PageInfo: { // root type
+ endCursor?: string | null; // String
+ hasNextPage: boolean; // Boolean!
+ hasPreviousPage: boolean; // Boolean!
+ startCursor?: string | null; // String
+ }
+ Query: {};
+ QueryChatRoomConnection_Connection: { // root type
+ edges?: Array | null; // [ChatRoomEdge]
+ nodes?: Array | null; // [ChatRoom]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ }
+ QueryChatRoomMessageConnection_Connection: { // root type
+ edges?: Array | null; // [ChatRoomMessageEdge]
+ nodes?: Array | null; // [ChatRoomMessage]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ }
+ QueryUserChatRoomConnection_Connection: { // root type
+ edges?: Array | null; // [UserChatRoomEdge]
+ nodes?: Array | null; // [UserChatRoom]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ }
+ QueryUserConnection_Connection: { // root type
+ edges?: Array | null; // [UserEdge]
+ nodes?: Array | null; // [User]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ }
+ User: { // root type
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ email: string; // String!
+ iconUrl?: string | null; // String
+ iv?: string | null; // String
+ role?: NexusGenEnums['Role'] | null; // Role
+ uid: string; // String!
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ username?: string | null; // String
+ }
+ UserChatRoom: { // root type
+ chatRoomId: number; // Int!
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ userId: number; // Int!
+ }
+ UserChatRoomEdge: { // root type
+ cursor: string; // String!
+ node?: NexusGenRootTypes['UserChatRoom'] | null; // UserChatRoom
+ }
+ UserEdge: { // root type
+ cursor: string; // String!
+ node?: NexusGenRootTypes['User'] | null; // User
+ }
+}
+
+export interface NexusGenInterfaces {
+ Node: any;
+}
+
+export interface NexusGenUnions {
+}
+
+export type NexusGenRootTypes = NexusGenInterfaces & NexusGenObjects
+
+export type NexusGenAllTypes = NexusGenRootTypes & NexusGenScalars & NexusGenEnums
+
+export interface NexusGenFieldTypes {
+ ChatRoom: { // field return type
+ chatRoomMessages: NexusGenRootTypes['ChatRoomMessage'][]; // [ChatRoomMessage!]!
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ id: string | null; // ID
+ maxTokens: number; // Int!
+ model: string; // String!
+ name: string | null; // String
+ stream: boolean; // Boolean!
+ temperature: number; // Int!
+ title: string | null; // String
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ userChatRooms: NexusGenRootTypes['UserChatRoom'][]; // [UserChatRoom!]!
+ }
+ ChatRoomEdge: { // field return type
+ cursor: string; // String!
+ node: NexusGenRootTypes['ChatRoom'] | null; // ChatRoom
+ }
+ ChatRoomMessage: { // field return type
+ chatRoomId: string | null; // ID
+ content: string; // String!
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ id: string | null; // ID
+ role: string; // String!
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ userId: string | null; // ID
+ }
+ ChatRoomMessageEdge: { // field return type
+ cursor: string; // String!
+ node: NexusGenRootTypes['ChatRoomMessage'] | null; // ChatRoomMessage
+ }
+ Mutation: { // field return type
+ createChatRoom: NexusGenRootTypes['ChatRoom'] | null; // ChatRoom
+ createChatRoomMessage: NexusGenRootTypes['ChatRoomMessage'] | null; // ChatRoomMessage
+ createUser: NexusGenRootTypes['User'] | null; // User
+ createUserChatRoom: NexusGenRootTypes['UserChatRoom'] | null; // UserChatRoom
+ deleteChatRoom: NexusGenRootTypes['ChatRoom'] | null; // ChatRoom
+ deleteChatRoomMessage: NexusGenRootTypes['ChatRoomMessage'] | null; // ChatRoomMessage
+ deleteUser: NexusGenRootTypes['User'] | null; // User
+ deleteUserChatRoom: NexusGenRootTypes['UserChatRoom'] | null; // UserChatRoom
+ updateChatRoom: NexusGenRootTypes['ChatRoom'] | null; // ChatRoom
+ updateChatRoomMessage: NexusGenRootTypes['ChatRoomMessage'] | null; // ChatRoomMessage
+ updateUser: NexusGenRootTypes['User'] | null; // User
+ updateUserChatRoom: NexusGenRootTypes['UserChatRoom'] | null; // UserChatRoom
+ }
+ PageInfo: { // field return type
+ endCursor: string | null; // String
+ hasNextPage: boolean; // Boolean!
+ hasPreviousPage: boolean; // Boolean!
+ startCursor: string | null; // String
+ }
+ Query: { // field return type
+ chatRoomConnection: NexusGenRootTypes['QueryChatRoomConnection_Connection'] | null; // QueryChatRoomConnection_Connection
+ chatRoomMessageConnection: NexusGenRootTypes['QueryChatRoomMessageConnection_Connection'] | null; // QueryChatRoomMessageConnection_Connection
+ getChatRoom: NexusGenRootTypes['ChatRoom'] | null; // ChatRoom
+ getChatRoomMessages: Array | null; // [ChatRoomMessage]
+ getUser: NexusGenRootTypes['User'] | null; // User
+ getUserChatRoom: NexusGenRootTypes['UserChatRoom'] | null; // UserChatRoom
+ me: NexusGenRootTypes['User'] | null; // User
+ node: NexusGenRootTypes['Node'] | null; // Node
+ nodes: Array; // [Node]!
+ postTweet: boolean | null; // Boolean
+ userChatRoomConnection: NexusGenRootTypes['QueryUserChatRoomConnection_Connection'] | null; // QueryUserChatRoomConnection_Connection
+ userConnection: NexusGenRootTypes['QueryUserConnection_Connection'] | null; // QueryUserConnection_Connection
+ }
+ QueryChatRoomConnection_Connection: { // field return type
+ edges: Array | null; // [ChatRoomEdge]
+ nodes: Array | null; // [ChatRoom]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ totalCount: number | null; // Int
+ }
+ QueryChatRoomMessageConnection_Connection: { // field return type
+ edges: Array | null; // [ChatRoomMessageEdge]
+ nodes: Array | null; // [ChatRoomMessage]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ totalCount: number | null; // Int
+ }
+ QueryUserChatRoomConnection_Connection: { // field return type
+ edges: Array | null; // [UserChatRoomEdge]
+ nodes: Array | null; // [UserChatRoom]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ totalCount: number | null; // Int
+ }
+ QueryUserConnection_Connection: { // field return type
+ edges: Array | null; // [UserEdge]
+ nodes: Array | null; // [User]
+ pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo!
+ totalCount: number | null; // Int
+ }
+ User: { // field return type
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ email: string; // String!
+ iconUrl: string | null; // String
+ id: string | null; // ID
+ iv: string | null; // String
+ role: NexusGenEnums['Role'] | null; // Role
+ uid: string; // String!
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ username: string | null; // String
+ }
+ UserChatRoom: { // field return type
+ chatRoomId: number; // Int!
+ createdAt: NexusGenScalars['DateTime']; // DateTime!
+ id: string | null; // ID
+ updatedAt: NexusGenScalars['DateTime']; // DateTime!
+ userId: number; // Int!
+ }
+ UserChatRoomEdge: { // field return type
+ cursor: string; // String!
+ node: NexusGenRootTypes['UserChatRoom'] | null; // UserChatRoom
+ }
+ UserEdge: { // field return type
+ cursor: string; // String!
+ node: NexusGenRootTypes['User'] | null; // User
+ }
+ Node: { // field return type
+ id: string | null; // ID
+ }
+}
+
+export interface NexusGenFieldTypeNames {
+ ChatRoom: { // field return type name
+ chatRoomMessages: 'ChatRoomMessage'
+ createdAt: 'DateTime'
+ id: 'ID'
+ maxTokens: 'Int'
+ model: 'String'
+ name: 'String'
+ stream: 'Boolean'
+ temperature: 'Int'
+ title: 'String'
+ updatedAt: 'DateTime'
+ userChatRooms: 'UserChatRoom'
+ }
+ ChatRoomEdge: { // field return type name
+ cursor: 'String'
+ node: 'ChatRoom'
+ }
+ ChatRoomMessage: { // field return type name
+ chatRoomId: 'ID'
+ content: 'String'
+ createdAt: 'DateTime'
+ id: 'ID'
+ role: 'String'
+ updatedAt: 'DateTime'
+ userId: 'ID'
+ }
+ ChatRoomMessageEdge: { // field return type name
+ cursor: 'String'
+ node: 'ChatRoomMessage'
+ }
+ Mutation: { // field return type name
+ createChatRoom: 'ChatRoom'
+ createChatRoomMessage: 'ChatRoomMessage'
+ createUser: 'User'
+ createUserChatRoom: 'UserChatRoom'
+ deleteChatRoom: 'ChatRoom'
+ deleteChatRoomMessage: 'ChatRoomMessage'
+ deleteUser: 'User'
+ deleteUserChatRoom: 'UserChatRoom'
+ updateChatRoom: 'ChatRoom'
+ updateChatRoomMessage: 'ChatRoomMessage'
+ updateUser: 'User'
+ updateUserChatRoom: 'UserChatRoom'
+ }
+ PageInfo: { // field return type name
+ endCursor: 'String'
+ hasNextPage: 'Boolean'
+ hasPreviousPage: 'Boolean'
+ startCursor: 'String'
+ }
+ Query: { // field return type name
+ chatRoomConnection: 'QueryChatRoomConnection_Connection'
+ chatRoomMessageConnection: 'QueryChatRoomMessageConnection_Connection'
+ getChatRoom: 'ChatRoom'
+ getChatRoomMessages: 'ChatRoomMessage'
+ getUser: 'User'
+ getUserChatRoom: 'UserChatRoom'
+ me: 'User'
+ node: 'Node'
+ nodes: 'Node'
+ postTweet: 'Boolean'
+ userChatRoomConnection: 'QueryUserChatRoomConnection_Connection'
+ userConnection: 'QueryUserConnection_Connection'
+ }
+ QueryChatRoomConnection_Connection: { // field return type name
+ edges: 'ChatRoomEdge'
+ nodes: 'ChatRoom'
+ pageInfo: 'PageInfo'
+ totalCount: 'Int'
+ }
+ QueryChatRoomMessageConnection_Connection: { // field return type name
+ edges: 'ChatRoomMessageEdge'
+ nodes: 'ChatRoomMessage'
+ pageInfo: 'PageInfo'
+ totalCount: 'Int'
+ }
+ QueryUserChatRoomConnection_Connection: { // field return type name
+ edges: 'UserChatRoomEdge'
+ nodes: 'UserChatRoom'
+ pageInfo: 'PageInfo'
+ totalCount: 'Int'
+ }
+ QueryUserConnection_Connection: { // field return type name
+ edges: 'UserEdge'
+ nodes: 'User'
+ pageInfo: 'PageInfo'
+ totalCount: 'Int'
+ }
+ User: { // field return type name
+ createdAt: 'DateTime'
+ email: 'String'
+ iconUrl: 'String'
+ id: 'ID'
+ iv: 'String'
+ role: 'Role'
+ uid: 'String'
+ updatedAt: 'DateTime'
+ username: 'String'
+ }
+ UserChatRoom: { // field return type name
+ chatRoomId: 'Int'
+ createdAt: 'DateTime'
+ id: 'ID'
+ updatedAt: 'DateTime'
+ userId: 'Int'
+ }
+ UserChatRoomEdge: { // field return type name
+ cursor: 'String'
+ node: 'UserChatRoom'
+ }
+ UserEdge: { // field return type name
+ cursor: 'String'
+ node: 'User'
+ }
+ Node: { // field return type name
+ id: 'ID'
+ }
+}
+
+export interface NexusGenArgTypes {
+ Mutation: {
+ createChatRoom: { // args
+ maxTokens?: number | null; // Int
+ model?: string | null; // String
+ name?: string | null; // String
+ stream?: boolean | null; // Boolean
+ systemContent?: string | null; // String
+ temperature?: number | null; // Int
+ title?: string | null; // String
+ }
+ createChatRoomMessage: { // args
+ chatRoomId?: string | null; // String
+ content?: string | null; // String
+ role?: string | null; // String
+ }
+ createUser: { // args
+ email?: string | null; // String
+ iconUrl?: string | null; // String
+ uid?: string | null; // String
+ username?: string | null; // String
+ }
+ createUserChatRoom: { // args
+ chatRoomId?: number | null; // Int
+ userId?: number | null; // Int
+ }
+ deleteChatRoom: { // args
+ id?: string | null; // String
+ }
+ deleteChatRoomMessage: { // args
+ id?: string | null; // String
+ }
+ deleteUser: { // args
+ id?: string | null; // String
+ }
+ deleteUserChatRoom: { // args
+ id?: string | null; // String
+ }
+ updateChatRoom: { // args
+ id?: string | null; // String
+ model?: string | null; // String
+ name?: string | null; // String
+ stream?: boolean | null; // Boolean
+ title?: string | null; // String
+ }
+ updateChatRoomMessage: { // args
+ chatRoomId?: number | null; // Int
+ content?: string | null; // String
+ id?: string | null; // String
+ userId?: number | null; // Int
+ }
+ updateUser: { // args
+ email?: string | null; // String
+ iconUrl?: string | null; // String
+ id?: string | null; // String
+ uid?: string | null; // String
+ username?: string | null; // String
+ }
+ updateUserChatRoom: { // args
+ chatRoomId?: number | null; // Int
+ id?: string | null; // String
+ }
+ }
+ Query: {
+ chatRoomConnection: { // args
+ after?: string | null; // String
+ before?: string | null; // String
+ first?: number | null; // Int
+ last?: number | null; // Int
+ }
+ chatRoomMessageConnection: { // args
+ after?: string | null; // String
+ before?: string | null; // String
+ chatRoomId?: string | null; // String
+ first?: number | null; // Int
+ last?: number | null; // Int
+ }
+ getChatRoom: { // args
+ id?: string | null; // String
+ }
+ getChatRoomMessages: { // args
+ chatRoomId?: string | null; // String
+ }
+ getUser: { // args
+ id?: string | null; // String
+ }
+ getUserChatRoom: { // args
+ id?: string | null; // String
+ }
+ node: { // args
+ id: string; // ID!
+ }
+ nodes: { // args
+ ids: string[]; // [ID!]!
+ }
+ postTweet: { // args
+ id?: string | null; // String
+ text?: string | null; // String
+ }
+ userChatRoomConnection: { // args
+ after?: string | null; // String
+ before?: string | null; // String
+ first?: number | null; // Int
+ last?: number | null; // Int
+ }
+ userConnection: { // args
+ after?: string | null; // String
+ before?: string | null; // String
+ first?: number | null; // Int
+ last?: number | null; // Int
+ }
+ }
+}
+
+export interface NexusGenAbstractTypeMembers {
+}
+
+export interface NexusGenTypeInterfaces {
+}
+
+export type NexusGenObjectNames = keyof NexusGenObjects;
+
+export type NexusGenInputNames = never;
+
+export type NexusGenEnumNames = keyof NexusGenEnums;
+
+export type NexusGenInterfaceNames = keyof NexusGenInterfaces;
+
+export type NexusGenScalarNames = keyof NexusGenScalars;
+
+export type NexusGenUnionNames = never;
+
+export type NexusGenObjectsUsingAbstractStrategyIsTypeOf = never;
+
+export type NexusGenAbstractsUsingStrategyResolveType = "Node";
+
+export type NexusGenFeaturesConfig = {
+ abstractTypeStrategies: {
+ isTypeOf: false
+ resolveType: true
+ __typename: false
+ }
+}
+
+export interface NexusGenTypes {
+ context: any;
+ inputTypes: NexusGenInputs;
+ rootTypes: NexusGenRootTypes;
+ inputTypeShapes: NexusGenInputs & NexusGenEnums & NexusGenScalars;
+ argTypes: NexusGenArgTypes;
+ fieldTypes: NexusGenFieldTypes;
+ fieldTypeNames: NexusGenFieldTypeNames;
+ allTypes: NexusGenAllTypes;
+ typeInterfaces: NexusGenTypeInterfaces;
+ objectNames: NexusGenObjectNames;
+ inputNames: NexusGenInputNames;
+ enumNames: NexusGenEnumNames;
+ interfaceNames: NexusGenInterfaceNames;
+ scalarNames: NexusGenScalarNames;
+ unionNames: NexusGenUnionNames;
+ allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames'];
+ allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames'];
+ allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes']
+ abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames'];
+ abstractTypeMembers: NexusGenAbstractTypeMembers;
+ objectsUsingAbstractStrategyIsTypeOf: NexusGenObjectsUsingAbstractStrategyIsTypeOf;
+ abstractsUsingStrategyResolveType: NexusGenAbstractsUsingStrategyResolveType;
+ features: NexusGenFeaturesConfig;
+}
+
+
+declare global {
+ interface NexusGenPluginTypeConfig {
+ }
+ interface NexusGenPluginInputTypeConfig {
+ }
+ interface NexusGenPluginFieldConfig {
+
+
+ /**
+ * The complexity for an individual field. Return a number
+ * or a function that returns a number to specify the
+ * complexity for this field.
+ */
+ complexity?: QueryComplexity
+ }
+ interface NexusGenPluginInputFieldConfig {
+ }
+ interface NexusGenPluginSchemaConfig {
+ }
+ interface NexusGenPluginArgConfig {
+ }
+}
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/schema/permissions.ts b/typescript/skeet-graphql/graphql/src/schema/permissions.ts
new file mode 100644
index 000000000000..532295b27833
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/schema/permissions.ts
@@ -0,0 +1,38 @@
+import { not, rule, shield } from 'graphql-shield'
+import { GraphQLError } from 'graphql'
+
+const isAuthenticated = rule({ cache: 'contextual' })(
+ async (_parent, _args, ctx) => {
+ return ctx.user?.uid !== ''
+ }
+)
+
+const isAdmin = rule()(async (parent, args, ctx, info) => {
+ return ctx.user.role === 'ADMIN'
+})
+
+const isGm = rule()(async (parent, args, ctx, info) => {
+ return ctx.user.role === 'GM'
+})
+
+export const permissions = shield(
+ {
+ Query: {
+ me: isAuthenticated,
+ userConnection: isAuthenticated,
+ },
+ Mutation: {},
+ },
+ {
+ fallbackError: async (thrownThing) => {
+ console.log(thrownThing)
+ if (thrownThing instanceof GraphQLError) {
+ return thrownThing
+ } else if (thrownThing instanceof Error) {
+ return new GraphQLError('Internal server error')
+ } else {
+ return new GraphQLError('Not Authorized')
+ }
+ },
+ }
+)
diff --git a/typescript/skeet-graphql/graphql/src/schema/schema.graphql b/typescript/skeet-graphql/graphql/src/schema/schema.graphql
new file mode 100644
index 000000000000..54f4236bcbfe
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/schema/schema.graphql
@@ -0,0 +1,274 @@
+### This file was generated by Nexus Schema
+### Do not make changes to this file directly
+
+
+type ChatRoom {
+ chatRoomMessages: [ChatRoomMessage!]!
+ createdAt: DateTime!
+ id: ID
+ maxTokens: Int!
+ model: String!
+ name: String
+ stream: Boolean!
+ temperature: Int!
+ title: String
+ updatedAt: DateTime!
+ userChatRooms: [UserChatRoom!]!
+}
+
+type ChatRoomEdge {
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Cursor"""
+ cursor: String!
+
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Node"""
+ node: ChatRoom
+}
+
+type ChatRoomMessage {
+ chatRoomId: ID
+ content: String!
+ createdAt: DateTime!
+ id: ID
+ role: String!
+ updatedAt: DateTime!
+ userId: ID
+}
+
+type ChatRoomMessageEdge {
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Cursor"""
+ cursor: String!
+
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Node"""
+ node: ChatRoomMessage
+}
+
+"""
+A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.
+"""
+scalar DateTime
+
+type Mutation {
+ createChatRoom(maxTokens: Int, model: String, name: String, stream: Boolean, systemContent: String, temperature: Int, title: String): ChatRoom
+ createChatRoomMessage(chatRoomId: String, content: String, role: String): ChatRoomMessage
+ createUser(email: String, iconUrl: String, uid: String, username: String): User
+ createUserChatRoom(chatRoomId: Int, userId: Int): UserChatRoom
+ deleteChatRoom(id: String): ChatRoom
+ deleteChatRoomMessage(id: String): ChatRoomMessage
+ deleteUser(id: String): User
+ deleteUserChatRoom(id: String): UserChatRoom
+ updateChatRoom(id: String, model: String, name: String, stream: Boolean, title: String): ChatRoom
+ updateChatRoomMessage(chatRoomId: Int, content: String, id: String, userId: Int): ChatRoomMessage
+ updateUser(email: String, iconUrl: String, id: String, uid: String, username: String): User
+ updateUserChatRoom(chatRoomId: Int, id: String): UserChatRoom
+}
+
+"""An object with a global ID"""
+interface Node {
+ """The global ID of the object."""
+ id: ID
+}
+
+"""
+PageInfo cursor, as defined in https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo
+"""
+type PageInfo {
+ """
+ The cursor corresponding to the last nodes in edges. Null if the connection is empty.
+ """
+ endCursor: String
+
+ """
+ Used to indicate whether more edges exist following the set defined by the clients arguments.
+ """
+ hasNextPage: Boolean!
+
+ """
+ Used to indicate whether more edges exist prior to the set defined by the clients arguments.
+ """
+ hasPreviousPage: Boolean!
+
+ """
+ The cursor corresponding to the first nodes in edges. Null if the connection is empty.
+ """
+ startCursor: String
+}
+
+type Query {
+ chatRoomConnection(
+ """Returns the elements in the list that come after the specified cursor"""
+ after: String
+
+ """Returns the elements in the list that come before the specified cursor"""
+ before: String
+
+ """Returns the first n elements from the list."""
+ first: Int
+
+ """Returns the last n elements from the list."""
+ last: Int
+ ): QueryChatRoomConnection_Connection
+ chatRoomMessageConnection(
+ """Returns the elements in the list that come after the specified cursor"""
+ after: String
+
+ """Returns the elements in the list that come before the specified cursor"""
+ before: String
+ chatRoomId: String
+
+ """Returns the first n elements from the list."""
+ first: Int
+
+ """Returns the last n elements from the list."""
+ last: Int
+ ): QueryChatRoomMessageConnection_Connection
+ getChatRoom(id: String): ChatRoom
+ getChatRoomMessages(chatRoomId: String): [ChatRoomMessage]
+ getUser(id: String): User
+ getUserChatRoom(id: String): UserChatRoom
+ me: User
+
+ """Fetches an object given its global ID"""
+ node(
+ """The global ID of an object"""
+ id: ID!
+ ): Node
+
+ """Fetches objects given their global IDs"""
+ nodes(
+ """The global IDs of objects"""
+ ids: [ID!]!
+ ): [Node]!
+ postTweet(id: String, text: String): Boolean
+ userChatRoomConnection(
+ """Returns the elements in the list that come after the specified cursor"""
+ after: String
+
+ """Returns the elements in the list that come before the specified cursor"""
+ before: String
+
+ """Returns the first n elements from the list."""
+ first: Int
+
+ """Returns the last n elements from the list."""
+ last: Int
+ ): QueryUserChatRoomConnection_Connection
+ userConnection(
+ """Returns the elements in the list that come after the specified cursor"""
+ after: String
+
+ """Returns the elements in the list that come before the specified cursor"""
+ before: String
+
+ """Returns the first n elements from the list."""
+ first: Int
+
+ """Returns the last n elements from the list."""
+ last: Int
+ ): QueryUserConnection_Connection
+}
+
+type QueryChatRoomConnection_Connection {
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-Edge-Types
+ """
+ edges: [ChatRoomEdge]
+
+ """Flattened list of ChatRoom type"""
+ nodes: [ChatRoom]
+
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo
+ """
+ pageInfo: PageInfo!
+ totalCount: Int
+}
+
+type QueryChatRoomMessageConnection_Connection {
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-Edge-Types
+ """
+ edges: [ChatRoomMessageEdge]
+
+ """Flattened list of ChatRoomMessage type"""
+ nodes: [ChatRoomMessage]
+
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo
+ """
+ pageInfo: PageInfo!
+ totalCount: Int
+}
+
+type QueryUserChatRoomConnection_Connection {
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-Edge-Types
+ """
+ edges: [UserChatRoomEdge]
+
+ """Flattened list of UserChatRoom type"""
+ nodes: [UserChatRoom]
+
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo
+ """
+ pageInfo: PageInfo!
+ totalCount: Int
+}
+
+type QueryUserConnection_Connection {
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-Edge-Types
+ """
+ edges: [UserEdge]
+
+ """Flattened list of User type"""
+ nodes: [User]
+
+ """
+ https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo
+ """
+ pageInfo: PageInfo!
+ totalCount: Int
+}
+
+enum Role {
+ ADMIN
+ MASTER
+ USER
+}
+
+type User {
+ createdAt: DateTime!
+ email: String!
+ iconUrl: String
+ id: ID
+ iv: String
+ role: Role
+ uid: String!
+ updatedAt: DateTime!
+ username: String
+}
+
+type UserChatRoom {
+ chatRoomId: Int!
+ createdAt: DateTime!
+ id: ID
+ updatedAt: DateTime!
+ userId: Int!
+}
+
+type UserChatRoomEdge {
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Cursor"""
+ cursor: String!
+
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Node"""
+ node: UserChatRoom
+}
+
+type UserEdge {
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Cursor"""
+ cursor: String!
+
+ """https://facebook.github.io/relay/graphql/connections.htm#sec-Node"""
+ node: User
+}
\ No newline at end of file
diff --git a/typescript/skeet-graphql/graphql/src/schema/schema.ts b/typescript/skeet-graphql/graphql/src/schema/schema.ts
new file mode 100644
index 000000000000..74e8ffadf9f8
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/src/schema/schema.ts
@@ -0,0 +1,33 @@
+import {
+ connectionPlugin,
+ makeSchema,
+ asNexusMethod,
+ queryComplexityPlugin,
+} from 'nexus'
+import { join } from 'path'
+import * as allTypes from '../graphql'
+import { relayNodeInterfacePlugin } from '@jcm/nexus-plugin-relay-node-interface'
+import { relayGlobalIdPlugin } from '@jcm/nexus-plugin-relay-global-id'
+import { relayNodeInterfacePluginConfig } from './Node'
+import { GraphQLDateTime } from 'graphql-scalars'
+
+export const schema = makeSchema({
+ types: [allTypes, asNexusMethod(GraphQLDateTime, 'datetime', 'Date')],
+ outputs: {
+ typegen: join(__dirname, './nexus-typegen.ts'),
+ schema: join(__dirname, './schema.graphql'),
+ },
+ plugins: [
+ connectionPlugin({
+ extendConnection: {
+ totalCount: { type: 'Int', requireResolver: false },
+ },
+ includeNodesField: true,
+ }),
+ relayNodeInterfacePlugin(relayNodeInterfacePluginConfig),
+ relayGlobalIdPlugin({
+ shouldAddRawId: process.env.NODE_ENV === 'development' ? true : false,
+ }),
+ queryComplexityPlugin(),
+ ],
+})
diff --git a/typescript/skeet-graphql/graphql/tests/graphql/express.test.ts b/typescript/skeet-graphql/graphql/tests/graphql/express.test.ts
new file mode 100644
index 000000000000..6bdd3329cb12
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/tests/graphql/express.test.ts
@@ -0,0 +1,16 @@
+import request from 'supertest'
+import { expressServer } from '../../src/index'
+
+describe('Skeet App is Running', () => {
+ test('GET', (done) => {
+ request(expressServer)
+ .get('/')
+ .then((res) => {
+ expect(res.statusCode).toBe(200)
+ done()
+ })
+ .catch((err) => {
+ done(err)
+ })
+ })
+})
diff --git a/typescript/skeet-graphql/graphql/tests/graphql/modelManager/User/query.test.ts b/typescript/skeet-graphql/graphql/tests/graphql/modelManager/User/query.test.ts
new file mode 100644
index 000000000000..6aba4eb7f9d5
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/tests/graphql/modelManager/User/query.test.ts
@@ -0,0 +1,36 @@
+import request from 'supertest'
+import { expressServer } from '../../../../src/index'
+
+describe('User Query', () => {
+ describe('usersConnection', () => {
+ let res: request.Response
+ beforeEach(async () => {
+ res = await request(expressServer)
+ .post('/graphql')
+ .send({
+ query: `query {
+ userConnection(first: 10) {
+ totalCount
+ edges {
+ cursor
+ node {
+ id
+ }
+ }
+ nodes {
+ id
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ }
+ }
+ }`,
+ })
+ })
+ it('successfully respond 200', () => {
+ expect(res.status).toBe(200)
+ })
+ })
+})
diff --git a/typescript/skeet-graphql/graphql/tests/jest.setup.ts b/typescript/skeet-graphql/graphql/tests/jest.setup.ts
new file mode 100644
index 000000000000..2b30dda89194
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/tests/jest.setup.ts
@@ -0,0 +1,7 @@
+import { expressServer } from '../src/index'
+
+beforeAll(async () => {})
+
+afterAll(async () => {
+ await (await expressServer).close()
+})
diff --git a/typescript/skeet-graphql/graphql/tsconfig.json b/typescript/skeet-graphql/graphql/tsconfig.json
new file mode 100755
index 000000000000..4b95849f3f4a
--- /dev/null
+++ b/typescript/skeet-graphql/graphql/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "CommonJS",
+ "outDir": "./dist",
+ "rootDir": ".",
+ "strict": true,
+ "moduleResolution": "node",
+ "baseUrl": ".",
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": false,
+ "resolveJsonModule": true,
+ "lib": ["esnext"],
+ "inlineSources": true,
+ "inlineSourceMap": false,
+ "declaration": false,
+ "noEmitOnError": true,
+ "typeRoots": ["./node_modules/@types"],
+ "sourceMap": true,
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "ts-node": {
+ "compilerOptions": {
+ "module": "NodeNext"
+ }
+ },
+ "include": ["src/**/*", "prisma/**/*", "test/**/*"],
+ "exclude": ["node_modules"],
+ "compileOnSave": true
+}
diff --git a/typescript/skeet-graphql/lib/firebaseAppConfig/skeet-graphql.mjs b/typescript/skeet-graphql/lib/firebaseAppConfig/skeet-graphql.mjs
new file mode 100644
index 000000000000..58bfbf4b7679
--- /dev/null
+++ b/typescript/skeet-graphql/lib/firebaseAppConfig/skeet-graphql.mjs
@@ -0,0 +1,10 @@
+const firebaseConfig = {
+ projectId: "skeet-graphql",
+ appId: "1:818638014982:web:2b634dc809a351340a9187",
+ storageBucket: "skeet-graphql.appspot.com",
+ apiKey: "AIzaSyAxTADDsXI9QENVObjIb7HVAj5LMMy3L0o",
+ authDomain: "skeet-graphql.firebaseapp.com",
+ messagingSenderId: "818638014982",
+ measurementId: "G-VSD8SQ0FZD",
+}
+export default firebaseConfig;
\ No newline at end of file
diff --git a/typescript/skeet-graphql/lib/firebaseConfig.mjs b/typescript/skeet-graphql/lib/firebaseConfig.mjs
new file mode 100644
index 000000000000..d198b9e21fa1
--- /dev/null
+++ b/typescript/skeet-graphql/lib/firebaseConfig.mjs
@@ -0,0 +1,10 @@
+const firebaseConfig = {
+ projectId: 'skeet-graphql',
+ appId: '1:818638014982:web:2b634dc809a351340a9187',
+ storageBucket: 'skeet-graphql.appspot.com',
+ apiKey: 'AIzaSyAxTADDsXI9QENVObjIb7HVAj5LMMy3L0o',
+ authDomain: 'skeet-graphql.firebaseapp.com',
+ messagingSenderId: '818638014982',
+ measurementId: 'G-VSD8SQ0FZD',
+}
+export default firebaseConfig
diff --git a/typescript/skeet-graphql/lib/firebaseConfig.ts b/typescript/skeet-graphql/lib/firebaseConfig.ts
new file mode 100644
index 000000000000..d198b9e21fa1
--- /dev/null
+++ b/typescript/skeet-graphql/lib/firebaseConfig.ts
@@ -0,0 +1,10 @@
+const firebaseConfig = {
+ projectId: 'skeet-graphql',
+ appId: '1:818638014982:web:2b634dc809a351340a9187',
+ storageBucket: 'skeet-graphql.appspot.com',
+ apiKey: 'AIzaSyAxTADDsXI9QENVObjIb7HVAj5LMMy3L0o',
+ authDomain: 'skeet-graphql.firebaseapp.com',
+ messagingSenderId: '818638014982',
+ measurementId: 'G-VSD8SQ0FZD',
+}
+export default firebaseConfig
diff --git a/typescript/skeet-graphql/next-env.d.ts b/typescript/skeet-graphql/next-env.d.ts
new file mode 100644
index 000000000000..4f11a03dc6cc
--- /dev/null
+++ b/typescript/skeet-graphql/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/typescript/skeet-graphql/next-i18next.config.js b/typescript/skeet-graphql/next-i18next.config.js
new file mode 100644
index 000000000000..e950d7c9f310
--- /dev/null
+++ b/typescript/skeet-graphql/next-i18next.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'ja'],
+ },
+}
diff --git a/typescript/skeet-graphql/next.config.js b/typescript/skeet-graphql/next.config.js
new file mode 100644
index 000000000000..831cb6cedfe0
--- /dev/null
+++ b/typescript/skeet-graphql/next.config.js
@@ -0,0 +1,33 @@
+/** @type {import('next').NextConfig} */
+const relay = require('./relay.config.js')
+
+const nextConfig = {
+ trailingSlash: true,
+ reactStrictMode: true,
+ swcMinify: true,
+ images: { unoptimized: true },
+ compiler: {
+ relay,
+ },
+}
+
+const intercept = require('intercept-stdout')
+
+// safely ignore recoil warning messages in dev (triggered by HMR)
+function interceptStdout(text) {
+ if (text.includes('Duplicate atom key')) {
+ return ''
+ }
+ return text
+}
+
+if (process.env.NODE_ENV === 'development') {
+ intercept(interceptStdout)
+}
+
+const withPWA = require('next-pwa')({
+ disable: process.env.NODE_ENV !== 'production',
+ dest: 'public',
+})
+
+module.exports = withPWA({ ...nextConfig })
diff --git a/typescript/skeet-graphql/package.json b/typescript/skeet-graphql/package.json
new file mode 100644
index 000000000000..7eeab661c3b5
--- /dev/null
+++ b/typescript/skeet-graphql/package.json
@@ -0,0 +1,131 @@
+{
+ "name": "skeet-graphql",
+ "version": "1.0.0",
+ "description": "Skeet Framework Boilerplate with Next.js and GraphQL",
+ "repository": "https://github.com/elsoul/skeet-graphql.git",
+ "author": "ELSOUL LABO B.V.",
+ "license": "Apache-2.0",
+ "private": false,
+ "scripts": {
+ "format": "prettier --write --ignore-unknown .",
+ "lint": "eslint --ext .ts,.tsx --fix .",
+ "skeet": "run-p skeet:*",
+ "skeet:graphql": "yarn --cwd ./graphql dev",
+ "skeet:openai": "yarn --cwd functions/openai dev",
+ "skeet:dev": "firebase emulators:start",
+ "skeet:webapp": "yarn dev",
+ "update:packages": "ncu -u -x 'node-fetch,glob' && yarn",
+ "db:deploy": "yarn --cwd ./graphql db:deploy",
+ "db:seed": "yarn --cwd ./graphql db:seed",
+ "dev": "run-p 'dev:*'",
+ "dev:relay": "relay-compiler --watch",
+ "dev:next": "next dev -p 4200",
+ "build:production:webapp": "yarn build",
+ "build": "relay-compiler && NODE_ENV=production next build && NODE_ENV=production next export -o web-build",
+ "postbuild": "npx ts-node -r tsconfig-paths/register --transpile-only ./scripts/generateSitemap.ts",
+ "deploy": "yarn build && npx firebase deploy --only hosting",
+ "deploy:rules": "npx firebase deploy --only firestore:rules,storage",
+ "check:webapp": "npx serve web-build",
+ "send:sitemap": "npx ts-node -r tsconfig-paths/register --transpile-only ./scripts/sendSitemap.ts"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-svg-core": "6.4.0",
+ "@fortawesome/free-brands-svg-icons": "6.4.0",
+ "@fortawesome/free-regular-svg-icons": "6.4.0",
+ "@fortawesome/react-fontawesome": "0.2.0",
+ "@headlessui/react": "1.7.15",
+ "@heroicons/react": "2.0.18",
+ "@hookform/resolvers": "3.1.1",
+ "@skeet-framework/firestore": "1.1.2",
+ "@skeet-framework/utils": "^0.8.5",
+ "clsx": "1.2.1",
+ "date-fns": "2.30.0",
+ "dotenv": "16.3.1",
+ "firebase": "9.23.0",
+ "highlight.js": "11.8.0",
+ "i18next": "23.2.3",
+ "lodash.throttle": "4.1.1",
+ "markdown-to-txt": "2.0.1",
+ "next": "13.4.7",
+ "next-i18next": "14.0.0",
+ "next-language-detector": "1.0.2",
+ "next-pwa": "5.6.0",
+ "next-themes": "0.2.1",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-dropzone": "14.2.3",
+ "react-hook-form": "7.45.1",
+ "react-i18next": "13.0.1",
+ "react-relay": "15.0.0",
+ "react-relay-network-modern": "6.2.2",
+ "recoil": "0.7.7",
+ "recoil-persist": "5.0.1",
+ "regenerator-runtime": "0.13.11",
+ "relay-runtime": "15.0.0",
+ "text-encoding": "0.7.0",
+ "zod": "3.21.4"
+ },
+ "devDependencies": {
+ "@tailwindcss/forms": "0.5.3",
+ "@tailwindcss/typography": "0.5.9",
+ "@types/github-slugger": "1.3.0",
+ "@types/glob": "8.1.0",
+ "@types/jest": "29.5.2",
+ "@types/node": "20.3.2",
+ "@types/node-fetch": "2.6.4",
+ "@types/react": "18.2.14",
+ "@types/react-dom": "18.2.6",
+ "@types/react-relay": "14.1.4",
+ "@types/relay-runtime": "14.1.12",
+ "@types/text-encoding": "0.0.36",
+ "@typescript-eslint/eslint-plugin": "5.60.1",
+ "@typescript-eslint/parser": "5.60.1",
+ "autoprefixer": "10.4.14",
+ "cssnano": "6.0.1",
+ "dotenv": "16.3.1",
+ "esbuild": "0.18.9",
+ "eslint": "8.43.0",
+ "eslint-config-next": "13.4.7",
+ "eslint-config-prettier": "8.8.0",
+ "eslint-plugin-react": "7.32.2",
+ "eslint-plugin-react-hooks": "4.6.0",
+ "fast-xml-parser": "4.2.5",
+ "firebase-tools": "12.4.0",
+ "github-slugger": "2.0.0",
+ "glob": "8.1.0",
+ "gray-matter": "4.0.3",
+ "intercept-stdout": "0.1.2",
+ "jest": "29.5.0",
+ "mdast-util-gfm-table": "1.0.7",
+ "mdast-util-to-string": "3.2.0",
+ "node-fetch": "2.6.7",
+ "nodemon": "2.0.22",
+ "npm-check-updates": "16.10.13",
+ "npm-run-all": "4.1.5",
+ "postcss": "8.4.24",
+ "prettier": "2.8.8",
+ "prettier-plugin-tailwindcss": "0.3.0",
+ "raw-loader": "4.0.2",
+ "rehype-code-titles": "1.2.0",
+ "rehype-highlight": "6.0.0",
+ "rehype-parse": "8.0.4",
+ "rehype-remark": "9.1.2",
+ "rehype-stringify": "9.0.3",
+ "relay-compiler": "15.0.0",
+ "remark": "14.0.3",
+ "remark-directive": "2.0.1",
+ "remark-external-links": "9.0.1",
+ "remark-gfm": "3.0.1",
+ "remark-parse": "10.0.2",
+ "remark-rehype": "10.1.0",
+ "remark-slug": "7.0.1",
+ "remark-stringify": "10.0.3",
+ "tailwind-scrollbar-hide": "1.1.7",
+ "tailwindcss": "3.3.2",
+ "ts-jest": "29.1.0",
+ "ts-loader": "9.4.3",
+ "tsconfig-paths": "4.2.0",
+ "typescript": "5.1.3",
+ "unified": "10.1.2"
+ }
+}
diff --git a/typescript/skeet-graphql/postcss.config.js b/typescript/skeet-graphql/postcss.config.js
new file mode 100644
index 000000000000..11e8b4b06b4a
--- /dev/null
+++ b/typescript/skeet-graphql/postcss.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
+ },
+}
diff --git a/typescript/skeet-graphql/public/android-chrome-192x192.png b/typescript/skeet-graphql/public/android-chrome-192x192.png
new file mode 100644
index 000000000000..09315a30b903
Binary files /dev/null and b/typescript/skeet-graphql/public/android-chrome-192x192.png differ
diff --git a/typescript/skeet-graphql/public/android-chrome-512x512.png b/typescript/skeet-graphql/public/android-chrome-512x512.png
new file mode 100644
index 000000000000..ac5c246cc6e3
Binary files /dev/null and b/typescript/skeet-graphql/public/android-chrome-512x512.png differ
diff --git a/typescript/skeet-graphql/public/apple-touch-icon.png b/typescript/skeet-graphql/public/apple-touch-icon.png
new file mode 100644
index 000000000000..8e8e1d20d27b
Binary files /dev/null and b/typescript/skeet-graphql/public/apple-touch-icon.png differ
diff --git a/typescript/skeet-graphql/public/browserconfig.xml b/typescript/skeet-graphql/public/browserconfig.xml
new file mode 100644
index 000000000000..d416bc536fbc
--- /dev/null
+++ b/typescript/skeet-graphql/public/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #ffffff
+
+
+
diff --git a/typescript/skeet-graphql/public/favicon-16x16.png b/typescript/skeet-graphql/public/favicon-16x16.png
new file mode 100644
index 000000000000..aeccc1de0638
Binary files /dev/null and b/typescript/skeet-graphql/public/favicon-16x16.png differ
diff --git a/typescript/skeet-graphql/public/favicon-32x32.png b/typescript/skeet-graphql/public/favicon-32x32.png
new file mode 100644
index 000000000000..2d6337037193
Binary files /dev/null and b/typescript/skeet-graphql/public/favicon-32x32.png differ
diff --git a/typescript/skeet-graphql/public/favicon.ico b/typescript/skeet-graphql/public/favicon.ico
new file mode 100644
index 000000000000..481b2d7a3954
Binary files /dev/null and b/typescript/skeet-graphql/public/favicon.ico differ
diff --git a/typescript/skeet-graphql/public/favicon.svg b/typescript/skeet-graphql/public/favicon.svg
new file mode 100644
index 000000000000..f1d86645d9b0
--- /dev/null
+++ b/typescript/skeet-graphql/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/public/locales/en/auth.json b/typescript/skeet-graphql/public/locales/en/auth.json
new file mode 100644
index 000000000000..c12c8c1e04e7
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/auth.json
@@ -0,0 +1,45 @@
+{
+ "goToLogin": "Go to sign in",
+ "invalidParamsErrorTitle": "Invalid URL",
+ "invalidParamsErrorBody": "Sorry, Something went wrong... Please try it again later.",
+ "loginToYourAccount": "Sign in to your account",
+ "or": "Or",
+ "registerYourAccount": "Register your account",
+ "registerPassword": "Register password",
+ "email": "Email",
+ "username": "Username",
+ "password": "Password",
+ "forgotYourPassword": "Forgot your password?",
+ "linkError": "Link Error",
+ "urlError": "Could not open the link",
+ "resetYourPassword": "Reset your password",
+ "reset": "Reset",
+ "sentResetPasswordRequest": "Succeed Reset Password Request",
+ "confirmEmail": "Please check your email",
+ "resetRequestErrorTitle": "Reset Error",
+ "resetRequestErrorBody": "Failed to reset password. Please check your email address.",
+ "inputNewPassword": "Please input your new password.",
+ "resetPasswordSuccessTitle": "Registered your new password🎉",
+ "resetPasswordSuccessBody": "Your new password has been registered. Please sign in with it.",
+ "resetPasswordErrorTitle": "Reset Error",
+ "resetPasswordErrorBody": "Failed to reset password. Please try it again later.",
+ "sentConfirmEmailTitle": "Sent confirmation email",
+ "sentConfirmEmailBody": "Thank you for your registration. Please check your email.",
+ "thanksForRequest": "Thank you. We sent the email for your confirmation so please check your registered email.",
+ "emailErrorText": "Please input email address.",
+ "usernameErrorText": "Please input username (1~20 characters).",
+ "passwordErrorText": "Please enter a password of at least 8 characters.",
+ "privacyErrorText": "Please agree to the privacy policy.",
+ "errorNotVerifiedTitle": "Not verified.",
+ "errorNotVerifiedBody": "Sent email to verify. Please check your email box.",
+ "verifySuccessTitle": "Verify Success🎉",
+ "verifySuccessBody": "Welcome to Skeet App Template",
+ "verifyErrorTitle": "Verify Error",
+ "verifyErrorBody": "Something went wrong... Please try it again later.",
+ "confirmDoneTitle": "Confirmation completed!",
+ "confirmDoneBody": "Thank you for the confirmation. Welcome to Skeet App Template🙌",
+ "alreadyExistTitle": "Already exist",
+ "alreadyExistBody": "This email address is already exist. Please try to sign in.",
+ "userNotFoundTitle": "User not found",
+ "userNotFoundBody": "This email address is not registered. Please try to sign up."
+}
diff --git a/typescript/skeet-graphql/public/locales/en/chat.json b/typescript/skeet-graphql/public/locales/en/chat.json
new file mode 100644
index 000000000000..2f15a89309f6
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/chat.json
@@ -0,0 +1,21 @@
+{
+ "title": "Open AI Chat",
+ "newChat": "New Chat",
+ "chatList": "Chat List",
+ "createChatRoom": "Create New Chat",
+ "chatRoomCreatedSuccessTitle": "Chat Room Created",
+ "chatRoomCreatedSuccessBody": "Chat room has been created successfully.",
+ "model": "Model",
+ "modelErrorText": "Please select a model from the list.",
+ "maxTokens": "Max Tokens (100 ~ 4096: more thinking)",
+ "maxTokensErrorText": "Please enter a number between 100 and 4096.",
+ "temperature": "Temperature (0: most conservative ~ 2: most creative)",
+ "temperatureErrorText": "Please enter a number between 0 and 2.",
+ "defaultSystemContent": "This is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly.",
+ "systemContent": "AI Character Setting",
+ "systemContentErrorText": "Please enter AI character setting in 1000 characters.",
+ "chatGPTCustom": "Chat GPT Custom with API",
+ "chatMessageSubmit": "Submit",
+ "pleaseRefetch": "Excuse me. Failed to get data. Please try again later.",
+ "refetchButton": "Retry"
+}
diff --git a/typescript/skeet-graphql/public/locales/en/common.json b/typescript/skeet-graphql/public/locales/en/common.json
new file mode 100644
index 000000000000..d9ef8dc46001
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/common.json
@@ -0,0 +1,49 @@
+{
+ "agreeOn": "Agree on",
+ "terms": "Terms",
+ "privacy": "Privacy Policy",
+ "404title": "Page not found",
+ "404body": "Sorry, we couldn't find the page you're looking for.",
+ "backToTop": "Back to top page",
+ "toc": "Table of Contents",
+ "openMenu": "Open menu",
+ "closeMenu": "Close menu",
+ "aiChat": "AI Chat",
+ "login": "Sign in",
+ "register": "Sign up",
+ "navs": {
+ "defaultMainNav": {
+ "news": "News",
+ "doc": "Doc"
+ },
+ "commonFooterNav": {
+ "news": "News",
+ "doc": "Doc",
+ "privacy": "Privacy"
+ }
+ },
+ "AgreeToPolicy": {
+ "title": "Your Choices Regarding Cookies",
+ "body": "We and our third party partners use cookies and similar technologies to process certain information, such as your IP address and digital identifiers, to analyze site usage and provide you better experiences. Please read our privacy policy for the detail.",
+ "yes": "Yes, I Accept",
+ "no": "No, I Do Not Accept"
+ },
+ "DiscordRow": {
+ "title": "Community Discord",
+ "body": "If you have any inquiries, please create a support ticket in the community Discord.",
+ "button": "JOIN"
+ },
+ "succeedLogin": "Succeed to sign in🎉",
+ "howdy": "Howdy?",
+ "errorLoginTitle": "Failed to sign in.",
+ "errorLoginBody": "Something went wrong... Please try it again.",
+ "succeedLogout": "Succeed to sign out",
+ "seeYouSoon": "See you soon👋",
+ "logout": "Sign out",
+ "errorTokenExpiredTitle": "Login expired",
+ "errorTokenExpiredBody": "Please sign in again.",
+ "errorTitle": "Error",
+ "errorBody": "Something went wrong... Please try it again later.",
+ "noTitle": "No title",
+ "tokens": "tokens"
+}
diff --git a/typescript/skeet-graphql/public/locales/en/doc.json b/typescript/skeet-graphql/public/locales/en/doc.json
new file mode 100644
index 000000000000..550eb07b4f90
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/doc.json
@@ -0,0 +1,29 @@
+{
+ "title": "Doc Home",
+ "body": "You can start creating documents compatible with syntax highlights immediately",
+ "previousPage": "Previous page",
+ "nextPage": "Next page",
+ "actions": {
+ "motivation": {
+ "title": "Motivation",
+ "body": "Skeet allows you to get your app up and running quickly and maintain it for the long term at a low cost."
+ },
+ "quickstart": {
+ "title": "Quickstart",
+ "body": "Describes the setup for getting started with the Skeet framework."
+ }
+ },
+ "menuNav": {
+ "home": "Doc Home",
+ "general": {
+ "groupTitle": "General",
+ "motivation": "Motivation",
+ "quickstart": "Quickstart",
+ "readme": "README"
+ }
+ },
+ "headerNav": {
+ "home": "Home",
+ "news": "News"
+ }
+}
diff --git a/typescript/skeet-graphql/public/locales/en/home.json b/typescript/skeet-graphql/public/locales/en/home.json
new file mode 100644
index 000000000000..55785130133f
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/home.json
@@ -0,0 +1,5 @@
+{
+ "HeroRow": {
+ "body": "Next.js Boilerplate. SEO compatible, i18n translation, SSG, PWA. You can start building your WebApp today, and its deployment is guaranteed."
+ }
+}
diff --git a/typescript/skeet-graphql/public/locales/en/legal.json b/typescript/skeet-graphql/public/locales/en/legal.json
new file mode 100644
index 000000000000..0967ef424bce
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/legal.json
@@ -0,0 +1 @@
+{}
diff --git a/typescript/skeet-graphql/public/locales/en/news.json b/typescript/skeet-graphql/public/locales/en/news.json
new file mode 100644
index 000000000000..6fbf8d97defb
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/news.json
@@ -0,0 +1,4 @@
+{
+ "title": "News",
+ "body": "From Skeet Dev"
+}
diff --git a/typescript/skeet-graphql/public/locales/en/settings.json b/typescript/skeet-graphql/public/locales/en/settings.json
new file mode 100644
index 000000000000..5c35c7030bf4
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/settings.json
@@ -0,0 +1,19 @@
+{
+ "title": "Settings",
+ "register": "Register",
+ "username": "Username",
+ "editIconUrl": "Edit avatar image",
+ "editProfile": "Edit profile",
+ "avatarUpdated": "Avatar Updated",
+ "avatarUpdatedMessage": "Successfully updated your avatar image🎉",
+ "avatarUpdatedError": "Avatar Update Failed",
+ "avatarUpdatedErrorMessage": "Something went wrong... Please try it again later.",
+ "updateProfileSuccess": "Profile Updated",
+ "updateProfileSuccessMessage": "Successfully updated your profile🎉",
+ "updateProfileError": "Profile Update Failed",
+ "updateProfileErrorMessage": "Something went wrong... Please try it again later.",
+ "usernameErrorText": "Please input username (1~20 characters).",
+ "upload": "Upload",
+ "dropFiles": "Drop the files here ...",
+ "dragDropFiles": "Drag 'n' drop some files here, or click to select files"
+}
diff --git a/typescript/skeet-graphql/public/locales/en/user.json b/typescript/skeet-graphql/public/locales/en/user.json
new file mode 100644
index 000000000000..3d00ed624853
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/en/user.json
@@ -0,0 +1,9 @@
+{
+ "menuNav": {
+ "chat": "Open AI Chat",
+ "settings": "Settings"
+ },
+ "headerNav": {
+ "settings": "Settings"
+ }
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/auth.json b/typescript/skeet-graphql/public/locales/ja/auth.json
new file mode 100644
index 000000000000..57c9a0d23e2c
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/auth.json
@@ -0,0 +1,45 @@
+{
+ "goToLogin": "ログインページへ",
+ "invalidParamsErrorTitle": "不正なURLです",
+ "invalidParamsErrorBody": "すみません、なにか問題が発生しました。もう一度お試しください。",
+ "loginToYourAccount": "アカウントにログイン",
+ "or": "もしくは",
+ "registerPassword": "パスワードを登録",
+ "registerYourAccount": "アカウントを作成",
+ "email": "メールアドレス",
+ "username": "ユーザー名",
+ "password": "パスワード",
+ "forgotYourPassword": "パスワードをお忘れですか?",
+ "linkError": "リンクエラー",
+ "urlError": "開けないURLです",
+ "resetYourPassword": "パスワードのリセット",
+ "reset": "リセット",
+ "sentResetPasswordRequest": "リセットリクエストを送信しました",
+ "confirmEmail": "メールを確認してください",
+ "resetRequestErrorTitle": "リセットエラー",
+ "resetRequestErrorBody": "パスワードリセットに失敗しました。メールアドレスを確認してください。",
+ "inputNewPassword": "新しいパスワードを入力してください。",
+ "resetPasswordSuccessTitle": "パスワードが新しくなりました🎉",
+ "resetPasswordSuccessBody": "新しいパスワードを登録しました。早速ログインしてみてください。",
+ "resetPasswordErrorTitle": "リセットエラー",
+ "resetPasswordErrorBody": "パスワードリセットに失敗しました。後ほどもう一度お試しください。",
+ "sentConfirmEmailTitle": "確認メールを送信しました",
+ "sentConfirmEmailBody": "ご登録ありがとうございます。メールボックスをご確認ください。",
+ "thanksForRequest": "ありがとうございます。確認のためメールをお送りしましたので、ご登録のメールアドレスをご確認ください。",
+ "emailErrorText": "メールアドレスを入力してください。",
+ "usernameErrorText": "1~20文字のユーザー名を入力してください。",
+ "passwordErrorText": "8文字以上のパスワードを入力してください。",
+ "privacyErrorText": "プライバシーポリシーに同意してください。",
+ "errorNotVerifiedTitle": "まだ認証されていません",
+ "errorNotVerifiedBody": "認証メールをお送りいたしました。メールボックスを確認してください。",
+ "verifySuccessTitle": "認証成功🎉",
+ "verifySuccessBody": "Skeet App Templateへようこそ",
+ "verifyErrorTitle": "認証エラー",
+ "verifyErrorBody": "エラーが発生しました。もう一度お試しください。",
+ "confirmDoneTitle": "確認完了!",
+ "confirmDoneBody": "ご確認ありがとうございます。そしてSkeet App Templateへようこそ🙌",
+ "alreadyExistTitle": "すでに登録されています。",
+ "alreadyExistBody": "こちらのメールアドレスはすでに登録されています。ログインをお試しください。",
+ "userNotFoundTitle": "ユーザーが見つかりません",
+ "userNotFoundBody": "このメールアドレスはまだ登録されていません。アカウントを作成してください。"
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/chat.json b/typescript/skeet-graphql/public/locales/ja/chat.json
new file mode 100644
index 000000000000..f07efa5ef63a
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/chat.json
@@ -0,0 +1,21 @@
+{
+ "title": "オープンAIチャット",
+ "newChat": "チャットルーム作成",
+ "chatList": "チャットルーム一覧",
+ "createChatRoom": "新しいチャットを開始",
+ "chatRoomCreatedSuccessTitle": "チャットルーム作成完了",
+ "chatRoomCreatedSuccessBody": "チャットルームが作成されました。",
+ "model": "モデル",
+ "modelErrorText": "モデルをリストから選択してください。",
+ "maxTokens": "最大トークン数 (100 ~ 4096:より多く考える)",
+ "maxTokensErrorText": "100から4096までの数値を入力してください。",
+ "temperature": "確度 (0:最も固い ~ 2:奇抜な発想)",
+ "temperatureErrorText": "0から2までの数値を入力してください。",
+ "defaultSystemContent": "これはAIアシスタントとの会話です。アシスタントは親切で、創造的で、賢くて、とてもフレンドリーです。",
+ "systemContent": "AIのキャラ設定",
+ "systemContentErrorText": "AIのキャラ設定を入力してください。(1000文字まで)",
+ "chatGPTCustom": "カスタム Chat GPT with API",
+ "chatMessageSubmit": "送信",
+ "pleaseRefetch": "すみません。データ取得に失敗しました。時間をおいて再度取得してください。",
+ "refetchButton": "再取得"
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/common.json b/typescript/skeet-graphql/public/locales/ja/common.json
new file mode 100644
index 000000000000..0105e3de2d66
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/common.json
@@ -0,0 +1,49 @@
+{
+ "agreeOn": "次に同意:",
+ "terms": "利用規約",
+ "privacy": "プライバシーポリシー",
+ "404title": "ページが見つかりませんでした",
+ "404body": "すみません、お探しのページは見つかりませんでした。",
+ "backToTop": "トップページに戻る",
+ "toc": "目次",
+ "openMenu": "メニューを開く",
+ "closeMenu": "メニューを閉じる",
+ "aiChat": "AIチャット",
+ "login": "ログイン",
+ "register": "アカウント登録",
+ "navs": {
+ "defaultMainNav": {
+ "news": "ニュース",
+ "doc": "ドキュメント"
+ },
+ "commonFooterNav": {
+ "news": "ニュース",
+ "doc": "ドキュメント",
+ "privacy": "プライバシーポリシー"
+ }
+ },
+ "AgreeToPolicy": {
+ "title": "クッキーについて",
+ "body": "私達および私達のサードパーティパートナーは、Cookie および類似の技術を使用して、IP アドレスやデジタル識別子などの特定の情報を処理し、サイトの使用状況を分析し、ユーザー体験向上に努めます。 詳しくはプライバシーポリシーをお読みください。",
+ "yes": "はい、承諾します",
+ "no": "いいえ、やめてください"
+ },
+ "DiscordRow": {
+ "title": "コミュニティ Discord",
+ "body": "各種お問い合わせ等ございましたらコミュニティDiscord内にてサポートチケットを作成してください。",
+ "button": "JOIN"
+ },
+ "succeedLogin": "ログイン成功🎉",
+ "howdy": "おつかれさまです",
+ "errorLoginTitle": "ログイン失敗",
+ "errorLoginBody": "大変お手数おかけしますが、もう一度お試しください。",
+ "succeedLogout": "ログアウトしました",
+ "seeYouSoon": "では、また👋",
+ "logout": "ログアウト",
+ "errorTokenExpiredTitle": "ログイン期限切れ",
+ "errorTokenExpiredBody": "もう一度ログインしてください。",
+ "errorTitle": "エラーです",
+ "errorBody": "大変お手数おかけしますが、もう一度お試しください。",
+ "noTitle": "無題",
+ "tokens": "トークン"
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/doc.json b/typescript/skeet-graphql/public/locales/ja/doc.json
new file mode 100644
index 000000000000..2dfe62b2bc2b
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/doc.json
@@ -0,0 +1,29 @@
+{
+ "title": "ドキュメントホーム",
+ "body": "シンタックスハイライト対応のドキュメント作成をすぐにスタートできます",
+ "previousPage": "前のページ",
+ "nextPage": "次のページ",
+ "actions": {
+ "motivation": {
+ "title": "モチベーション",
+ "body": "Skeet は素早くアプリを立ち上げ、少ないコストで長期的にメンテナンスしていくことを可能にします。"
+ },
+ "quickstart": {
+ "title": "クイックスタート",
+ "body": "Skeet フレームワークを使い始めるための設定について説明します。"
+ }
+ },
+ "menuNav": {
+ "home": "ドキュメントホーム",
+ "general": {
+ "groupTitle": "全般",
+ "motivation": "モチベーション",
+ "quickstart": "クイックスタート",
+ "readme": "README"
+ }
+ },
+ "headerNav": {
+ "home": "ホームページ",
+ "news": "ニュース"
+ }
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/home.json b/typescript/skeet-graphql/public/locales/ja/home.json
new file mode 100644
index 000000000000..b5e1e099170b
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/home.json
@@ -0,0 +1,5 @@
+{
+ "HeroRow": {
+ "body": "Next.jsのボイラープレート。SEO対応、多言語対応、SSG、PWA。WebAppをすぐに構築開始でき、そのデプロイは保証されています。"
+ }
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/legal.json b/typescript/skeet-graphql/public/locales/ja/legal.json
new file mode 100644
index 000000000000..0967ef424bce
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/legal.json
@@ -0,0 +1 @@
+{}
diff --git a/typescript/skeet-graphql/public/locales/ja/news.json b/typescript/skeet-graphql/public/locales/ja/news.json
new file mode 100644
index 000000000000..30626c2cad14
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/news.json
@@ -0,0 +1,4 @@
+{
+ "title": "ニュース",
+ "body": "from Skeet Dev"
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/settings.json b/typescript/skeet-graphql/public/locales/ja/settings.json
new file mode 100644
index 000000000000..7bfcde3608da
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/settings.json
@@ -0,0 +1,19 @@
+{
+ "title": "設定",
+ "register": "登録",
+ "username": "ユーザー名",
+ "editIconUrl": "アバター画像を変更",
+ "editProfile": "プロフィールを編集",
+ "avatarUpdated": "アバター更新成功",
+ "avatarUpdatedMessage": "正常にアバター画像を更新できました🎉",
+ "avatarUpdatedError": "アバター更新失敗",
+ "avatarUpdatedErrorMessage": "アバター画像の更新に失敗しました😢もう一度お試しください",
+ "updateProfileSuccess": "プロフィール更新成功",
+ "updateProfileSuccessMessage": "正常にプロフィールを更新できました🎉",
+ "updateProfileError": "プロフィール更新失敗",
+ "updateProfileErrorMessage": "プロフィールの更新に失敗しました😢もう一度お試しください",
+ "usernameErrorText": "1~20文字のユーザー名を入力してください。",
+ "upload": "アップロード",
+ "dropFiles": "ファイルをここにドロップしてください...",
+ "dragDropFiles": "ファイルをドラッグ&ドロップするか、クリックしてファイルを選択してください"
+}
diff --git a/typescript/skeet-graphql/public/locales/ja/user.json b/typescript/skeet-graphql/public/locales/ja/user.json
new file mode 100644
index 000000000000..2948246e026e
--- /dev/null
+++ b/typescript/skeet-graphql/public/locales/ja/user.json
@@ -0,0 +1,9 @@
+{
+ "menuNav": {
+ "chat": "オープンAIチャット",
+ "settings": "設定"
+ },
+ "headerNav": {
+ "settings": "設定"
+ }
+}
diff --git a/typescript/skeet-graphql/public/mstile-144x144.png b/typescript/skeet-graphql/public/mstile-144x144.png
new file mode 100644
index 000000000000..8ad64a8221b9
Binary files /dev/null and b/typescript/skeet-graphql/public/mstile-144x144.png differ
diff --git a/typescript/skeet-graphql/public/mstile-150x150.png b/typescript/skeet-graphql/public/mstile-150x150.png
new file mode 100644
index 000000000000..a6b30b8900c3
Binary files /dev/null and b/typescript/skeet-graphql/public/mstile-150x150.png differ
diff --git a/typescript/skeet-graphql/public/mstile-310x150.png b/typescript/skeet-graphql/public/mstile-310x150.png
new file mode 100644
index 000000000000..cc98652d549d
Binary files /dev/null and b/typescript/skeet-graphql/public/mstile-310x150.png differ
diff --git a/typescript/skeet-graphql/public/mstile-310x310.png b/typescript/skeet-graphql/public/mstile-310x310.png
new file mode 100644
index 000000000000..1e9eb06679e0
Binary files /dev/null and b/typescript/skeet-graphql/public/mstile-310x310.png differ
diff --git a/typescript/skeet-graphql/public/mstile-70x70.png b/typescript/skeet-graphql/public/mstile-70x70.png
new file mode 100644
index 000000000000..f10b7b5915f3
Binary files /dev/null and b/typescript/skeet-graphql/public/mstile-70x70.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/06/13/EffortlessServerlessSkeet.png b/typescript/skeet-graphql/public/news/2023/06/13/EffortlessServerlessSkeet.png
new file mode 100644
index 000000000000..9de705b44ad2
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/06/13/EffortlessServerlessSkeet.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/06/19/SkeetDemoPublished.png b/typescript/skeet-graphql/public/news/2023/06/19/SkeetDemoPublished.png
new file mode 100644
index 000000000000..006e82bd73ca
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/06/19/SkeetDemoPublished.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/06/19/SkeeterAppSample16-9.png b/typescript/skeet-graphql/public/news/2023/06/19/SkeeterAppSample16-9.png
new file mode 100644
index 000000000000..0332513bdd22
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/06/19/SkeeterAppSample16-9.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/06/23/SkeetTypeSafeFirestore2.png b/typescript/skeet-graphql/public/news/2023/06/23/SkeetTypeSafeFirestore2.png
new file mode 100644
index 000000000000..b8cb5f92bdc6
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/06/23/SkeetTypeSafeFirestore2.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/06/29/SkeetTutorialYouTubeThumbnail.png b/typescript/skeet-graphql/public/news/2023/06/29/SkeetTutorialYouTubeThumbnail.png
new file mode 100644
index 000000000000..26a9b13824e0
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/06/29/SkeetTutorialYouTubeThumbnail.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/06/29/SkeetTutorialYouTubeThumbnail2.png b/typescript/skeet-graphql/public/news/2023/06/29/SkeetTutorialYouTubeThumbnail2.png
new file mode 100644
index 000000000000..93bc434d545f
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/06/29/SkeetTutorialYouTubeThumbnail2.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/07/10/ChatWithCodeHighlight.png b/typescript/skeet-graphql/public/news/2023/07/10/ChatWithCodeHighlight.png
new file mode 100644
index 000000000000..731d2c61bcb4
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/07/10/ChatWithCodeHighlight.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/07/10/CreateChatRoom.png b/typescript/skeet-graphql/public/news/2023/07/10/CreateChatRoom.png
new file mode 100644
index 000000000000..36ee24a02f54
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/07/10/CreateChatRoom.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/07/10/NewReleaseSkeetxNextjs.png b/typescript/skeet-graphql/public/news/2023/07/10/NewReleaseSkeetxNextjs.png
new file mode 100644
index 000000000000..3f0f80e80777
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/07/10/NewReleaseSkeetxNextjs.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/07/10/SkeetCreateSelectTemplate.png b/typescript/skeet-graphql/public/news/2023/07/10/SkeetCreateSelectTemplate.png
new file mode 100644
index 000000000000..b770bc3d00c1
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/07/10/SkeetCreateSelectTemplate.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/07/10/WebAppBoilerplate.png b/typescript/skeet-graphql/public/news/2023/07/10/WebAppBoilerplate.png
new file mode 100644
index 000000000000..3d39e345045d
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/07/10/WebAppBoilerplate.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/08/01/apollo-console.png b/typescript/skeet-graphql/public/news/2023/08/01/apollo-console.png
new file mode 100644
index 000000000000..9def8427d092
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/08/01/apollo-console.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/08/01/prisma-studio.jpg b/typescript/skeet-graphql/public/news/2023/08/01/prisma-studio.jpg
new file mode 100644
index 000000000000..7cfd8b9ae604
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/08/01/prisma-studio.jpg differ
diff --git a/typescript/skeet-graphql/public/news/2023/08/01/skeet-create-got-graphql.png b/typescript/skeet-graphql/public/news/2023/08/01/skeet-create-got-graphql.png
new file mode 100644
index 000000000000..f5a59740c6ca
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/08/01/skeet-create-got-graphql.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/08/01/skeet-graphql.png b/typescript/skeet-graphql/public/news/2023/08/01/skeet-graphql.png
new file mode 100644
index 000000000000..1c4b222352d5
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/08/01/skeet-graphql.png differ
diff --git a/typescript/skeet-graphql/public/news/2023/08/01/skeet-next-graphql.png b/typescript/skeet-graphql/public/news/2023/08/01/skeet-next-graphql.png
new file mode 100644
index 000000000000..46174ea2a79b
Binary files /dev/null and b/typescript/skeet-graphql/public/news/2023/08/01/skeet-next-graphql.png differ
diff --git a/typescript/skeet-graphql/public/ogp.png b/typescript/skeet-graphql/public/ogp.png
new file mode 100644
index 000000000000..183de6555e13
Binary files /dev/null and b/typescript/skeet-graphql/public/ogp.png differ
diff --git a/typescript/skeet-graphql/public/robots.txt b/typescript/skeet-graphql/public/robots.txt
new file mode 100644
index 000000000000..91744325bc24
--- /dev/null
+++ b/typescript/skeet-graphql/public/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Allow: /
+Sitemap: https://graphql-next.skeet.dev/sitemap.xml
diff --git a/typescript/skeet-graphql/public/safari-pinned-tab.svg b/typescript/skeet-graphql/public/safari-pinned-tab.svg
new file mode 100644
index 000000000000..510f02195e8e
--- /dev/null
+++ b/typescript/skeet-graphql/public/safari-pinned-tab.svg
@@ -0,0 +1,50 @@
+
+
+
+
+Created by potrace 1.14, written by Peter Selinger 2001-2017
+
+
+
+
+
+
+
diff --git a/typescript/skeet-graphql/public/site.webmanifest b/typescript/skeet-graphql/public/site.webmanifest
new file mode 100644
index 000000000000..452adce76410
--- /dev/null
+++ b/typescript/skeet-graphql/public/site.webmanifest
@@ -0,0 +1,20 @@
+{
+ "name": "Skeet Next GraphQL",
+ "short_name": "Skeet Next GraphQL",
+ "start_url": "/",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/typescript/skeet-graphql/relay.config.js b/typescript/skeet-graphql/relay.config.js
new file mode 100755
index 000000000000..69746dfbb0b3
--- /dev/null
+++ b/typescript/skeet-graphql/relay.config.js
@@ -0,0 +1,19 @@
+module.exports = {
+ src: './src',
+ language: 'typescript',
+ schema: './graphql/src/schema/schema.graphql',
+ artifactDirectory: 'src/__generated__',
+ exclude: [
+ '**/node_modules/**',
+ '**/__mocks__/**',
+ '**/__generated__/**',
+ '**/posts/**',
+ '**/test/**',
+ '**/public/**',
+ '**/.next/**',
+ '**/out/**',
+ '**/web-build/**',
+ '**/functions/**',
+ '**/graphql/**',
+ ],
+}
diff --git a/typescript/skeet-graphql/scripts/generateSitemap.ts b/typescript/skeet-graphql/scripts/generateSitemap.ts
new file mode 100644
index 000000000000..af662b1c3f4f
--- /dev/null
+++ b/typescript/skeet-graphql/scripts/generateSitemap.ts
@@ -0,0 +1,41 @@
+import fs from 'fs'
+import globby from 'globby'
+import siteConfig from '../src/config/site'
+
+const { domain } = siteConfig
+const distDir = 'web-build'
+
+async function generateSiteMap() {
+ const pages = await globby([
+ `${distDir}/en.html`,
+ `${distDir}/ja.html`,
+ `${distDir}/en/**/*.html`,
+ `${distDir}/ja/**/*.html`,
+ ])
+ console.log(pages)
+
+ const sitemap = `
+
+ ${pages
+ .filter((page) => !page.includes('404') && !page.includes('500'))
+ .map((page) => {
+ const path = page
+ .replace(distDir, '')
+ .replace('/index', '')
+ .replace('.html', '')
+ return `
+
+ ${`https://${domain}${path}/`}
+ weekly
+ 0.5
+
+ `
+ })
+ .join('')}
+
+ `
+
+ fs.writeFileSync('web-build/sitemap.xml', sitemap)
+}
+
+generateSiteMap()
diff --git a/typescript/skeet-graphql/scripts/sendSitemap.ts b/typescript/skeet-graphql/scripts/sendSitemap.ts
new file mode 100644
index 000000000000..67607201ece4
--- /dev/null
+++ b/typescript/skeet-graphql/scripts/sendSitemap.ts
@@ -0,0 +1,56 @@
+import dotenv from 'dotenv'
+dotenv.config()
+// import { readFileSync } from 'fs'
+// import { XMLParser } from 'fast-xml-parser'
+import fetch from 'node-fetch'
+import siteConfig from '../src/config/site'
+
+// const parser = new XMLParser()
+const { domain } = siteConfig
+
+const main = async () => {
+ try {
+ const googlePing = await fetch(
+ `https://www.google.com/ping?sitemap=https://${domain}/sitemap.xml`,
+ {
+ method: 'GET',
+ }
+ )
+ console.log(googlePing)
+ // bing
+ // const apiKey = process.env.BING_API_KEY
+ // if (!apiKey) {
+ // throw Error('No api key')
+ // }
+ // const sitemapXML = readFileSync('web-build/sitemap.xml', 'utf-8')
+ // console.log(sitemapXML)
+ // const sitemapJSON = parser.parse(sitemapXML)
+ // //@ts-ignore
+ // const urlList = sitemapJSON.urlset.url.map((item) => item.loc)
+ // const [siteUrl] = urlList
+ // const body = { siteUrl, urlList }
+ // const response = await fetch(
+ // `https://ssl.bing.com/webmaster/api.svc/pox/SubmitUrlBatch?apikey=${apiKey}`,
+ // {
+ // method: 'POST',
+ // body: JSON.stringify(body),
+ // headers: { 'Content-Type': 'application/json' },
+ // }
+ // )
+ // console.log(response)
+ } catch (error) {
+ if (error instanceof Error) {
+ if (error.message.includes('No api key')) {
+ console.error('Please set Bing API key in .env')
+ } else if (error.message.includes('ENOENT: no such file or directory')) {
+ console.error(
+ 'There is no sitemap.xml. please run `yarn build` to make sitemap on your local '
+ )
+ } else {
+ console.error(error)
+ }
+ }
+ }
+}
+
+main()
diff --git a/typescript/skeet-graphql/skeet-cloud.config.json b/typescript/skeet-graphql/skeet-cloud.config.json
new file mode 100644
index 000000000000..743c5431975f
--- /dev/null
+++ b/typescript/skeet-graphql/skeet-cloud.config.json
@@ -0,0 +1,51 @@
+{
+ "app": {
+ "name": "skeet-graphql",
+ "projectId": "skeet-graphql",
+ "fbProjectId": "skeet-graphql",
+ "template": "Next.js(React) - GraphQL",
+ "region": "asia-northeast1",
+ "appDomain": "skeet.dev",
+ "nsDomain": "skeet.dev",
+ "lbDomain": "sql.skeet.dev",
+ "functionsDomain": "sql.skeet.dev",
+ "hasLoadBalancer": true
+ },
+ "cloudRun": {
+ "name": "skeet-skeet-graphql-api",
+ "url": "https://sql.skeet.dev",
+ "cpu": 1,
+ "maxConcurrency": 80,
+ "maxInstances": 100,
+ "minInstances": 0,
+ "memory": "4Gi"
+ },
+ "db": {
+ "databaseVersion": "POSTGRES_15",
+ "cpu": 1,
+ "memory": "3840MiB",
+ "storageSize": 10,
+ "whiteList": ""
+ },
+ "taskQueues": [
+ {
+ "queueName": "createUser",
+ "location": "asia-northeast1",
+ "maxAttempts": 3,
+ "maxConcurrent": 1,
+ "maxRate": 1,
+ "maxInterval": "10s",
+ "minInterval": "1s"
+ },
+ {
+ "queueName": "createChatRoomMessage",
+ "location": "asia-northeast1",
+ "maxAttempts": 3,
+ "maxConcurrent": 1,
+ "maxRate": 1,
+ "maxInterval": "10s",
+ "minInterval": "1s"
+ }
+ ],
+ "cloudArmor": []
+}
diff --git a/typescript/skeet-graphql/src/__generated__/AuthLayoutQuery.graphql.ts b/typescript/skeet-graphql/src/__generated__/AuthLayoutQuery.graphql.ts
new file mode 100644
index 000000000000..f6ef2177e688
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/AuthLayoutQuery.graphql.ts
@@ -0,0 +1,90 @@
+/**
+ * @generated SignedSource<>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+export type AuthLayoutQuery$variables = {};
+export type AuthLayoutQuery$data = {
+ readonly me: {
+ readonly iconUrl: string | null;
+ readonly id: string | null;
+ readonly username: string | null;
+ } | null;
+};
+export type AuthLayoutQuery = {
+ response: AuthLayoutQuery$data;
+ variables: AuthLayoutQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "me",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "iconUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "username",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+];
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "AuthLayoutQuery",
+ "selections": (v0/*: any*/),
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "AuthLayoutQuery",
+ "selections": (v0/*: any*/)
+ },
+ "params": {
+ "cacheID": "382859540be0772e1c621aed0cf30231",
+ "id": null,
+ "metadata": {},
+ "name": "AuthLayoutQuery",
+ "operationKind": "query",
+ "text": "query AuthLayoutQuery {\n me {\n id\n iconUrl\n username\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "8d78b8056488a95b0a4b5f1af8876fe1";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/ChatBoxQuery.graphql.ts b/typescript/skeet-graphql/src/__generated__/ChatBoxQuery.graphql.ts
new file mode 100644
index 000000000000..e9693ebf0cb0
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/ChatBoxQuery.graphql.ts
@@ -0,0 +1,258 @@
+/**
+ * @generated SignedSource<<7c55be7e9a31b348661fac96664af338>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+export type ChatBoxQuery$variables = {
+ chatRoomId?: string | null;
+ first?: number | null;
+};
+export type ChatBoxQuery$data = {
+ readonly chatRoomMessageConnection: {
+ readonly edges: ReadonlyArray<{
+ readonly node: {
+ readonly content: string;
+ readonly createdAt: any;
+ readonly id: string | null;
+ readonly role: string;
+ readonly updatedAt: any;
+ } | null;
+ } | null> | null;
+ readonly nodes: ReadonlyArray<{
+ readonly id: string | null;
+ } | null> | null;
+ readonly pageInfo: {
+ readonly hasNextPage: boolean;
+ };
+ } | null;
+ readonly getChatRoom: {
+ readonly createdAt: any;
+ readonly id: string | null;
+ readonly maxTokens: number;
+ readonly model: string;
+ readonly temperature: number;
+ readonly title: string | null;
+ readonly updatedAt: any;
+ } | null;
+};
+export type ChatBoxQuery = {
+ response: ChatBoxQuery$data;
+ variables: ChatBoxQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "chatRoomId"
+},
+v1 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "first"
+},
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+},
+v3 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+},
+v4 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "updatedAt",
+ "storageKey": null
+},
+v5 = [
+ {
+ "alias": null,
+ "args": [
+ {
+ "kind": "Variable",
+ "name": "id",
+ "variableName": "chatRoomId"
+ }
+ ],
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "getChatRoom",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "maxTokens",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "title",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "model",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "temperature",
+ "storageKey": null
+ },
+ (v3/*: any*/),
+ (v4/*: any*/)
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": [
+ {
+ "kind": "Variable",
+ "name": "chatRoomId",
+ "variableName": "chatRoomId"
+ },
+ {
+ "kind": "Variable",
+ "name": "first",
+ "variableName": "first"
+ }
+ ],
+ "concreteType": "QueryChatRoomMessageConnection_Connection",
+ "kind": "LinkedField",
+ "name": "chatRoomMessageConnection",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoomMessageEdge",
+ "kind": "LinkedField",
+ "name": "edges",
+ "plural": true,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoomMessage",
+ "kind": "LinkedField",
+ "name": "node",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "role",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "content",
+ "storageKey": null
+ },
+ (v3/*: any*/),
+ (v4/*: any*/)
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "PageInfo",
+ "kind": "LinkedField",
+ "name": "pageInfo",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hasNextPage",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoomMessage",
+ "kind": "LinkedField",
+ "name": "nodes",
+ "plural": true,
+ "selections": [
+ (v2/*: any*/)
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+];
+return {
+ "fragment": {
+ "argumentDefinitions": [
+ (v0/*: any*/),
+ (v1/*: any*/)
+ ],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "ChatBoxQuery",
+ "selections": (v5/*: any*/),
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [
+ (v1/*: any*/),
+ (v0/*: any*/)
+ ],
+ "kind": "Operation",
+ "name": "ChatBoxQuery",
+ "selections": (v5/*: any*/)
+ },
+ "params": {
+ "cacheID": "abd3e564323c1f012c0aeb5de8f9d3b0",
+ "id": null,
+ "metadata": {},
+ "name": "ChatBoxQuery",
+ "operationKind": "query",
+ "text": "query ChatBoxQuery(\n $first: Int\n $chatRoomId: String\n) {\n getChatRoom(id: $chatRoomId) {\n id\n maxTokens\n title\n model\n temperature\n createdAt\n updatedAt\n }\n chatRoomMessageConnection(first: $first, chatRoomId: $chatRoomId) {\n edges {\n node {\n id\n role\n content\n createdAt\n updatedAt\n }\n }\n pageInfo {\n hasNextPage\n }\n nodes {\n id\n }\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "cc153c33020056dc25a842e5d30e04a1";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/ChatMenuMutation.graphql.ts b/typescript/skeet-graphql/src/__generated__/ChatMenuMutation.graphql.ts
new file mode 100644
index 000000000000..711fe2e50b08
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/ChatMenuMutation.graphql.ts
@@ -0,0 +1,143 @@
+/**
+ * @generated SignedSource<<21fca908d2835069b73d5a77b0860349>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Mutation } from 'relay-runtime';
+export type ChatMenuMutation$variables = {
+ maxTokens?: number | null;
+ model?: string | null;
+ stream?: boolean | null;
+ systemContent?: string | null;
+ temperature?: number | null;
+};
+export type ChatMenuMutation$data = {
+ readonly createChatRoom: {
+ readonly id: string | null;
+ } | null;
+};
+export type ChatMenuMutation = {
+ response: ChatMenuMutation$data;
+ variables: ChatMenuMutation$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "maxTokens"
+},
+v1 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "model"
+},
+v2 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "stream"
+},
+v3 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "systemContent"
+},
+v4 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "temperature"
+},
+v5 = [
+ {
+ "alias": null,
+ "args": [
+ {
+ "kind": "Variable",
+ "name": "maxTokens",
+ "variableName": "maxTokens"
+ },
+ {
+ "kind": "Variable",
+ "name": "model",
+ "variableName": "model"
+ },
+ {
+ "kind": "Variable",
+ "name": "stream",
+ "variableName": "stream"
+ },
+ {
+ "kind": "Variable",
+ "name": "systemContent",
+ "variableName": "systemContent"
+ },
+ {
+ "kind": "Variable",
+ "name": "temperature",
+ "variableName": "temperature"
+ }
+ ],
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "createChatRoom",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+];
+return {
+ "fragment": {
+ "argumentDefinitions": [
+ (v0/*: any*/),
+ (v1/*: any*/),
+ (v2/*: any*/),
+ (v3/*: any*/),
+ (v4/*: any*/)
+ ],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "ChatMenuMutation",
+ "selections": (v5/*: any*/),
+ "type": "Mutation",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [
+ (v1/*: any*/),
+ (v0/*: any*/),
+ (v4/*: any*/),
+ (v2/*: any*/),
+ (v3/*: any*/)
+ ],
+ "kind": "Operation",
+ "name": "ChatMenuMutation",
+ "selections": (v5/*: any*/)
+ },
+ "params": {
+ "cacheID": "b1f3a3665aba9f44149de2f385396456",
+ "id": null,
+ "metadata": {},
+ "name": "ChatMenuMutation",
+ "operationKind": "mutation",
+ "text": "mutation ChatMenuMutation(\n $model: String\n $maxTokens: Int\n $temperature: Int\n $stream: Boolean\n $systemContent: String\n) {\n createChatRoom(model: $model, maxTokens: $maxTokens, temperature: $temperature, stream: $stream, systemContent: $systemContent) {\n id\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "5fa285d511b0a232be48cd78981181a9";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/ChatMenuPaginationQuery.graphql.ts b/typescript/skeet-graphql/src/__generated__/ChatMenuPaginationQuery.graphql.ts
new file mode 100644
index 000000000000..f560c75d0ff1
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/ChatMenuPaginationQuery.graphql.ts
@@ -0,0 +1,266 @@
+/**
+ * @generated SignedSource<<72f1370448f8d370abb02ba09416fadf>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+import { FragmentRefs } from "relay-runtime";
+export type ChatMenuPaginationQuery$variables = {
+ after?: string | null;
+ before?: string | null;
+ first?: number | null;
+ last?: number | null;
+};
+export type ChatMenuPaginationQuery$data = {
+ readonly " $fragmentSpreads": FragmentRefs<"ChatMenu_query">;
+};
+export type ChatMenuPaginationQuery = {
+ response: ChatMenuPaginationQuery$data;
+ variables: ChatMenuPaginationQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "after"
+ },
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "before"
+ },
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "first"
+ },
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "last"
+ }
+],
+v1 = [
+ {
+ "kind": "Variable",
+ "name": "after",
+ "variableName": "after"
+ },
+ {
+ "kind": "Variable",
+ "name": "before",
+ "variableName": "before"
+ },
+ {
+ "kind": "Variable",
+ "name": "first",
+ "variableName": "first"
+ },
+ {
+ "kind": "Variable",
+ "name": "last",
+ "variableName": "last"
+ }
+],
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "ChatMenuPaginationQuery",
+ "selections": [
+ {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "ChatMenu_query"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "ChatMenuPaginationQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "concreteType": "QueryChatRoomConnection_Connection",
+ "kind": "LinkedField",
+ "name": "chatRoomConnection",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoomEdge",
+ "kind": "LinkedField",
+ "name": "edges",
+ "plural": true,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "node",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "maxTokens",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "model",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "title",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "updatedAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "temperature",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "cursor",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "nodes",
+ "plural": true,
+ "selections": [
+ (v2/*: any*/)
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "PageInfo",
+ "kind": "LinkedField",
+ "name": "pageInfo",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "endCursor",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hasNextPage",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hasPreviousPage",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "startCursor",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "filters": null,
+ "handle": "connection",
+ "key": "ChatMenu_chatRoomConnection",
+ "kind": "LinkedHandle",
+ "name": "chatRoomConnection"
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "7851f76624f7a781fe4f811df27cbc3e",
+ "id": null,
+ "metadata": {},
+ "name": "ChatMenuPaginationQuery",
+ "operationKind": "query",
+ "text": "query ChatMenuPaginationQuery(\n $after: String\n $before: String\n $first: Int\n $last: Int\n) {\n ...ChatMenu_query\n}\n\nfragment ChatMenu_query on Query {\n chatRoomConnection(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n maxTokens\n model\n title\n createdAt\n updatedAt\n temperature\n __typename\n }\n cursor\n }\n nodes {\n id\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "6f7d0804d82b115cd7c9de0be2bf5058";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/ChatMenu_query.graphql.ts b/typescript/skeet-graphql/src/__generated__/ChatMenu_query.graphql.ts
new file mode 100644
index 000000000000..e5b9597f4e82
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/ChatMenu_query.graphql.ts
@@ -0,0 +1,250 @@
+/**
+ * @generated SignedSource<<7bb6f82ec9e80fff7daf2c1ccd1e81bd>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ReaderFragment, RefetchableFragment } from 'relay-runtime';
+import { FragmentRefs } from "relay-runtime";
+export type ChatMenu_query$data = {
+ readonly chatRoomConnection: {
+ readonly edges: ReadonlyArray<{
+ readonly node: {
+ readonly createdAt: any;
+ readonly id: string | null;
+ readonly maxTokens: number;
+ readonly model: string;
+ readonly temperature: number;
+ readonly title: string | null;
+ readonly updatedAt: any;
+ } | null;
+ } | null> | null;
+ readonly nodes: ReadonlyArray<{
+ readonly id: string | null;
+ } | null> | null;
+ readonly pageInfo: {
+ readonly endCursor: string | null;
+ readonly hasNextPage: boolean;
+ readonly hasPreviousPage: boolean;
+ readonly startCursor: string | null;
+ };
+ } | null;
+ readonly " $fragmentType": "ChatMenu_query";
+};
+export type ChatMenu_query$key = {
+ readonly " $data"?: ChatMenu_query$data;
+ readonly " $fragmentSpreads": FragmentRefs<"ChatMenu_query">;
+};
+
+const node: ReaderFragment = (function(){
+var v0 = [
+ "chatRoomConnection"
+],
+v1 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "argumentDefinitions": [
+ {
+ "kind": "RootArgument",
+ "name": "after"
+ },
+ {
+ "kind": "RootArgument",
+ "name": "before"
+ },
+ {
+ "kind": "RootArgument",
+ "name": "first"
+ },
+ {
+ "kind": "RootArgument",
+ "name": "last"
+ }
+ ],
+ "kind": "Fragment",
+ "metadata": {
+ "connection": [
+ {
+ "count": null,
+ "cursor": null,
+ "direction": "bidirectional",
+ "path": (v0/*: any*/)
+ }
+ ],
+ "refetch": {
+ "connection": {
+ "forward": {
+ "count": "first",
+ "cursor": "after"
+ },
+ "backward": {
+ "count": "last",
+ "cursor": "before"
+ },
+ "path": (v0/*: any*/)
+ },
+ "fragmentPathInResult": [],
+ "operation": require('./ChatMenuPaginationQuery.graphql')
+ }
+ },
+ "name": "ChatMenu_query",
+ "selections": [
+ {
+ "alias": "chatRoomConnection",
+ "args": null,
+ "concreteType": "QueryChatRoomConnection_Connection",
+ "kind": "LinkedField",
+ "name": "__ChatMenu_chatRoomConnection_connection",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoomEdge",
+ "kind": "LinkedField",
+ "name": "edges",
+ "plural": true,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "node",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "maxTokens",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "model",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "title",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "updatedAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "temperature",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "cursor",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "nodes",
+ "plural": true,
+ "selections": [
+ (v1/*: any*/)
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "PageInfo",
+ "kind": "LinkedField",
+ "name": "pageInfo",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "endCursor",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hasNextPage",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hasPreviousPage",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "startCursor",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+};
+})();
+
+(node as any).hash = "6f7d0804d82b115cd7c9de0be2bf5058";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/ChatScreenQuery.graphql.ts b/typescript/skeet-graphql/src/__generated__/ChatScreenQuery.graphql.ts
new file mode 100644
index 000000000000..9cd249e865c0
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/ChatScreenQuery.graphql.ts
@@ -0,0 +1,274 @@
+/**
+ * @generated SignedSource<<8080b79a29dd603d7ee90c01d482b539>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+import { FragmentRefs } from "relay-runtime";
+export type ChatScreenQuery$variables = {
+ after?: string | null;
+ before?: string | null;
+ first?: number | null;
+ last?: number | null;
+};
+export type ChatScreenQuery$data = {
+ readonly " $fragmentSpreads": FragmentRefs<"ChatMenu_query">;
+};
+export type ChatScreenQuery = {
+ response: ChatScreenQuery$data;
+ variables: ChatScreenQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "after"
+},
+v1 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "before"
+},
+v2 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "first"
+},
+v3 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "last"
+},
+v4 = [
+ {
+ "kind": "Variable",
+ "name": "after",
+ "variableName": "after"
+ },
+ {
+ "kind": "Variable",
+ "name": "before",
+ "variableName": "before"
+ },
+ {
+ "kind": "Variable",
+ "name": "first",
+ "variableName": "first"
+ },
+ {
+ "kind": "Variable",
+ "name": "last",
+ "variableName": "last"
+ }
+],
+v5 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [
+ (v0/*: any*/),
+ (v1/*: any*/),
+ (v2/*: any*/),
+ (v3/*: any*/)
+ ],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "ChatScreenQuery",
+ "selections": [
+ {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "ChatMenu_query"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [
+ (v2/*: any*/),
+ (v0/*: any*/),
+ (v3/*: any*/),
+ (v1/*: any*/)
+ ],
+ "kind": "Operation",
+ "name": "ChatScreenQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v4/*: any*/),
+ "concreteType": "QueryChatRoomConnection_Connection",
+ "kind": "LinkedField",
+ "name": "chatRoomConnection",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoomEdge",
+ "kind": "LinkedField",
+ "name": "edges",
+ "plural": true,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "node",
+ "plural": false,
+ "selections": [
+ (v5/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "maxTokens",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "model",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "title",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "updatedAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "temperature",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "cursor",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "ChatRoom",
+ "kind": "LinkedField",
+ "name": "nodes",
+ "plural": true,
+ "selections": [
+ (v5/*: any*/)
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "PageInfo",
+ "kind": "LinkedField",
+ "name": "pageInfo",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "endCursor",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hasNextPage",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hasPreviousPage",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "startCursor",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": (v4/*: any*/),
+ "filters": null,
+ "handle": "connection",
+ "key": "ChatMenu_chatRoomConnection",
+ "kind": "LinkedHandle",
+ "name": "chatRoomConnection"
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "67ccd97b7794c71a887114636b7bf208",
+ "id": null,
+ "metadata": {},
+ "name": "ChatScreenQuery",
+ "operationKind": "query",
+ "text": "query ChatScreenQuery(\n $first: Int\n $after: String\n $last: Int\n $before: String\n) {\n ...ChatMenu_query\n}\n\nfragment ChatMenu_query on Query {\n chatRoomConnection(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n maxTokens\n model\n title\n createdAt\n updatedAt\n temperature\n __typename\n }\n cursor\n }\n nodes {\n id\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "2836761d65817355708114c29052336a";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/EditUserIconUrlMutation.graphql.ts b/typescript/skeet-graphql/src/__generated__/EditUserIconUrlMutation.graphql.ts
new file mode 100644
index 000000000000..a959084b89e9
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/EditUserIconUrlMutation.graphql.ts
@@ -0,0 +1,125 @@
+/**
+ * @generated SignedSource<<48fdb0e9e010524374d4787d4151630b>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Mutation } from 'relay-runtime';
+export type EditUserIconUrlMutation$variables = {
+ iconUrl?: string | null;
+ id?: string | null;
+};
+export type EditUserIconUrlMutation$data = {
+ readonly updateUser: {
+ readonly iconUrl: string | null;
+ } | null;
+};
+export type EditUserIconUrlMutation = {
+ response: EditUserIconUrlMutation$data;
+ variables: EditUserIconUrlMutation$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "iconUrl"
+},
+v1 = {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "id"
+},
+v2 = [
+ {
+ "kind": "Variable",
+ "name": "iconUrl",
+ "variableName": "iconUrl"
+ },
+ {
+ "kind": "Variable",
+ "name": "id",
+ "variableName": "id"
+ }
+],
+v3 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "iconUrl",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [
+ (v0/*: any*/),
+ (v1/*: any*/)
+ ],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "EditUserIconUrlMutation",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v2/*: any*/),
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "updateUser",
+ "plural": false,
+ "selections": [
+ (v3/*: any*/)
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Mutation",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [
+ (v1/*: any*/),
+ (v0/*: any*/)
+ ],
+ "kind": "Operation",
+ "name": "EditUserIconUrlMutation",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v2/*: any*/),
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "updateUser",
+ "plural": false,
+ "selections": [
+ (v3/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "5b67f4816eb4231b4290f1c765d40979",
+ "id": null,
+ "metadata": {},
+ "name": "EditUserIconUrlMutation",
+ "operationKind": "mutation",
+ "text": "mutation EditUserIconUrlMutation(\n $id: String\n $iconUrl: String\n) {\n updateUser(id: $id, iconUrl: $iconUrl) {\n iconUrl\n id\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "03b84bcd94f4d2883aba2bbff9e24147";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/EditUserProfileMutation.graphql.ts b/typescript/skeet-graphql/src/__generated__/EditUserProfileMutation.graphql.ts
new file mode 100644
index 000000000000..3ce19732c69e
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/EditUserProfileMutation.graphql.ts
@@ -0,0 +1,121 @@
+/**
+ * @generated SignedSource<>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Mutation } from 'relay-runtime';
+export type EditUserProfileMutation$variables = {
+ id?: string | null;
+ username?: string | null;
+};
+export type EditUserProfileMutation$data = {
+ readonly updateUser: {
+ readonly username: string | null;
+ } | null;
+};
+export type EditUserProfileMutation = {
+ response: EditUserProfileMutation$data;
+ variables: EditUserProfileMutation$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "id"
+ },
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "username"
+ }
+],
+v1 = [
+ {
+ "kind": "Variable",
+ "name": "id",
+ "variableName": "id"
+ },
+ {
+ "kind": "Variable",
+ "name": "username",
+ "variableName": "username"
+ }
+],
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "username",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "EditUserProfileMutation",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "updateUser",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/)
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Mutation",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "EditUserProfileMutation",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "updateUser",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "2b5d1459629219ec2993f9abffd9dbe0",
+ "id": null,
+ "metadata": {},
+ "name": "EditUserProfileMutation",
+ "operationKind": "mutation",
+ "text": "mutation EditUserProfileMutation(\n $id: String\n $username: String\n) {\n updateUser(id: $id, username: $username) {\n username\n id\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "7d14efc535cb2e13cab692564f36c121";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/__generated__/README.md b/typescript/skeet-graphql/src/__generated__/README.md
new file mode 100644
index 000000000000..53967d666740
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/README.md
@@ -0,0 +1 @@
+Relay creates Type files in this folder.
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/__generated__/UserLayoutQuery.graphql.ts b/typescript/skeet-graphql/src/__generated__/UserLayoutQuery.graphql.ts
new file mode 100644
index 000000000000..4b6c181ce970
--- /dev/null
+++ b/typescript/skeet-graphql/src/__generated__/UserLayoutQuery.graphql.ts
@@ -0,0 +1,90 @@
+/**
+ * @generated SignedSource<<2fc14e63a558798ffea3c8a2d7ca5914>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+export type UserLayoutQuery$variables = {};
+export type UserLayoutQuery$data = {
+ readonly me: {
+ readonly iconUrl: string | null;
+ readonly id: string | null;
+ readonly username: string | null;
+ } | null;
+};
+export type UserLayoutQuery = {
+ response: UserLayoutQuery$data;
+ variables: UserLayoutQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "me",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "iconUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "username",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+];
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "UserLayoutQuery",
+ "selections": (v0/*: any*/),
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "UserLayoutQuery",
+ "selections": (v0/*: any*/)
+ },
+ "params": {
+ "cacheID": "e2df0e061279e0e3f4e002be1f43e55f",
+ "id": null,
+ "metadata": {},
+ "name": "UserLayoutQuery",
+ "operationKind": "query",
+ "text": "query UserLayoutQuery {\n me {\n id\n iconUrl\n username\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "3158b9fc47af5f8d0589ffe0cf786bf1";
+
+export default node;
diff --git a/typescript/skeet-graphql/src/assets/animation/loading/3-dots-bounce-white.svg b/typescript/skeet-graphql/src/assets/animation/loading/3-dots-bounce-white.svg
new file mode 100644
index 000000000000..4d507f3d0ce9
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/animation/loading/3-dots-bounce-white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/animation/loading/3-dots-bounce.svg b/typescript/skeet-graphql/src/assets/animation/loading/3-dots-bounce.svg
new file mode 100644
index 000000000000..713b485ff2de
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/animation/loading/3-dots-bounce.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoHorizontal.svg b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoHorizontal.svg
new file mode 100644
index 000000000000..441d476af984
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoHorizontal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoHorizontalInvert.svg b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoHorizontalInvert.svg
new file mode 100644
index 000000000000..07cc13fb1d49
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoHorizontalInvert.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoSquare.svg b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoSquare.svg
new file mode 100644
index 000000000000..f1d86645d9b0
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoSquare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoSquareInvert.svg b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoSquareInvert.svg
new file mode 100644
index 000000000000..3dcc51fbe859
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/SkeetLogoSquareInvert.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/Firebase.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/Firebase.svg
new file mode 100644
index 000000000000..b6620211abe1
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/Firebase.svg
@@ -0,0 +1,55 @@
+
+
+
+ logo_lockup_firebase_horizontal
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/GoogleCloud.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/GoogleCloud.svg
new file mode 100644
index 000000000000..4b57ebf59590
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/GoogleCloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/Nexus.png b/typescript/skeet-graphql/src/assets/img/logo/projects/Nexus.png
new file mode 100644
index 000000000000..9d78fa7d6e50
Binary files /dev/null and b/typescript/skeet-graphql/src/assets/img/logo/projects/Nexus.png differ
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/TypeScriptHorizontal.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/TypeScriptHorizontal.svg
new file mode 100644
index 000000000000..f3a050d25c49
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/TypeScriptHorizontal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/android.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/android.svg
new file mode 100644
index 000000000000..dc6874a061c1
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/android.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/apollo.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/apollo.svg
new file mode 100644
index 000000000000..cf89c15a3628
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/apollo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/eslint.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/eslint.svg
new file mode 100644
index 000000000000..aca9d5d051d3
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/eslint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/express.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/express.svg
new file mode 100644
index 000000000000..7655f8d16d9d
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/express.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/graphql.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/graphql.svg
new file mode 100644
index 000000000000..b6c240c190c0
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/graphql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/i18next.webp b/typescript/skeet-graphql/src/assets/img/logo/projects/i18next.webp
new file mode 100644
index 000000000000..af8d0ed45569
Binary files /dev/null and b/typescript/skeet-graphql/src/assets/img/logo/projects/i18next.webp differ
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/ios.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/ios.svg
new file mode 100644
index 000000000000..140e145dd137
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/ios.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/jest.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/jest.svg
new file mode 100644
index 000000000000..79b105c2daef
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/jest.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/nextjs.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/nextjs.svg
new file mode 100644
index 000000000000..967d758aebb8
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/nextjs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/postgre.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/postgre.svg
new file mode 100644
index 000000000000..bb89f2bcb075
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/postgre.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/prettier.png b/typescript/skeet-graphql/src/assets/img/logo/projects/prettier.png
new file mode 100644
index 000000000000..6614bcb81346
Binary files /dev/null and b/typescript/skeet-graphql/src/assets/img/logo/projects/prettier.png differ
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/prisma.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/prisma.svg
new file mode 100644
index 000000000000..389033e8baea
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/prisma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/react.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/react.svg
new file mode 100644
index 000000000000..3d5d391dcadb
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/recoil.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/recoil.svg
new file mode 100644
index 000000000000..dd1b81507eab
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/recoil.svg
@@ -0,0 +1,8 @@
+
+
+ Recoil
+
+
+
+
+
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/relay.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/relay.svg
new file mode 100644
index 000000000000..3cf9f48c24b5
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/relay.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/tailwindcss.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/tailwindcss.svg
new file mode 100644
index 000000000000..709a9d00fcfe
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/tailwindcss.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/img/logo/projects/typescript.svg b/typescript/skeet-graphql/src/assets/img/logo/projects/typescript.svg
new file mode 100644
index 000000000000..36cc14251d60
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/img/logo/projects/typescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/typescript/skeet-graphql/src/assets/styles/globals.css b/typescript/skeet-graphql/src/assets/styles/globals.css
new file mode 100644
index 000000000000..7e5d21bead44
--- /dev/null
+++ b/typescript/skeet-graphql/src/assets/styles/globals.css
@@ -0,0 +1,41 @@
+@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700;900&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;500;700;900&display=swap');
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ font-family: Outfit, 'Noto Sans JP', ui-sans-serif, system-ui, -apple-system,
+ BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+ 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+ 'Segoe UI Symbol', 'Noto Color Emoji';
+ scroll-padding-top: 104px;
+ box-sizing: border-box;
+ }
+}
+
+@layer utilities {
+ .content-height {
+ height: calc(var(--vh, 1vh) * 100 - 112px);
+ }
+ .content-height-mobile {
+ height: calc(var(--vh, 1vh) * 100 - 112px - 32px);
+ }
+ .chat-height-1 {
+ height: calc(var(--vh, 1vh) * 100 - 112px - 40px - 16px);
+ }
+ .chat-height-2 {
+ height: calc(var(--vh, 1vh) * 100 - 112px - 80px - 16px);
+ }
+ .chat-height-3 {
+ height: calc(var(--vh, 1vh) * 100 - 112px - 112px - 16px);
+ }
+ .chat-height-4 {
+ height: calc(var(--vh, 1vh) * 100 - 112px - 144px - 16px);
+ }
+ .chat-height-5 {
+ height: calc(var(--vh, 1vh) * 100 - 112px - 192px - 16px);
+ }
+}
diff --git a/typescript/skeet-graphql/src/components/articles/ScrollSyncToc.tsx b/typescript/skeet-graphql/src/components/articles/ScrollSyncToc.tsx
new file mode 100644
index 000000000000..b53885e3755b
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/ScrollSyncToc.tsx
@@ -0,0 +1,134 @@
+//@ts-nocheck
+import throttle from 'lodash.throttle'
+import { useState, useEffect, useCallback } from 'react'
+import { remark } from 'remark'
+import { visit } from 'unist-util-visit'
+import { toString as mdastToString } from 'mdast-util-to-string'
+import GithubSlugger from 'github-slugger'
+
+import Toc from './Toc'
+
+const githubSlugger = new GithubSlugger()
+const OFFSET_ACTIVE_ITEM = 128
+
+type Props = {
+ rawMarkdownBody: string
+}
+
+export default function ScrollSyncToc({ rawMarkdownBody }: Props) {
+ const [activeItemIds, setActiveItemIds] = useState([])
+ const [itemTopOffsets, setItemTopOffsets] = useState([])
+ const [toc, setToc] = useState([])
+
+ useEffect(() => {
+ setToc(_getToc(rawMarkdownBody))
+ }, [rawMarkdownBody])
+
+ useEffect(() => {
+ setItemTopOffsets(_getElementTopOffsetsById(toc))
+ }, [toc])
+
+ const handleScroll = useCallback(() => {
+ const item = itemTopOffsets.find((current, i) => {
+ const next = itemTopOffsets[i + 1]
+ const currentPosition = window.scrollY
+ const judgePosition = currentPosition + OFFSET_ACTIVE_ITEM
+ return next
+ ? judgePosition >= current.offsetTop && judgePosition < next.offsetTop
+ : judgePosition >= current.offsetTop
+ })
+
+ const activeItemIds = item
+ ? item.parents
+ ? [item.id, ...item.parents.map((i) => i.id)]
+ : [item.id]
+ : []
+
+ setActiveItemIds(activeItemIds)
+ }, [itemTopOffsets])
+
+ useEffect(() => {
+ const throttledHandleScroll = throttle(handleScroll, 100)
+ window.addEventListener('scroll', throttledHandleScroll)
+ return () => {
+ window.removeEventListener('scroll', throttledHandleScroll)
+ }
+ }, [handleScroll])
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+function _getToc(rawMarkdownBody: string) {
+ const headings = _extractToc(rawMarkdownBody)
+ return _attachParents(headings)
+}
+
+function _extractToc(rawMarkdownBody: string) {
+ githubSlugger.reset()
+ const result = []
+ const ast = remark().parse(rawMarkdownBody)
+ visit(ast, 'heading', (child) => {
+ const value = child.children[0].value
+ const id = githubSlugger.slug(value || mdastToString(child))
+ const depth = child.depth
+ result.push({
+ value,
+ id,
+ depth,
+ })
+ })
+ return result
+}
+
+const MIN_HEADER_DEPTH = 2
+function _attachParents(headings: string[]) {
+ headings.reverse()
+ const result = headings.map((h, i) => {
+ const lastIndex = headings.length - 1
+ if (i === lastIndex) {
+ return h
+ }
+
+ let currentDepth = h.depth
+
+ for (let targetIndex = i + 1; targetIndex <= lastIndex; targetIndex++) {
+ if (currentDepth === MIN_HEADER_DEPTH) {
+ break
+ }
+ const targetH = headings[targetIndex]
+ if (currentDepth > targetH.depth) {
+ if (h.parents) {
+ h.parents.push(targetH)
+ } else {
+ h.parents = [targetH]
+ }
+ currentDepth = targetH.depth
+ }
+ }
+
+ return h
+ })
+
+ return result.reverse()
+}
+
+const _getElementTopOffsetsById = (ids) => {
+ return ids
+ .map(({ _value, id, parents }) => {
+ const element = document.getElementById(id)
+ return element
+ ? {
+ id,
+ offsetTop: element.offsetTop,
+ parents,
+ }
+ : null
+ })
+ .filter((item) => item)
+}
diff --git a/typescript/skeet-graphql/src/components/articles/Toc.tsx b/typescript/skeet-graphql/src/components/articles/Toc.tsx
new file mode 100644
index 000000000000..ea47096b1c1e
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/Toc.tsx
@@ -0,0 +1,48 @@
+import clsx from 'clsx'
+import { useTranslation } from 'next-i18next'
+type Props = {
+ toc: {
+ id: string
+ depth: number
+ value: string
+ }[]
+ activeItemIds: string[]
+}
+
+export default function Toc({ toc, activeItemIds }: Props) {
+ const { t } = useTranslation()
+ return (
+ <>
+ {toc.length > 0 && (
+ <>
+
+
+ >
+ )}
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/doc/DocContents.tsx b/typescript/skeet-graphql/src/components/articles/doc/DocContents.tsx
new file mode 100644
index 000000000000..120b51fbb69e
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/doc/DocContents.tsx
@@ -0,0 +1,36 @@
+import Container from '@/components/common/atoms/Container'
+import ScrollSyncToc from '@/components/articles/ScrollSyncToc'
+import type { DocContent } from '@/types/article'
+import DocPagination from './DocPagination'
+
+type Props = {
+ article: DocContent
+ articleHtml: string
+}
+
+export default function DocContents({ article, articleHtml }: Props) {
+ return (
+ <>
+
+
+
+
{article.title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/doc/DocIndex.tsx b/typescript/skeet-graphql/src/components/articles/doc/DocIndex.tsx
new file mode 100644
index 000000000000..78c2e1982b2d
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/doc/DocIndex.tsx
@@ -0,0 +1,94 @@
+import { useTranslation } from 'next-i18next'
+import Link from '@/components/routing/Link'
+import type { DocIndex } from '@/types/article'
+import {
+ HeartIcon,
+ RocketLaunchIcon,
+ ArrowUpRightIcon,
+} from '@heroicons/react/24/outline'
+import clsx from 'clsx'
+
+const actions = [
+ {
+ title: 'doc:actions.motivation.title',
+ body: 'doc:actions.motivation.body',
+ href: '/doc/general/motivation',
+ icon: HeartIcon,
+ iconForeground: 'text-pink-700',
+ iconBackground: 'bg-pink-50',
+ },
+ {
+ title: 'doc:actions.quickstart.title',
+ body: 'doc:actions.quickstart.body',
+ href: '/doc/general/quickstart',
+ icon: RocketLaunchIcon,
+ iconForeground: 'text-green-700',
+ iconBackground: 'bg-green-50',
+ },
+]
+
+export default function DocIndex() {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+
+
+ {t('doc:title')}
+
+
+ {t('doc:body')}
+
+
+
+
+
+ {actions.map((action, actionIdx) => (
+
+
+
+
+
+
+ {t(action.title)}
+
+
+
+ {t(action.body)}
+
+
+
+
+
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/doc/DocPagination.tsx b/typescript/skeet-graphql/src/components/articles/doc/DocPagination.tsx
new file mode 100644
index 000000000000..44176a857888
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/doc/DocPagination.tsx
@@ -0,0 +1,88 @@
+import { useTranslation } from 'next-i18next'
+import Link from '@/components/routing/Link'
+import { useMemo } from 'react'
+import { docMenuNav } from '@/config/navs'
+import { useRouter } from 'next/router'
+import {
+ ArrowSmallLeftIcon,
+ ArrowSmallRightIcon,
+} from '@heroicons/react/24/outline'
+
+export default function DocPagination() {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const asPathWithoutLang = useMemo(() => {
+ return router.asPath.replace('/ja/', '/').replace('/en/', '/')
+ }, [router.asPath])
+ const pageInfo = useMemo(() => {
+ const pages = docMenuNav
+ .map((item) => {
+ if (item.href) {
+ return item
+ }
+ if (item.children) {
+ return item.children.map((i) => i)
+ }
+ })
+ .flat()
+ const currentPageNum = pages.findIndex(
+ (item) => asPathWithoutLang === item?.href
+ )
+
+ return {
+ previousPage: pages[currentPageNum - 1],
+ nextPage: pages[currentPageNum + 1],
+ }
+ }, [asPathWithoutLang])
+
+ return (
+ <>
+
+
+ {pageInfo.previousPage && (
+ <>
+
+
+
+
+
+
+ {t('doc:previousPage')}
+
+
+
+ {t(pageInfo.previousPage.name)}
+
+
+
+
+
+ >
+ )}
+
+
+ {pageInfo.nextPage && (
+ <>
+
+
+
+
+
+ {t('doc:nextPage')}
+
+
+
+
+ {t(pageInfo.nextPage.name)}
+
+
+
+
+
+ >
+ )}
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/legal/LegalContents.tsx b/typescript/skeet-graphql/src/components/articles/legal/LegalContents.tsx
new file mode 100644
index 000000000000..0afcb7129008
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/legal/LegalContents.tsx
@@ -0,0 +1,31 @@
+import ScrollSyncToc from '@/components/articles/ScrollSyncToc'
+import Container from '@/components/common/atoms/Container'
+import type { LegalContent } from '@/types/article'
+
+type Props = {
+ article: LegalContent
+ articleHtml: string
+}
+
+export default function LegalContents({ article, articleHtml }: Props) {
+ return (
+ <>
+
+
+
+
{article.title}
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/news/NewsContents.tsx b/typescript/skeet-graphql/src/components/articles/news/NewsContents.tsx
new file mode 100644
index 000000000000..e9fbf5db72e4
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/news/NewsContents.tsx
@@ -0,0 +1,48 @@
+import Container from '@/components/common/atoms/Container'
+import ScrollSyncToc from '@/components/articles/ScrollSyncToc'
+import Image from 'next/image'
+import type { NewsContent } from '@/types/article'
+
+type Props = {
+ article: NewsContent
+ articleHtml: string
+}
+
+export default function NewsContents({ article, articleHtml }: Props) {
+ return (
+ <>
+
+
+
+
{article.title}
+
+ {article.date}
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/news/NewsIndex.tsx b/typescript/skeet-graphql/src/components/articles/news/NewsIndex.tsx
new file mode 100644
index 000000000000..26be5bdfda86
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/news/NewsIndex.tsx
@@ -0,0 +1,64 @@
+import { useTranslation } from 'next-i18next'
+import Image from 'next/image'
+import Link from '@/components/routing/Link'
+import type { NewsIndex } from '@/types/article'
+import TopNewsRow from './TopNewsRow'
+
+type Props = {
+ articles: NewsIndex[]
+ urls: string[]
+}
+
+export default function NewsIndex({ articles, urls }: Props) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+
+
+ {articles.slice(4).map((article, index) => (
+
+
+
+
+
+
+
+
+ {article.date}
+
+
+ {article.category}
+
+
+
+
+
+ {article.title}
+
+
+
+
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/news/NewsPageIndex.tsx b/typescript/skeet-graphql/src/components/articles/news/NewsPageIndex.tsx
new file mode 100644
index 000000000000..bd99863995db
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/news/NewsPageIndex.tsx
@@ -0,0 +1,69 @@
+import { useTranslation } from 'next-i18next'
+import Image from 'next/image'
+import Link from '@/components/routing/Link'
+import type { NewsIndex } from '@/types/article'
+
+type Props = {
+ articles: NewsIndex[]
+ urls: string[]
+}
+
+export default function NewsPageIndex({ articles, urls }: Props) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+
+
+ News
+
+
+
+ {articles.map((article, index) => (
+
+
+
+
+
+
+
+
+ {article.date}
+
+
+ {article.category}
+
+
+
+
+
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/articles/news/TopNewsRow.tsx b/typescript/skeet-graphql/src/components/articles/news/TopNewsRow.tsx
new file mode 100644
index 000000000000..b152861b145e
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/articles/news/TopNewsRow.tsx
@@ -0,0 +1,115 @@
+import { useTranslation } from 'next-i18next'
+import Image from 'next/image'
+import Link from '@/components/routing/Link'
+import type { NewsIndex } from '@/types/article'
+
+type Props = {
+ articles: NewsIndex[]
+ urls: string[]
+}
+
+export default function TopNewsRow({ articles, urls }: Props) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+
+
+ News
+
+
+
+ {articles.slice(0, 1).map((article, index) => (
+
+
+
+
+
+
+
+
+ {article.date}
+
+
+ {article.category}
+
+
+
+
+
+ {`${article.content.slice(0, 160)} ...`}
+
+
+
+
+
+ ))}
+
+
+ {articles.slice(1).map((article, index) => (
+
+
+
+
+
+
+
+ {article.date}
+
+
+ {article.category}
+
+
+
+
+ {article.title}
+
+
+ {`${article.content.slice(0, 120)} ...`}
+
+
+
+
+
+ ))}
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/common/atoms/Button.tsx b/typescript/skeet-graphql/src/components/common/atoms/Button.tsx
new file mode 100644
index 000000000000..688e9b98aaeb
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/common/atoms/Button.tsx
@@ -0,0 +1,114 @@
+import type { ReactNode } from 'react'
+import Link from '@/components/routing/Link'
+import clsx from 'clsx'
+
+const baseStyles = {
+ solid:
+ 'group inline-flex items-center justify-center py-2 px-4 font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 hover:cursor-pointer',
+ outline:
+ 'group inline-flex ring-1 items-center justify-center py-2 px-4 focus:outline-none hover:cursor-pointer',
+}
+
+const variantStyles = {
+ solid: {
+ gray: 'bg-gray-900 text-white dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-300 hover:bg-gray-700 hover:text-gray-50 active:bg-gray-800 active:text-gray-50 focus-visible:outline-gray-900',
+ blue: 'bg-blue-600 text-white hover:text-blue-50 hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 focus-visible:outline-blue-600',
+ red: 'bg-red-600 text-white hover:text-red-50 hover:bg-red-500 active:bg-red-800 active:text-red-100 focus-visible:outline-red-600',
+ green:
+ 'bg-green-600 text-white hover:text-green-50 hover:bg-green-500 active:bg-green-800 active:text-green-100 focus-visible:outline-green-600',
+ yellow:
+ 'bg-yellow-600 text-white hover:text-yellow-50 hover:bg-yellow-500 active:bg-yellow-800 active:text-yellow-100 focus-visible:outline-yellow-600',
+ purple:
+ 'bg-purple-600 text-white hover:text-purple-50 hover:bg-purple-500 active:bg-purple-800 active:text-purple-100 focus-visible:outline-purple-600',
+ orange:
+ 'bg-orange-600 text-white hover:text-orange-50 hover:bg-orange-500 active:bg-orange-800 active:text-orange-100 focus-visible:outline-orange-600',
+ amber:
+ 'bg-amber-600 text-white hover:text-amber-50 hover:bg-amber-500 active:bg-amber-800 active:text-amber-100 focus-visible:outline-amber-600',
+ pink: 'bg-pink-600 text-white hover:text-pink-50 hover:bg-pink-500 active:bg-pink-800 active:text-pink-100 focus-visible:outline-pink-600',
+ indigo:
+ 'bg-indigo-600 text-white hover:text-indigo-50 hover:bg-indigo-500 active:bg-indigo-800 active:text-indigo-100 focus-visible:outline-indigo-600',
+
+ white:
+ 'bg-white text-gray-900 hover:bg-blue-50 active:bg-blue-200 active:text-blue-700 focus-visible:outline-white',
+ black:
+ 'bg-gray-900 text-white hover:bg-gray-500 active:bg-gray-700 active:text-bg-gray-100 focus-visible:outline-white',
+ },
+ outline: {
+ gray: 'ring-gray-200 text-gray-700 dark:text-gray-100 dark:ring-gray-100 dark:hover:text-gray-50 dark:hover:ring-gray-50 hover:text-gray-900 hover:ring-gray-400 active:bg-gray-100 dark:active:bg-gray-500 active:text-gray-700 focus-visible:outline-blue-600 focus-visible:ring-gray-300',
+ blue: 'ring-blue-200 text-blue-700 dark:text-blue-200 hover:text-blue-500 hover:ring-blue-400 dark:hover:ring-blue-100 dark:hover:text-blue-100 active:bg-blue-100 active:text-blue-700 dark:active:bg-blue-400 focus-visible:outline-blue-600 focus-visible:ring-blue-300',
+ red: 'ring-red-200 text-red-700 dark:text-red-200 hover:text-red-500 hover:ring-red-400 dark:hover:ring-red-100 dark:hover:text-red-100 active:bg-red-100 active:text-red-700 dark:active:bg-red-400 focus-visible:outline-red-600 focus-visible:ring-red-300',
+ green:
+ 'ring-green-200 text-green-700 dark:text-green-200 hover:text-green-500 hover:ring-green-400 dark:hover:ring-green-100 dark:hover:text-green-100 active:bg-green-100 active:text-green-700 dark:active:bg-green-400 focus-visible:outline-green-600 focus-visible:ring-green-300',
+ yellow:
+ 'ring-yellow-200 text-yellow-700 dark:text-yellow-200 hover:text-yellow-500 hover:ring-yellow-400 dark:hover:ring-yellow-100 dark:hover:text-yellow-100 active:bg-yellow-100 active:text-yellow-700 dark:active:bg-yellow-400 focus-visible:outline-yellow-600 focus-visible:ring-yellow-300',
+ purple:
+ 'ring-purple-200 text-purple-700 dark:text-purple-200 hover:text-purple-500 hover:ring-purple-400 dark:hover:ring-purple-100 dark:hover:text-purple-100 active:bg-purple-100 active:text-purple-700 dark:active:bg-purple-400 focus-visible:outline-purple-600 focus-visible:ring-purple-300',
+ orange:
+ 'ring-orange-200 text-orange-700 dark:text-orange-200 hover:text-orange-500 hover:ring-orange-400 dark:hover:ring-orange-100 dark:hover:text-orange-100 active:bg-orange-100 active:text-orange-700 dark:active:bg-orange-400 focus-visible:outline-orange-600 focus-visible:ring-orange-300',
+ amber:
+ 'ring-amber-200 text-amber-700 dark:text-amber-200 hover:text-amber-500 hover:ring-amber-400 dark:hover:ring-amber-100 dark:hover:text-amber-100 active:bg-amber-100 active:text-amber-700 dark:active:bg-amber-400 focus-visible:outline-amber-600 focus-visible:ring-amber-300',
+ pink: 'ring-pink-200 text-pink-700 dark:text-pink-200 hover:text-pink-500 hover:ring-pink-400 dark:hover:ring-pink-100 dark:hover:text-pink-100 active:bg-pink-100 active:text-pink-700 dark:active:bg-pink-400 focus-visible:outline-pink-600 focus-visible:ring-pink-300',
+ indigo:
+ 'ring-indigo-200 text-indigo-700 dark:text-indigo-200 hover:text-indigo-500 hover:ring-indigo-400 dark:hover:ring-indigo-100 dark:hover:text-indigo-100 active:bg-indigo-100 active:text-indigo-700 dark:active:bg-indigo-400 focus-visible:outline-indigo-600 focus-visible:ring-indigo-300',
+ white:
+ 'ring-gray-200 text-white hover:ring-gray-500 hover:text-gray-100 active:ring-gray-700 active:text-gray-400 focus-visible:outline-white',
+ black:
+ 'ring-gray-900 text-gray-900 hover:ring-gray-500 hover:text-gray-500 active:ring-gray-700 active:text-gray-700 focus-visible:outline-white',
+ },
+}
+
+type Props = {
+ children: ReactNode | string
+ variant?: 'solid' | 'outline'
+ color?:
+ | 'gray'
+ | 'blue'
+ | 'red'
+ | 'green'
+ | 'yellow'
+ | 'purple'
+ | 'orange'
+ | 'amber'
+ | 'pink'
+ | 'indigo'
+ | 'white'
+ | 'black'
+ className?: string
+ href?: string
+ target?: string
+ rel?: string
+ onClick?: () => void
+ type?: 'submit' | 'reset' | 'button'
+ disabled?: boolean
+}
+
+export default function Button({
+ variant = 'solid',
+ color = 'gray',
+ className,
+ href,
+ children,
+ ...props
+}: Props) {
+ className = clsx(
+ baseStyles[variant],
+ variantStyles[variant][color],
+ className
+ )
+
+ return href ? (
+ href.includes('https://') ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ )
+ ) : (
+
+ {children}
+
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/common/atoms/Container.tsx b/typescript/skeet-graphql/src/components/common/atoms/Container.tsx
new file mode 100644
index 000000000000..335279dea603
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/common/atoms/Container.tsx
@@ -0,0 +1,20 @@
+import clsx from 'clsx'
+import type { ReactNode } from 'react'
+
+type Props = {
+ className?: string
+ children: ReactNode
+}
+
+export default function Container({ className, children, ...props }: Props) {
+ return (
+ <>
+
+ {children}
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/common/atoms/LogoHorizontal.tsx b/typescript/skeet-graphql/src/components/common/atoms/LogoHorizontal.tsx
new file mode 100644
index 000000000000..aa2d9b18c07a
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/common/atoms/LogoHorizontal.tsx
@@ -0,0 +1,31 @@
+import Image from 'next/image'
+import logoHorizontal from '@/assets/img/logo/SkeetLogoHorizontal.svg'
+import logoHorizontalInvert from '@/assets/img/logo/SkeetLogoHorizontalInvert.svg'
+import clsx from 'clsx'
+
+type Props = {
+ className?: string
+ onClick?: () => void
+}
+
+export default function LogoHorizontal({ className, ...rest }: Props) {
+ return (
+ <>
+
+ Skeet
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/common/atoms/LogoHorizontalLink.tsx b/typescript/skeet-graphql/src/components/common/atoms/LogoHorizontalLink.tsx
new file mode 100644
index 000000000000..12469d861186
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/common/atoms/LogoHorizontalLink.tsx
@@ -0,0 +1,37 @@
+import Link from '@/components/routing/Link'
+import Image from 'next/image'
+import logoHorizontal from '@/assets/img/logo/SkeetLogoHorizontal.svg'
+import logoHorizontalInvert from '@/assets/img/logo/SkeetLogoHorizontalInvert.svg'
+import clsx from 'clsx'
+
+type Props = {
+ className?: string
+ href?: string
+ onClick?: () => void
+}
+
+export default function LogoHorizontalLink({
+ className,
+ href = '/',
+ ...rest
+}: Props) {
+ return (
+ <>
+
+ Skeet
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/error/InvalidParamsError.tsx b/typescript/skeet-graphql/src/components/error/InvalidParamsError.tsx
new file mode 100644
index 000000000000..00044108d648
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/error/InvalidParamsError.tsx
@@ -0,0 +1,26 @@
+import Button from '@/components/common/atoms/Button'
+import { useTranslation } from 'next-i18next'
+
+export default function InvalidParamsError() {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+
+
+ {t('auth:invalidParamsErrorTitle')}
+
+
+ {t('auth:invalidParamsErrorBody')}
+
+
+ {t('auth:goToLogin')}
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/error/UserScreenErrorBoundary.tsx b/typescript/skeet-graphql/src/components/error/UserScreenErrorBoundary.tsx
new file mode 100644
index 000000000000..1462ab90377e
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/error/UserScreenErrorBoundary.tsx
@@ -0,0 +1,33 @@
+import React from 'react'
+
+type State = {
+ error?: Error | null
+}
+
+type Props = {
+ children: React.ReactNode
+ showRetry: React.ReactNode
+}
+
+export default class UserScreenErrorBoundary extends React.Component<
+ Props,
+ State
+> {
+ constructor(props: Props) {
+ super(props)
+ this.state = { error: null }
+ }
+
+ static getDerivedStateFromError(error: Error): State {
+ return { error: error }
+ }
+
+ componentDidCatch(error: Error) {}
+
+ render() {
+ if (this.state.error) {
+ return this.props.showRetry
+ }
+ return this.props.children
+ }
+}
diff --git a/typescript/skeet-graphql/src/components/loading/AppLoading.tsx b/typescript/skeet-graphql/src/components/loading/AppLoading.tsx
new file mode 100644
index 000000000000..5561a57bacdd
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/loading/AppLoading.tsx
@@ -0,0 +1,24 @@
+import Image from 'next/image'
+import DotsBounce from '@/assets/animation/loading/3-dots-bounce.svg'
+import DotsBounceWhite from '@/assets/animation/loading/3-dots-bounce-white.svg'
+
+export default function AppLoading() {
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/loading/ChatMenuLoading.tsx b/typescript/skeet-graphql/src/components/loading/ChatMenuLoading.tsx
new file mode 100644
index 000000000000..4672d04d524f
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/loading/ChatMenuLoading.tsx
@@ -0,0 +1,13 @@
+export default function ChatMenuLoading() {
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/loading/UserScreenLoading.tsx b/typescript/skeet-graphql/src/components/loading/UserScreenLoading.tsx
new file mode 100644
index 000000000000..9540cce99319
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/loading/UserScreenLoading.tsx
@@ -0,0 +1,9 @@
+export default function UserScreenLoading() {
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/action/ResetPasswordAction.tsx b/typescript/skeet-graphql/src/components/pages/action/ResetPasswordAction.tsx
new file mode 100644
index 000000000000..03a846c4fbe1
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/action/ResetPasswordAction.tsx
@@ -0,0 +1,164 @@
+import { useCallback, useEffect, useState, useMemo } from 'react'
+import { useTranslation } from 'next-i18next'
+import { auth } from '@/lib/firebase'
+import { confirmPasswordReset, verifyPasswordResetCode } from 'firebase/auth'
+import { passwordSchema } from '@/utils/form'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import clsx from 'clsx'
+import AppLoading from '@/components/loading/AppLoading'
+import { z } from 'zod'
+import { useForm, Controller } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import useToastMessage from '@/hooks/useToastMessage'
+import { useRouter } from 'next/router'
+
+const schema = z.object({
+ password: passwordSchema,
+})
+
+type Inputs = z.infer
+
+type Props = {
+ oobCode: string
+}
+
+export default function ResetPasswordAction({ oobCode }: Props) {
+ const [isLoading, setLoading] = useState(true)
+ const [isRegisterLoading, setRegisterLoading] = useState(false)
+ const { t } = useTranslation()
+ const [email, setEmail] = useState('')
+ const addToast = useToastMessage()
+ const router = useRouter()
+
+ const verifyEmail = useCallback(async () => {
+ try {
+ if (!auth) throw new Error('auth not initialized')
+ const gotEmail = await verifyPasswordResetCode(auth, oobCode)
+ setEmail(gotEmail)
+ setLoading(false)
+ } catch (err) {
+ console.error(err)
+ addToast({
+ type: 'error',
+ title: t('auth:verifyErrorTitle'),
+ description: t('auth:verifyErrorBody'),
+ })
+ router.push('/auth/login')
+ }
+ }, [router, t, oobCode, addToast])
+
+ useEffect(() => {
+ verifyEmail()
+ }, [verifyEmail])
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ control,
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ password: '',
+ },
+ })
+
+ const onSubmit = useCallback(
+ async (data: Inputs) => {
+ try {
+ setRegisterLoading(true)
+ if (!auth) throw new Error('auth not initialized')
+ await confirmPasswordReset(auth, oobCode, data.password)
+ addToast({
+ type: 'success',
+ title: t('auth:resetPasswordSuccessTitle'),
+ description: t('auth:resetPasswordSuccessBody'),
+ })
+ router.push('/auth/login')
+ } catch (err) {
+ console.error(err)
+ addToast({
+ type: 'error',
+ title: t('auth:resetPasswordErrorTitle'),
+ description: t('auth:resetPasswordErrorBody'),
+ })
+ } finally {
+ setRegisterLoading(false)
+ }
+ },
+ [oobCode, t, router, addToast]
+ )
+
+ const isDisabled = useMemo(
+ () => isLoading || isRegisterLoading || errors.password != null,
+ [isLoading, isRegisterLoading, errors.password]
+ )
+
+ if (isLoading) {
+ return (
+ <>
+
+ >
+ )
+ }
+
+ return (
+ <>
+
+
+
+
+ {t('inputNewPassword')}
+
+
+ {email}
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/action/VerifyEmailAction.tsx b/typescript/skeet-graphql/src/components/pages/action/VerifyEmailAction.tsx
new file mode 100644
index 000000000000..752bba79187d
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/action/VerifyEmailAction.tsx
@@ -0,0 +1,74 @@
+import { useCallback, useEffect, useState } from 'react'
+
+import { useTranslation } from 'next-i18next'
+import { auth } from '@/lib/firebase'
+import { applyActionCode } from 'firebase/auth'
+import AppLoading from '@/components/loading/AppLoading'
+import { useRouter } from 'next/router'
+import { CheckCircleIcon } from '@heroicons/react/24/outline'
+import useToastMessage from '@/hooks/useToastMessage'
+import Button from '@/components/common/atoms/Button'
+
+type Props = {
+ oobCode: string
+}
+
+export default function VerifyEmailAction({ oobCode }: Props) {
+ const [isLoading, setLoading] = useState(true)
+ const { t } = useTranslation()
+ const router = useRouter()
+ const addToast = useToastMessage()
+
+ const verifyUser = useCallback(async () => {
+ if (auth) {
+ try {
+ await applyActionCode(auth, oobCode)
+ addToast({
+ type: 'success',
+ title: t('auth:verifySuccessTitle'),
+ description: t('auth:verifySuccessBody'),
+ })
+ setLoading(false)
+ } catch (err) {
+ console.error(err)
+ addToast({
+ type: 'error',
+ title: t('auth:verifyErrorTitle'),
+ description: t('auth:verifyErrorBody'),
+ })
+ router.push('/auth/login')
+ }
+ }
+ }, [router, t, oobCode, addToast, setLoading])
+
+ useEffect(() => {
+ verifyUser()
+ }, [verifyUser])
+
+ if (isLoading) {
+ return (
+ <>
+
+ >
+ )
+ }
+
+ return (
+ <>
+
+
+
+
+ {t('auth:confirmDoneTitle')}
+
+
+ {t('auth:confirmDoneBody')}
+
+
+ {t('auth:backToLogin')}
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/auth/CheckEmailScreen.tsx b/typescript/skeet-graphql/src/components/pages/auth/CheckEmailScreen.tsx
new file mode 100644
index 000000000000..669377607332
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/auth/CheckEmailScreen.tsx
@@ -0,0 +1,27 @@
+import { useTranslation } from 'next-i18next'
+import Link from '@/components/routing/Link'
+import { EnvelopeOpenIcon } from '@heroicons/react/24/outline'
+
+export default function CheckEmailScreen() {
+ const { t } = useTranslation()
+ return (
+ <>
+
+
+
+
+ {t('auth:confirmEmail')}
+
+
+ {t('auth:thanksForRequest')}
+
+
+
+ {t('auth:goToLogin')}
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/auth/LoginScreen.tsx b/typescript/skeet-graphql/src/components/pages/auth/LoginScreen.tsx
new file mode 100644
index 000000000000..d6d59aa82285
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/auth/LoginScreen.tsx
@@ -0,0 +1,194 @@
+import { useTranslation } from 'next-i18next'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import { useCallback, useState, useMemo } from 'react'
+import useToastMessage from '@/hooks/useToastMessage'
+import {
+ signInWithEmailAndPassword,
+ sendEmailVerification,
+ signOut,
+} from 'firebase/auth'
+import { emailSchema, passwordSchema } from '@/utils/form'
+import { auth } from '@/lib/firebase'
+import clsx from 'clsx'
+import Link from '@/components/routing/Link'
+import { z } from 'zod'
+import { useForm, Controller } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+
+const schema = z.object({
+ email: emailSchema,
+ password: passwordSchema,
+})
+
+type Inputs = z.infer
+
+export default function LoginScreen() {
+ const { t } = useTranslation()
+ const addToast = useToastMessage()
+ const [isLoading, setLoading] = useState(false)
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ control,
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ })
+
+ const onSubmit = useCallback(
+ async (data: Inputs) => {
+ if (auth) {
+ try {
+ setLoading(true)
+ const userCredential = await signInWithEmailAndPassword(
+ auth,
+ data.email,
+ data.password
+ )
+
+ if (!userCredential.user.emailVerified) {
+ await sendEmailVerification(userCredential.user)
+ await signOut(auth)
+ throw new Error('Not verified')
+ }
+ } catch (err) {
+ console.error(err)
+ if (err instanceof Error && err.message === 'Not verified') {
+ addToast({
+ type: 'error',
+ title: t('auth:errorNotVerifiedTitle'),
+ description: t('auth:errorNotVerifiedBody'),
+ })
+ } else if (
+ err instanceof Error &&
+ err.message.includes('auth/user-not-found')
+ ) {
+ addToast({
+ type: 'error',
+ title: t('auth:userNotFoundTitle'),
+ description: t('auth:userNotFoundBody'),
+ })
+ } else {
+ addToast({
+ type: 'error',
+ title: t('errorLoginTitle'),
+ description: t('errorLoginBody'),
+ })
+ }
+ if (auth?.currentUser) {
+ signOut(auth)
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+ },
+ [t, addToast]
+ )
+
+ const isDisabled = useMemo(
+ () => isLoading || errors.email != null || errors.password != null,
+ [isLoading, errors.email, errors.password]
+ )
+
+ return (
+ <>
+
+
+
+
+ {t('auth:loginToYourAccount')}
+
+
+
+ {t('auth:or')}{' '}
+
+ {t('auth:registerYourAccount')}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/auth/RegisterScreen.tsx b/typescript/skeet-graphql/src/components/pages/auth/RegisterScreen.tsx
new file mode 100644
index 000000000000..b7cd5f3202ac
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/auth/RegisterScreen.tsx
@@ -0,0 +1,226 @@
+import { useTranslation } from 'next-i18next'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import clsx from 'clsx'
+import { useCallback, useMemo, useState } from 'react'
+import {
+ createUserWithEmailAndPassword,
+ sendEmailVerification,
+ signOut,
+} from 'firebase/auth'
+import { auth } from '@/lib/firebase'
+import useToastMessage from '@/hooks/useToastMessage'
+import { useRouter } from 'next/router'
+import { emailSchema, passwordSchema, privacySchema } from '@/utils/form'
+import { z } from 'zod'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm, Controller } from 'react-hook-form'
+import Link from '@/components/routing/Link'
+
+const schema = z.object({
+ email: emailSchema,
+ password: passwordSchema,
+ privacy: privacySchema,
+})
+
+type Inputs = z.infer
+
+export default function RegisterScreen() {
+ const { t, i18n } = useTranslation()
+ const isJapanese = useMemo(() => i18n.language === 'ja', [i18n])
+ const [isLoading, setLoading] = useState(false)
+ const addToast = useToastMessage()
+ const router = useRouter()
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ control,
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ email: '',
+ password: '',
+ privacy: false,
+ },
+ })
+
+ const onSubmit = useCallback(
+ async (data: Inputs) => {
+ if (auth && data.privacy) {
+ try {
+ setLoading(true)
+ auth.languageCode = isJapanese ? 'ja' : 'en'
+ const userCredential = await createUserWithEmailAndPassword(
+ auth,
+ data.email,
+ data.password
+ )
+ await sendEmailVerification(userCredential.user)
+ await signOut(auth)
+
+ addToast({
+ type: 'success',
+ title: t('auth:sentConfirmEmailTitle'),
+ description: t('auth:sentConfirmEmailBody'),
+ })
+
+ router.push('/auth/check-email')
+ } catch (err) {
+ console.error(err)
+
+ if (
+ err instanceof Error &&
+ err.message.includes('Firebase: Error (auth/email-already-in-use).')
+ ) {
+ addToast({
+ type: 'error',
+ title: t('auth:alreadyExistTitle'),
+ description: t('auth:alreadyExistBody'),
+ })
+ } else {
+ addToast({
+ type: 'error',
+ title: t('errorLoginTitle'),
+ description: t('errorLoginBody'),
+ })
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+ },
+ [t, isJapanese, addToast, router]
+ )
+
+ const isDisabled = useMemo(
+ () =>
+ isLoading ||
+ errors.email != null ||
+ errors.password != null ||
+ errors.privacy != null,
+ [isLoading, errors.email, errors.password, errors.privacy]
+ )
+
+ return (
+ <>
+
+
+
+
+
+ {t('auth:registerYourAccount')}
+
+
+
+
+ {t('auth:or')}{' '}
+
+ {t('auth:loginToYourAccount')}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/auth/ResetPasswordScreen.tsx b/typescript/skeet-graphql/src/components/pages/auth/ResetPasswordScreen.tsx
new file mode 100644
index 000000000000..e0766228932d
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/auth/ResetPasswordScreen.tsx
@@ -0,0 +1,153 @@
+import { useTranslation } from 'next-i18next'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import { useCallback, useState, useMemo } from 'react'
+import { emailSchema } from '@/utils/form'
+import clsx from 'clsx'
+import { auth } from '@/lib/firebase'
+import { sendPasswordResetEmail } from 'firebase/auth'
+import { z } from 'zod'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm, Controller } from 'react-hook-form'
+import Link from '@/components/routing/Link'
+import useToastMessage from '@/hooks/useToastMessage'
+import { useRouter } from 'next/router'
+
+const schema = z.object({
+ email: emailSchema,
+})
+
+type Inputs = z.infer
+
+export default function ResetPasswordScreen() {
+ const { t } = useTranslation()
+ const [isLoading, setLoading] = useState(false)
+ const addToast = useToastMessage()
+ const router = useRouter()
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ control,
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ email: '',
+ },
+ })
+
+ const onSubmit = useCallback(
+ async (data: Inputs) => {
+ if (auth) {
+ try {
+ setLoading(true)
+ await sendPasswordResetEmail(auth, data.email)
+ addToast({
+ type: 'success',
+ title: t('auth:sentResetPasswordRequest'),
+ description: t('auth:confirmEmail'),
+ })
+ router.push('/auth/check-email')
+ } catch (err) {
+ console.error(err)
+ if (err instanceof Error && err.message === 'Not verified') {
+ addToast({
+ type: 'error',
+ title: t('auth:errorNotVerifiedTitle'),
+ description: t('auth:errorNotVerifiedBody'),
+ })
+ } else if (
+ err instanceof Error &&
+ err.message.includes('auth/user-not-found')
+ ) {
+ addToast({
+ type: 'error',
+ title: t('auth:userNotFoundTitle'),
+ description: t('auth:userNotFoundBody'),
+ })
+ } else {
+ addToast({
+ type: 'error',
+ title: t('errorLoginTitle'),
+ description: t('errorLoginBody'),
+ })
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+ },
+ [t, addToast, router]
+ )
+
+ const isDisabled = useMemo(
+ () => isLoading || errors.email != null,
+ [isLoading, errors.email]
+ )
+
+ return (
+ <>
+
+
+
+
+ {t('auth:resetYourPassword')}
+
+
+
+
+ {t('auth:or')}{' '}
+
+ {t('auth:registerYourAccount')}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/common/DiscordRow.tsx b/typescript/skeet-graphql/src/components/pages/common/DiscordRow.tsx
new file mode 100644
index 000000000000..e94f0d64b127
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/common/DiscordRow.tsx
@@ -0,0 +1,46 @@
+import Button from '@/components/common/atoms/Button'
+import Container from '@/components/common/atoms/Container'
+import siteConfig from '@/config/site'
+import { useTranslation } from 'next-i18next'
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faDiscord } from '@fortawesome/free-brands-svg-icons'
+
+export default function DiscordRow() {
+ const { t } = useTranslation()
+ return (
+ <>
+
+
+
+
+
+ {t('DiscordRow.title')}
+
+
+
+
{t('DiscordRow.body')}
+
+
+
+ {t('DiscordRow.button')}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/home/HeroRow.tsx b/typescript/skeet-graphql/src/components/pages/home/HeroRow.tsx
new file mode 100644
index 000000000000..e438a34c342b
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/home/HeroRow.tsx
@@ -0,0 +1,124 @@
+import Container from '@/components/common/atoms/Container'
+import { useTranslation } from 'next-i18next'
+import Image from 'next/image'
+import nextjsLogo from '@/assets/img/logo/projects/nextjs.svg'
+import i18nextLogo from '@/assets/img/logo/projects/i18next.webp'
+import recoilLogo from '@/assets/img/logo/projects/recoil.svg'
+import graphqlLogo from '@/assets/img/logo/projects/graphql.svg'
+import relayLogo from '@/assets/img/logo/projects/relay.svg'
+import firebaseLogo from '@/assets/img/logo/projects/Firebase.svg'
+import tailwindcssLogo from '@/assets/img/logo/projects/tailwindcss.svg'
+import typescriptLogo from '@/assets/img/logo/projects/TypeScriptHorizontal.svg'
+import Button from '@/components/common/atoms/Button'
+import clsx from 'clsx'
+
+export default function HomeHeroRow() {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+ WebApp Boilerplate
+
+
+ {t('home:HeroRow.body')}
+
+
+
+ {t('aiChat')}
+
+
+ GitHub
+
+
+
+
+ {[
+ [
+ {
+ name: 'Next.js',
+ logo: nextjsLogo,
+ link: 'https://nextjs.org/',
+ },
+ {
+ name: 'Firebase',
+ logo: firebaseLogo,
+ link: 'https://firebase.google.com/',
+ },
+ {
+ name: 'TypeScript',
+ logo: typescriptLogo,
+ link: 'https://www.typescriptlang.org/',
+ },
+ {
+ name: 'Tailwind',
+ logo: tailwindcssLogo,
+ link: 'https://tailwindcss.com/',
+ },
+ ],
+ [
+ {
+ name: 'GraphQL',
+ logo: graphqlLogo,
+ link: 'https://graphql.org/',
+ },
+ {
+ name: 'Relay',
+ logo: relayLogo,
+ link: 'https://relay.dev/',
+ },
+ {
+ name: 'Recoil',
+ logo: recoilLogo,
+ link: 'https://recoiljs.org/',
+ },
+ {
+ name: 'i18next',
+ logo: i18nextLogo,
+ link: 'https://www.i18next.com/',
+ },
+ ],
+ ].map((group, groupIndex) => (
+
+
+ {group.map((project) => (
+
+
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/user/chat/ChatBox.tsx b/typescript/skeet-graphql/src/components/pages/user/chat/ChatBox.tsx
new file mode 100644
index 000000000000..c746b3eb24f4
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/user/chat/ChatBox.tsx
@@ -0,0 +1,458 @@
+import clsx from 'clsx'
+import { useTranslation } from 'next-i18next'
+import { PaperAirplaneIcon } from '@heroicons/react/24/outline'
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ KeyboardEvent,
+} from 'react'
+import { useRecoilValue } from 'recoil'
+import { userState } from '@/store/user'
+
+import { GPTModel, chatContentSchema } from '@/utils/form'
+import { fetchSkeetFunctions } from '@/lib/skeet'
+import Image from 'next/image'
+import { ChatRoom } from './ChatMenu'
+import { CreateStreamChatMessageParams } from '@/types/http/openai/createStreamChatMessageParams'
+import { z } from 'zod'
+import { useForm, Controller } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { TextDecoder } from 'text-encoding'
+import useToastMessage from '@/hooks/useToastMessage'
+import { unified } from 'unified'
+import remarkParse from 'remark-parse'
+import remark2Rehype from 'remark-rehype'
+import rehypeHighlight from 'rehype-highlight'
+import rehypeStringify from 'rehype-stringify'
+import rehypeCodeTitles from 'rehype-code-titles'
+import remarkSlug from 'remark-slug'
+import remarkGfm from 'remark-gfm'
+import remarkDirective from 'remark-directive'
+import remarkExternalLinks from 'remark-external-links'
+import { ChatScreenQuery$variables } from '@/__generated__/ChatScreenQuery.graphql'
+import { PreloadedQuery, graphql, usePreloadedQuery } from 'react-relay'
+import {
+ ChatBoxQuery,
+ ChatBoxQuery$variables,
+} from '@/__generated__/ChatBoxQuery.graphql'
+import { sleep } from '@/utils/time'
+
+type ChatMessage = {
+ id: string
+ role: string
+ createdAt: string
+ updatedAt: string
+ content: string
+}
+
+export const chatBoxQuery = graphql`
+ query ChatBoxQuery($first: Int, $chatRoomId: String) {
+ getChatRoom(id: $chatRoomId) {
+ id
+ maxTokens
+ title
+ model
+ temperature
+ createdAt
+ updatedAt
+ }
+ chatRoomMessageConnection(first: $first, chatRoomId: $chatRoomId) {
+ edges {
+ node {
+ id
+ role
+ content
+ createdAt
+ updatedAt
+ }
+ }
+ pageInfo {
+ hasNextPage
+ }
+ nodes {
+ id
+ }
+ }
+ }
+`
+
+const schema = z.object({
+ chatContent: chatContentSchema,
+})
+
+type Inputs = z.infer
+
+type Props = {
+ currentChatRoomId: string
+ refetch: (variables: ChatScreenQuery$variables) => void
+ chatBoxQueryReference: PreloadedQuery>
+ chatBoxRefetch: (variables: ChatBoxQuery$variables) => void
+}
+
+export default function ChatBox({
+ currentChatRoomId,
+ refetch,
+ chatBoxQueryReference,
+ chatBoxRefetch,
+}: Props) {
+ const { t } = useTranslation()
+ const user = useRecoilValue(userState)
+ const [chatMessages, setChatMessages] = useState([])
+ const [chatRoom, setChatRoom] = useState(null)
+ const addToast = useToastMessage()
+
+ const chatContentRef = useRef(null)
+ const scrollToEnd = useCallback(() => {
+ if (currentChatRoomId && chatContentRef.current) {
+ chatContentRef.current.scrollTop = chatContentRef.current.scrollHeight
+ }
+ }, [chatContentRef, currentChatRoomId])
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ control,
+ reset,
+ watch,
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ chatContent: '',
+ },
+ })
+
+ const chatContent = watch('chatContent')
+ const chatContentLines = useMemo(() => {
+ return (chatContent.match(/\n/g) || []).length + 1
+ }, [chatContent])
+
+ const data = usePreloadedQuery(chatBoxQuery, chatBoxQueryReference)
+
+ const getChatRoom = useCallback(() => {
+ const chatRoomData = data.getChatRoom
+ if (chatRoomData) {
+ setChatRoom({
+ id: chatRoomData.id ?? currentChatRoomId,
+ maxTokens: chatRoomData.maxTokens,
+ title: chatRoomData.title ?? '',
+ model: chatRoomData.model as GPTModel,
+ temperature: chatRoomData.temperature,
+ createdAt: chatRoomData.createdAt,
+ updatedAt: chatRoomData.updatedAt,
+ })
+ } else {
+ console.log('No such document!')
+ }
+ }, [currentChatRoomId, data.getChatRoom])
+
+ useEffect(() => {
+ getChatRoom()
+ }, [getChatRoom])
+
+ const [isSending, setSending] = useState(false)
+
+ const getUserChatRoomMessage = useCallback(async () => {
+ const items = data?.chatRoomMessageConnection?.edges
+ if (items) {
+ const messages: ChatMessage[] = []
+ for await (const item of items) {
+ const html = await unified()
+ .use(remarkParse)
+ .use(remarkDirective)
+ .use(remarkGfm)
+ .use(remarkSlug)
+ .use(remarkExternalLinks, {
+ target: '_blank',
+ rel: ['noopener noreferrer'],
+ })
+ .use(remark2Rehype)
+ .use(rehypeCodeTitles)
+ .use(rehypeHighlight)
+ .use(rehypeStringify)
+ .process(item?.node?.content as string)
+
+ messages.push({
+ id: item?.node?.id,
+ ...item?.node,
+ content: html.value,
+ } as ChatMessage)
+ }
+
+ setChatMessages(messages)
+ }
+ }, [data])
+
+ useEffect(() => {
+ getUserChatRoomMessage()
+ }, [getUserChatRoomMessage])
+
+ useEffect(() => {
+ if (chatMessages.length > 0) {
+ scrollToEnd()
+ }
+ }, [chatMessages, scrollToEnd])
+
+ const isDisabled = useMemo(() => {
+ return isSending || errors.chatContent != null
+ }, [isSending, errors.chatContent])
+
+ const onSubmit = useCallback(
+ async (inputs: Inputs) => {
+ try {
+ setSending(true)
+ if (!isDisabled && user.uid && currentChatRoomId) {
+ setChatMessages((prev) => {
+ return [
+ ...prev,
+ {
+ id: `UserSendingMessage${new Date().toISOString()}`,
+ role: 'user',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ content: inputs.chatContent,
+ },
+ {
+ id: `AssistantAnsweringMessage${new Date().toISOString()}`,
+ role: 'assistant',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ content: '',
+ },
+ ]
+ })
+ const res = await fetchSkeetFunctions(
+ 'openai',
+ 'createStreamChatMessage',
+ {
+ chatRoomId: currentChatRoomId,
+ content: inputs.chatContent,
+ }
+ )
+ const reader = await res?.body?.getReader()
+ const decoder = new TextDecoder('utf-8')
+
+ while (true && reader) {
+ const { value, done } = await reader.read()
+ if (done) break
+ try {
+ const dataString = decoder.decode(value)
+ if (dataString != 'Stream done') {
+ const data = JSON.parse(dataString)
+ setChatMessages((prev) => {
+ const chunkSize = data.text.length
+ if (prev[prev.length - 1].content.length === 0) {
+ prev[prev.length - 1].content =
+ prev[prev.length - 1].content + data.text
+ }
+ if (
+ !prev[prev.length - 1].content
+ .slice(chunkSize * -1)
+ .includes(data.text)
+ ) {
+ prev[prev.length - 1].content =
+ prev[prev.length - 1].content + data.text
+ }
+
+ return [...prev]
+ })
+ }
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ if (chatRoom && chatRoom.title == '') {
+ await sleep(200)
+ refetch({ first: 15 })
+ }
+ chatBoxRefetch({ first: 100, chatRoomId: currentChatRoomId })
+ reset()
+ }
+ } catch (err) {
+ console.error(err)
+ if (
+ err instanceof Error &&
+ (err.message.includes('Firebase ID token has expired.') ||
+ err.message.includes('Error: getUserAuth'))
+ ) {
+ addToast({
+ type: 'error',
+ title: t('errorTokenExpiredTitle'),
+ description: t('errorTokenExpiredBody'),
+ })
+ } else {
+ addToast({
+ type: 'error',
+ title: t('errorTitle'),
+ description: t('errorBody'),
+ })
+ }
+ } finally {
+ setSending(false)
+ }
+ },
+ [
+ isDisabled,
+ t,
+ currentChatRoomId,
+ user.uid,
+ chatRoom,
+ addToast,
+ reset,
+ refetch,
+ chatBoxRefetch,
+ ]
+ )
+
+ const onKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
+ handleSubmit(onSubmit)()
+ }
+ },
+ [handleSubmit, onSubmit]
+ )
+
+ return (
+ <>
+
+
+
4
+ ? 'chat-height-5'
+ : chatContentLines == 4
+ ? 'chat-height-4'
+ : chatContentLines == 3
+ ? 'chat-height-3'
+ : chatContentLines == 2
+ ? 'chat-height-2'
+ : 'chat-height-1',
+ 'w-full overflow-y-auto pb-24'
+ )}
+ >
+ {chatMessages.map((chatMessage) => (
+
+
+ {chatMessage.role === 'user' && (
+
+ )}
+ {(chatMessage.role === 'assistant' ||
+ chatMessage.role === 'system') &&
+ chatRoom?.model === 'gpt-3.5-turbo' && (
+
+ )}
+ {(chatMessage.role === 'assistant' ||
+ chatMessage.role === 'system') &&
+ chatRoom?.model === 'gpt-4' && (
+
+ )}
+
+ {chatMessage.role === 'system' && (
+
+
+ {chatRoom?.title ? chatRoom?.title : t('noTitle')}
+
+
+ {chatRoom?.model}: {chatRoom?.maxTokens} {t('tokens')}
+
+
+ )}
+
+
+
+
+ ))}
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/user/chat/ChatMenu.tsx b/typescript/skeet-graphql/src/components/pages/user/chat/ChatMenu.tsx
new file mode 100644
index 000000000000..dc1e676400ea
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/user/chat/ChatMenu.tsx
@@ -0,0 +1,671 @@
+import { useTranslation } from 'next-i18next'
+import clsx from 'clsx'
+import {
+ ChatBubbleLeftIcon,
+ PlusCircleIcon,
+ QueueListIcon,
+ XMarkIcon,
+} from '@heroicons/react/24/outline'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import {
+ Fragment,
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+ KeyboardEvent,
+} from 'react'
+
+import {
+ GPTModel,
+ allowedGPTModel,
+ gptModelSchema,
+ temperatureSchema,
+ maxTokensSchema,
+ systemContentSchema,
+} from '@/utils/form'
+
+import { format } from 'date-fns'
+import useToastMessage from '@/hooks/useToastMessage'
+import { Dialog, Transition } from '@headlessui/react'
+import { z } from 'zod'
+import { useForm, Controller } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { graphql, useMutation, usePaginationFragment } from 'react-relay'
+import {
+ ChatMenuMutation,
+ ChatMenuMutation$data,
+} from '@/__generated__/ChatMenuMutation.graphql'
+import { ChatMenuPaginationQuery } from '@/__generated__/ChatMenuPaginationQuery.graphql'
+import { ChatMenu_query$key } from '@/__generated__/ChatMenu_query.graphql'
+
+export type ChatRoom = {
+ id: string
+ createdAt: string
+ updatedAt: string
+ model: GPTModel
+ maxTokens: number
+ temperature: number
+ title: string
+}
+
+const chatMenuPaginationQuery = graphql`
+ fragment ChatMenu_query on Query
+ @refetchable(queryName: "ChatMenuPaginationQuery") {
+ chatRoomConnection(
+ first: $first
+ after: $after
+ last: $last
+ before: $before
+ ) @connection(key: "ChatMenu_chatRoomConnection") {
+ edges {
+ node {
+ id
+ maxTokens
+ model
+ title
+ createdAt
+ updatedAt
+ temperature
+ }
+ }
+ nodes {
+ id
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ }
+ }
+ }
+`
+
+const chatMenuMutation = graphql`
+ mutation ChatMenuMutation(
+ $model: String
+ $maxTokens: Int
+ $temperature: Int
+ $stream: Boolean
+ $systemContent: String
+ ) {
+ createChatRoom(
+ model: $model
+ maxTokens: $maxTokens
+ temperature: $temperature
+ stream: $stream
+ systemContent: $systemContent
+ ) {
+ id
+ }
+ }
+`
+
+const schema = z.object({
+ model: gptModelSchema,
+ maxTokens: maxTokensSchema,
+ temperature: temperatureSchema,
+ systemContent: systemContentSchema,
+})
+
+type Inputs = z.infer
+
+type Props = {
+ isNewChatModalOpen: boolean
+ setNewChatModalOpen: (_value: boolean) => void
+ currentChatRoomId: string | null
+ setCurrentChatRoomId: (_value: string | null) => void
+ chatRoomsData: ChatMenu_query$key
+}
+
+export default function ChatMenu({
+ isNewChatModalOpen,
+ setNewChatModalOpen,
+ currentChatRoomId,
+ setCurrentChatRoomId,
+ chatRoomsData,
+}: Props) {
+ const { t, i18n } = useTranslation()
+ const isJapanese = useMemo(() => i18n.language === 'ja', [i18n])
+
+ const [isCreateLoading, setCreateLoading] = useState(false)
+ const [isChatListModalOpen, setChatListModalOpen] = useState(false)
+ const addToast = useToastMessage()
+ const [commit] = useMutation(chatMenuMutation)
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ control,
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ model: 'gpt-3.5-turbo',
+ maxTokens: 1000,
+ temperature: 1,
+ systemContent: isJapanese
+ ? 'あなたは、親切で、創造的で、賢く、とてもフレンドリーなアシスタントです。'
+ : 'You are the assistant who is helpful, creative, clever, and very friendly.',
+ },
+ })
+
+ const { data, loadNext, hasNext, isLoadingNext, refetch } =
+ usePaginationFragment(
+ chatMenuPaginationQuery,
+ chatRoomsData
+ )
+
+ const chatMenuRef = useRef(null)
+ const chatMenuRefMobile = useRef(null)
+
+ const handleScroll = useCallback(() => {
+ const current = chatMenuRef.current
+ if (current) {
+ const isBottom =
+ current.scrollHeight - current.scrollTop === current.clientHeight
+ if (isBottom && hasNext && !isLoadingNext) {
+ loadNext(15)
+ }
+ }
+ }, [chatMenuRef, loadNext, hasNext, isLoadingNext])
+
+ const handleScrollMobile = useCallback(() => {
+ const current = chatMenuRefMobile.current
+
+ if (current) {
+ const isBottom =
+ Math.floor(current.scrollHeight - current.scrollTop) ===
+ current.clientHeight
+ if (isBottom && hasNext && !isLoadingNext) {
+ loadNext(15)
+ }
+ }
+ }, [chatMenuRefMobile, loadNext, hasNext, isLoadingNext])
+
+ const isDisabled = useMemo(() => {
+ return (
+ isCreateLoading ||
+ errors.model != null ||
+ errors.systemContent != null ||
+ errors.maxTokens != null ||
+ errors.temperature != null
+ )
+ }, [
+ isCreateLoading,
+ errors.model,
+ errors.systemContent,
+ errors.maxTokens,
+ errors.temperature,
+ ])
+
+ const onSubmit = useCallback(
+ async (data: Inputs) => {
+ try {
+ setCreateLoading(true)
+ if (!isDisabled) {
+ commit({
+ variables: {
+ model: data.model,
+ systemContent: data.systemContent,
+ maxTokens: data.maxTokens,
+ temperature: data.temperature,
+ stream: true,
+ },
+ onCompleted: (result: ChatMenuMutation$data) => {
+ addToast({
+ type: 'success',
+ title: t('chat:chatRoomCreatedSuccessTitle'),
+ description: t('chat:chatRoomCreatedSuccessBody'),
+ })
+ setCurrentChatRoomId(result?.createChatRoom?.id ?? null)
+ setNewChatModalOpen(false)
+ setCreateLoading(false)
+ refetch({ first: 15 })
+ },
+ onError: (err) => {
+ console.error(err.message)
+ addToast({
+ type: 'error',
+ title: t('errorTitle'),
+ description: t('errorBody'),
+ })
+ setNewChatModalOpen(false)
+ setCreateLoading(false)
+ },
+ updater: (store) => {
+ store.invalidateStore()
+ },
+ })
+ }
+ } catch (err) {
+ console.error(err)
+ if (
+ err instanceof Error &&
+ (err.message.includes('Firebase ID token has expired.') ||
+ err.message.includes('Error: getUserAuth'))
+ ) {
+ addToast({
+ type: 'error',
+ title: t('errorTokenExpiredTitle'),
+ description: t('errorTokenExpiredBody'),
+ })
+ } else {
+ addToast({
+ type: 'error',
+ title: t('errorTitle'),
+ description: t('errorBody'),
+ })
+ }
+ setNewChatModalOpen(false)
+ setCreateLoading(false)
+ }
+ },
+ [
+ setNewChatModalOpen,
+ t,
+ setCreateLoading,
+ isDisabled,
+ setCurrentChatRoomId,
+ addToast,
+ commit,
+ refetch,
+ ]
+ )
+
+ const onKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
+ handleSubmit(onSubmit)()
+ }
+ },
+ [handleSubmit, onSubmit]
+ )
+
+ return (
+ <>
+
+
+
+
{
+ setChatListModalOpen(true)
+ }}
+ className={clsx('flex flex-row items-center justify-center')}
+ >
+
+
+
+
{t('chat:title')}
+
+
{
+ setNewChatModalOpen(true)
+ }}
+ className={clsx('flex flex-row items-center justify-center')}
+ >
+
+
+
+
+
+
+
{
+ setNewChatModalOpen(true)
+ }}
+ className={clsx(
+ 'flex w-full flex-row items-center justify-center bg-gray-900 px-3 py-2 dark:bg-gray-600'
+ )}
+ >
+
+
+ {t('chat:newChat')}
+
+
+
+ {data.chatRoomConnection?.edges?.map((chat) => (
+
{
+ setCurrentChatRoomId(chat?.node?.id ?? null)
+ }}
+ key={`ChatMenu Desktop ${chat?.node?.id}`}
+ className={clsx(
+ currentChatRoomId === chat?.node?.id &&
+ 'border-2 border-gray-900 dark:border-gray-50',
+ 'flex flex-row items-start justify-start gap-2 bg-gray-50 p-2 hover:cursor-pointer dark:bg-gray-800'
+ )}
+ >
+
+
+ {chat?.node?.title !== '' && chat?.node?.title != null ? (
+
+ {(chat?.node?.title?.length ?? 0) > 20
+ ? `${chat?.node?.title?.slice(0, 20)} ...`
+ : chat?.node?.title}
+
+ ) : (
+
+ {t('noTitle')}
+
+ )}
+
+ {format(
+ new Date(chat?.node?.createdAt),
+ 'yyyy-MM-dd HH:mm'
+ )}
+
+
+
+ ))}
+
+
+
+
+
+ setNewChatModalOpen(false)}
+ >
+
+
+
+ {/* This element is to trick the browser into centering the modal contents. */}
+
+
+
+
+
+
+
+
+
+
{
+ setNewChatModalOpen(false)
+ }}
+ className="h-5 w-5 text-gray-900 hover:cursor-pointer hover:text-gray-700 dark:text-gray-50 dark:hover:text-gray-200"
+ >
+
+
+
+
+
+ {t('chat:newChat')}
+
+
+
+
+
+
+
+ {t('chat:model')}
+ {errors.model && (
+
+ {' : '}
+ {t('chat:modelErrorText')}
+
+ )}
+
+
+ (
+
+ {allowedGPTModel.map((model) => (
+
+ {model}
+
+ ))}
+
+ )}
+ />
+
+
+
+
+ {t('chat:maxTokens')}
+ {errors.maxTokens && (
+
+ {' : '}
+ {t('chat:maxTokensErrorText')}
+
+ )}
+
+
+ (
+
+ field.onChange(
+ e.target.value
+ ? parseFloat(e.target.value)
+ : 0
+ )
+ }
+ />
+ )}
+ />
+
+
+
+
+ {t('chat:temperature')}
+ {errors.temperature && (
+
+ {' : '}
+ {t('chat:temperatureErrorText')}
+
+ )}
+
+
+ (
+
+ field.onChange(
+ e.target.value
+ ? parseFloat(e.target.value)
+ : 0
+ )
+ }
+ />
+ )}
+ />
+
+
+
+
+
+ {t('chat:systemContent')}
+ {errors.systemContent && (
+
+ {' : '}
+ {t('chat:systemContentErrorText')}
+
+ )}
+
+
+ (
+
+ )}
+ />
+
+
+
+
+
+ {t('chat:createChatRoom')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setChatListModalOpen(false)}
+ >
+
+
+ {/* This element is to trick the browser into centering the modal contents. */}
+
+
+
+
+
+
+
+
+
+
{
+ setChatListModalOpen(false)
+ }}
+ className="h-5 w-5 hover:cursor-pointer"
+ >
+
+
+
+
+
+ {t('chat:chatList')}
+
+
+
+ {data.chatRoomConnection?.edges?.map((chat) => (
+
{
+ setCurrentChatRoomId(chat?.node?.id ?? null)
+ setChatListModalOpen(false)
+ }}
+ key={`ChatMenu Mobile ${chat?.node?.id}`}
+ className={clsx(
+ currentChatRoomId === chat?.node?.id &&
+ 'border-2 border-gray-900 dark:border-gray-50',
+ 'flex flex-row items-start justify-start gap-2 bg-gray-50 p-2 hover:cursor-pointer dark:bg-gray-800'
+ )}
+ >
+
+
+ {chat?.node?.title !== '' &&
+ chat?.node?.title != null ? (
+
+ {(chat?.node?.title?.length ?? 0) > 20
+ ? `${chat?.node?.title?.slice(0, 20)} ...`
+ : chat?.node?.title}
+
+ ) : (
+
+ {t('noTitle')}
+
+ )}
+
+
+ {format(
+ new Date(chat?.node?.createdAt),
+ 'yyyy-MM-dd HH:mm'
+ )}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/user/chat/ChatScreen.tsx b/typescript/skeet-graphql/src/components/pages/user/chat/ChatScreen.tsx
new file mode 100644
index 000000000000..feda04107a65
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/user/chat/ChatScreen.tsx
@@ -0,0 +1,121 @@
+import ChatMenu, { ChatRoom } from '@/components/pages/user/chat/ChatMenu'
+import ChatBox, { chatBoxQuery } from '@/components/pages/user/chat/ChatBox'
+import { Suspense, useCallback, useEffect, useState } from 'react'
+import {
+ PreloadedQuery,
+ graphql,
+ usePreloadedQuery,
+ useQueryLoader,
+} from 'react-relay'
+import {
+ ChatScreenQuery,
+ ChatScreenQuery$variables,
+} from '@/__generated__/ChatScreenQuery.graphql'
+import UserScreenLoading from '@/components/loading/UserScreenLoading'
+import UserScreenErrorBoundary from '@/components/error/UserScreenErrorBoundary'
+import RefetchChat from './RefetchChat'
+import {
+ ChatBoxQuery,
+ ChatBoxQuery$variables,
+} from '@/__generated__/ChatBoxQuery.graphql'
+import clsx from 'clsx'
+import { PlusCircleIcon } from '@heroicons/react/24/outline'
+import { useTranslation } from 'next-i18next'
+
+export const chatScreenQuery = graphql`
+ query ChatScreenQuery(
+ $first: Int
+ $after: String
+ $last: Int
+ $before: String
+ ) {
+ ...ChatMenu_query
+ }
+`
+
+type Props = {
+ queryReference: PreloadedQuery>
+ refetch: (variables: ChatScreenQuery$variables) => void
+}
+
+export default function ChatScreen({ queryReference, refetch }: Props) {
+ const { t } = useTranslation()
+ const [isNewChatModalOpen, setNewChatModalOpen] = useState(false)
+ const [currentChatRoomId, setCurrentChatRoomId] = useState(
+ null
+ )
+ const data = usePreloadedQuery(chatScreenQuery, queryReference)
+
+ const [chatBoxQueryReference, loadQuery] =
+ useQueryLoader(chatBoxQuery)
+
+ const chatBoxRefetch = useCallback(
+ (variables: ChatBoxQuery$variables) => {
+ loadQuery(variables, { fetchPolicy: 'network-only' })
+ },
+ [loadQuery]
+ )
+
+ useEffect(() => {
+ if (currentChatRoomId != null) {
+ loadQuery({ first: 100, chatRoomId: currentChatRoomId })
+ }
+ }, [currentChatRoomId, loadQuery])
+
+ return (
+ <>
+
+
+ {!currentChatRoomId && (
+
+
+
+ {t('chat:chatGPTCustom')}
+
+
{
+ setNewChatModalOpen(true)
+ }}
+ className={clsx(
+ 'flex w-full flex-row items-center justify-center gap-4 bg-gray-900 px-3 py-2 hover:cursor-pointer hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-400'
+ )}
+ >
+
+
+ {t('chat:newChat')}
+
+
+
+
+ )}
+ {currentChatRoomId &&
+ (chatBoxQueryReference == null ? (
+ <>
+
+ >
+ ) : (
+ <>
+
}>
+
}
+ >
+
+
+
+ >
+ ))}
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/user/chat/RefetchChat.tsx b/typescript/skeet-graphql/src/components/pages/user/chat/RefetchChat.tsx
new file mode 100644
index 000000000000..82c79dc5779f
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/user/chat/RefetchChat.tsx
@@ -0,0 +1,36 @@
+import { ChatScreenQuery$variables } from '@/__generated__/ChatScreenQuery.graphql'
+import { ArrowPathIcon } from '@heroicons/react/24/outline'
+import clsx from 'clsx'
+import { useTranslation } from 'next-i18next'
+
+type Props = {
+ refetch: (variables: ChatScreenQuery$variables) => void
+}
+
+export default function RefetchChat({ refetch }: Props) {
+ const { t } = useTranslation()
+ return (
+ <>
+
+
+
+ {t('chat:pleaseRefetch')}
+
+
{
+ refetch({ first: 15 })
+ }}
+ className={clsx(
+ 'flex w-full flex-row items-center justify-center gap-4 bg-gray-900 px-3 py-2 hover:cursor-pointer hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-400'
+ )}
+ >
+
+
+ {t('chat:refetchButton')}
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/user/settings/EditUserIconUrl.tsx b/typescript/skeet-graphql/src/components/pages/user/settings/EditUserIconUrl.tsx
new file mode 100644
index 000000000000..92521cb79112
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/user/settings/EditUserIconUrl.tsx
@@ -0,0 +1,253 @@
+import clsx from 'clsx'
+import Image from 'next/image'
+import {
+ PencilSquareIcon,
+ PhotoIcon,
+ XMarkIcon,
+} from '@heroicons/react/24/outline'
+import { useTranslation } from 'next-i18next'
+import { Fragment, useCallback, useMemo, useState } from 'react'
+import { useRecoilState } from 'recoil'
+import { userState } from '@/store/user'
+import { storage } from '@/lib/firebase'
+import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'
+import useToastMessage from '@/hooks/useToastMessage'
+import { Dialog, Transition } from '@headlessui/react'
+import { useDropzone } from 'react-dropzone'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import { graphql, useMutation } from 'react-relay'
+import {
+ EditUserIconUrlMutation,
+ EditUserIconUrlMutation$data,
+} from '@/__generated__/EditUserIconUrlMutation.graphql'
+
+const editUserIconUrlMutation = graphql`
+ mutation EditUserIconUrlMutation($id: String, $iconUrl: String) {
+ updateUser(id: $id, iconUrl: $iconUrl) {
+ iconUrl
+ }
+ }
+`
+
+export default function EditUserIconUrl() {
+ const { t } = useTranslation()
+ const [user, setUser] = useRecoilState(userState)
+ const addToast = useToastMessage()
+ const [isLoading, setLoading] = useState(false)
+ const [image, setImage] = useState(null)
+ const [imageUrl, setImageUrl] = useState(null)
+ const [commit] = useMutation(editUserIconUrlMutation)
+
+ const [isModalOpen, setModalOpen] = useState(false)
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ onDrop: (acceptedFiles) => {
+ if (acceptedFiles.length > 0) {
+ const file = acceptedFiles[0]
+ const objectUrl = URL.createObjectURL(file)
+ setImage(file)
+ setImageUrl(objectUrl)
+ }
+ },
+ })
+
+ const uploadImage = useCallback(async () => {
+ try {
+ setLoading(true)
+ if (image && storage && user.uid !== '') {
+ const newProfileIconRef = ref(
+ storage,
+ `User/${user.uid}/profileIcon/profile.${image.type.split('/')[1]}`
+ )
+ await uploadBytes(newProfileIconRef, image)
+
+ const downloadUrl = await getDownloadURL(newProfileIconRef)
+
+ commit({
+ variables: {
+ id: user.id,
+ iconUrl: downloadUrl,
+ },
+ onCompleted: (result: EditUserIconUrlMutation$data) => {
+ setUser({
+ ...user,
+ iconUrl: downloadUrl,
+ })
+
+ addToast({
+ type: 'success',
+ title: t('settings:avatarUpdated'),
+ description: t('settings:avatarUpdatedMessage'),
+ })
+ setImage(null)
+ setImageUrl(null)
+ setModalOpen(false)
+ },
+ onError: (err) => {
+ console.error(err.message)
+ addToast({
+ type: 'error',
+ title: t('settings:avatarUpdatedError'),
+ description: t('settings:avatarUpdatedErrorMessage'),
+ })
+ },
+ updater: (store) => {
+ store.invalidateStore()
+ },
+ })
+ }
+ } catch (err) {
+ console.error(err)
+ if (
+ err instanceof Error &&
+ (err.message.includes('Firebase ID token has expired.') ||
+ err.message.includes('Error: getUserAuth'))
+ ) {
+ addToast({
+ type: 'error',
+ title: t('errorTokenExpiredTitle'),
+ description: t('errorTokenExpiredBody'),
+ })
+ } else {
+ addToast({
+ type: 'error',
+ title: t('settings:avatarUpdatedError'),
+ description: t('settings:avatarUpdatedErrorMessage'),
+ })
+ }
+ } finally {
+ setLoading(false)
+ }
+ }, [user, setUser, t, image, addToast, commit])
+
+ const isDisabled = useMemo(() => {
+ return image == null || isLoading
+ }, [image, isLoading])
+
+ return (
+ <>
+
+
+
+
+
{
+ setModalOpen(true)
+ }}
+ >
+
+ {t('settings:editIconUrl')}
+
+
+
+ setModalOpen(false)}
+ >
+
+
+
+ {/* This element is to trick the browser into centering the modal contents. */}
+
+
+
+
+
+
+
+
+
+
{
+ setModalOpen(false)
+ }}
+ className="h-5 w-5"
+ >
+
+
+
+
+
+ {t('settings:editIconUrl')}
+
+
+
+
+
+
+ {isDragActive ? (
+
+ {t('settings:dropFiles')}
+
+ ) : (
+
+ {t('settings:dragDropFiles')}
+
+ )}
+
+
+
+
+ {image && imageUrl && (
+ <>
+
+ >
+ )}
+
+
+ {
+ uploadImage()
+ }}
+ disabled={isDisabled}
+ className={clsx(
+ isDisabled
+ ? 'cursor-not-allowed bg-gray-300 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
+ : 'bg-gray-900 text-white hover:bg-gray-700 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-200',
+ 'w-full px-3 py-2 text-center text-lg font-bold'
+ )}
+ >
+ {t('settings:upload')}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/user/settings/EditUserProfile.tsx b/typescript/skeet-graphql/src/components/pages/user/settings/EditUserProfile.tsx
new file mode 100644
index 000000000000..9e498423a988
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/user/settings/EditUserProfile.tsx
@@ -0,0 +1,243 @@
+import clsx from 'clsx'
+import { PencilSquareIcon, XMarkIcon } from '@heroicons/react/24/outline'
+import { useTranslation } from 'next-i18next'
+import { useState, useCallback, useMemo, Fragment } from 'react'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import { useRecoilState } from 'recoil'
+import { userState } from '@/store/user'
+import { usernameSchema } from '@/utils/form'
+import useToastMessage from '@/hooks/useToastMessage'
+import { Dialog, Transition } from '@headlessui/react'
+import { z } from 'zod'
+import { useForm, Controller } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { graphql } from 'relay-runtime'
+import { useMutation } from 'react-relay'
+import {
+ EditUserProfileMutation,
+ EditUserProfileMutation$data,
+} from '@/__generated__/EditUserProfileMutation.graphql'
+
+const schema = z.object({
+ username: usernameSchema,
+})
+
+type Inputs = z.infer
+
+const editUserProfileMutation = graphql`
+ mutation EditUserProfileMutation($id: String, $username: String) {
+ updateUser(id: $id, username: $username) {
+ username
+ }
+ }
+`
+
+export default function EditUserProfile() {
+ const { t } = useTranslation()
+ const [isModalOpen, setModalOpen] = useState(false)
+ const [isLoading, setLoading] = useState(false)
+ const [user, setUser] = useRecoilState(userState)
+ const addToast = useToastMessage()
+ const [commit] = useMutation(editUserProfileMutation)
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ control,
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ username: user.username,
+ },
+ })
+
+ const onSubmit = useCallback(
+ async (data: Inputs) => {
+ try {
+ setLoading(true)
+ commit({
+ variables: {
+ id: user.id,
+ username: data.username,
+ },
+ onCompleted: (result: EditUserProfileMutation$data) => {
+ setUser({
+ ...user,
+ username: data.username,
+ })
+ addToast({
+ type: 'success',
+ title: t('settings:updateProfileSuccess'),
+ description: t('settings:updateProfileSuccessMessage'),
+ })
+ setModalOpen(false)
+ setLoading(false)
+ },
+ onError: (err) => {
+ console.error(err.message)
+ addToast({
+ type: 'error',
+ title: t('settings:updateProfileError'),
+ description: t('settings:updateProfileErrorMessage'),
+ })
+ setModalOpen(false)
+ setLoading(false)
+ },
+ updater: (store) => {
+ store.invalidateStore()
+ },
+ })
+ } catch (err) {
+ console.error(err)
+ if (
+ err instanceof Error &&
+ (err.message.includes('Firebase ID token has expired.') ||
+ err.message.includes('Error: getUserAuth'))
+ ) {
+ addToast({
+ type: 'error',
+ title: t('errorTokenExpiredTitle'),
+ description: t('errorTokenExpiredBody'),
+ })
+ } else {
+ addToast({
+ type: 'error',
+ title: t('settings:updateProfileError'),
+ description: t('settings:updateProfileErrorMessage'),
+ })
+ }
+ setModalOpen(false)
+ setLoading(false)
+ }
+ },
+ [t, user, setUser, addToast, setModalOpen, setLoading, commit]
+ )
+
+ const isDisabled = useMemo(
+ () => isLoading || errors.username != null,
+ [isLoading, errors.username]
+ )
+
+ return (
+ <>
+
+
+ {user.username}
+
+
+ {user.email}
+
+
+
+
{
+ setModalOpen(true)
+ }}
+ >
+
+ {t('settings:editProfile')}
+
+
+
+ setModalOpen(false)}
+ >
+
+
+
+ {/* This element is to trick the browser into centering the modal contents. */}
+
+
+
+
+
+
+
+
+
+
{
+ setModalOpen(false)
+ }}
+ className="h-5 w-5 hover:cursor-pointer"
+ >
+
+
+
+
+
+ {t('settings:editProfile')}
+
+
+
+
+
+
+
+ {t('settings:username')}
+ {errors.username && (
+
+ {' : '}
+ {t('settings:usernameErrorText')}
+
+ )}
+
+
+ (
+
+ )}
+ />
+
+
+
+
+
+ {t('settings:register')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/pages/user/settings/SettingsScreen.tsx b/typescript/skeet-graphql/src/components/pages/user/settings/SettingsScreen.tsx
new file mode 100644
index 000000000000..7f9f3e69eb85
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/pages/user/settings/SettingsScreen.tsx
@@ -0,0 +1,38 @@
+import LanguageChanger from '@/components/utils/LanguageChanger'
+import ColorModeChanger from '@/components/utils/ColorModeChanger'
+import { useTranslation } from 'next-i18next'
+
+import EditUserIconUrl from '@/components/pages/user/settings/EditUserIconUrl'
+import EditUserProfile from '@/components/pages/user/settings/EditUserProfile'
+
+export default function SettingsScreen() {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+
+
+ {t('settings:title')}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/routing/Link.tsx b/typescript/skeet-graphql/src/components/routing/Link.tsx
new file mode 100644
index 000000000000..0710c01ff725
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/routing/Link.tsx
@@ -0,0 +1,33 @@
+import { ReactNode } from 'react'
+import NextLink from 'next/link'
+import { useRouter } from 'next/router'
+
+type Props = {
+ children: ReactNode
+ href: string
+ className?: string
+ skipLocaleHandling?: boolean
+ locale?: string
+ onClick?: () => void
+}
+
+export default function Link({ children, skipLocaleHandling, ...rest }: Props) {
+ const router = useRouter()
+ const locale = rest.locale || router.query.locale || ''
+
+ let href = rest.href || router.asPath
+ if (href.indexOf('http') === 0) skipLocaleHandling = true
+ if (locale && !skipLocaleHandling) {
+ href = href
+ ? `/${locale}${href}`
+ : router.pathname.replace('[locale]', locale as string)
+ }
+
+ return (
+ <>
+
+ {children}
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/routing/Redirect.tsx b/typescript/skeet-graphql/src/components/routing/Redirect.tsx
new file mode 100644
index 000000000000..839bc4a67b66
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/routing/Redirect.tsx
@@ -0,0 +1,5 @@
+import useRedirect from '@/hooks/useRedirect'
+export default function Redirect() {
+ useRedirect()
+ return <>>
+}
diff --git a/typescript/skeet-graphql/src/components/utils/AgreeToPolicy.tsx b/typescript/skeet-graphql/src/components/utils/AgreeToPolicy.tsx
new file mode 100644
index 000000000000..1f1a9233389e
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/utils/AgreeToPolicy.tsx
@@ -0,0 +1,87 @@
+import { useCallback, useState, useEffect } from 'react'
+import { useRouter } from 'next/router'
+import { Analytics, logEvent, getAnalytics } from 'firebase/analytics'
+import { useTranslation } from 'next-i18next'
+import { useRecoilState } from 'recoil'
+import { policyAgreedState } from '@/store/policy'
+import Link from '@/components/routing/Link'
+import Button from '@/components/common/atoms/Button'
+import { firebaseApp } from '@/lib/firebase'
+
+export default function AgreeToPolicy() {
+ const [policyAgreed, setPolicyAgreed] = useRecoilState(policyAgreedState)
+ const [open, setOpen] = useState(!policyAgreed)
+
+ const [analytics, setAnalytics] = useState(undefined)
+
+ const router = useRouter()
+ const { t } = useTranslation()
+
+ const handleAgree = useCallback(() => {
+ setOpen(false)
+ setPolicyAgreed(true)
+ }, [setOpen, setPolicyAgreed])
+
+ const handleClose = useCallback(() => {
+ setOpen(false)
+ }, [])
+
+ useEffect(() => {
+ if (policyAgreed) {
+ if (firebaseApp && !analytics) {
+ if (
+ typeof window !== 'undefined' &&
+ process.env.NODE_ENV !== 'development'
+ ) {
+ setAnalytics(getAnalytics(firebaseApp))
+ }
+ }
+ if (firebaseApp && analytics) {
+ logEvent(analytics, 'page_view', {
+ page_title: document.title,
+ page_location: document.URL,
+ page_path: router.asPath,
+ })
+ }
+ } else {
+ setOpen(true)
+ }
+ }, [setOpen, policyAgreed, router.asPath, analytics])
+
+ return (
+ <>
+ {open && (
+
+
+
+
+
+ {t('AgreeToPolicy.title')}
+
+
{t('AgreeToPolicy.body')}
+
+ {t('privacy')}
+
+
+
+ handleClose()}
+ >
+ {t('AgreeToPolicy.no')}
+
+ handleAgree()}>
+ {t('AgreeToPolicy.yes')}
+
+
+
+
+
+ )}
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/utils/ColorModeChanger.tsx b/typescript/skeet-graphql/src/components/utils/ColorModeChanger.tsx
new file mode 100755
index 000000000000..3e54ea58a5f8
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/utils/ColorModeChanger.tsx
@@ -0,0 +1,26 @@
+import { useTheme } from 'next-themes'
+import { MoonIcon, SunIcon } from '@heroicons/react/20/solid'
+
+export default function ColorModeChanger() {
+ const { resolvedTheme, setTheme } = useTheme()
+
+ return (
+ <>
+ {
+ resolvedTheme === 'light' ? setTheme('dark') : setTheme('light')
+ }}
+ className="group inline-flex items-center p-1 text-base font-medium text-gray-700 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:text-gray-50"
+ >
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/utils/LanguageChanger.tsx b/typescript/skeet-graphql/src/components/utils/LanguageChanger.tsx
new file mode 100644
index 000000000000..59e21b89497a
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/utils/LanguageChanger.tsx
@@ -0,0 +1,77 @@
+import { Fragment } from 'react'
+import { Menu, Transition } from '@headlessui/react'
+
+import useChangeLanguage from '@/hooks/useChangeLanguage'
+import { LanguageIcon } from '@heroicons/react/20/solid'
+import clsx from 'clsx'
+
+export default function LanguageChanger() {
+ const { changeLanguage } = useChangeLanguage()
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {({ close }) => (
+ <>
+ {
+ changeLanguage('en')
+ close()
+ }}
+ >
+ English
+
+ >
+ )}
+
+
+ {({ close }) => (
+ <>
+ {
+ changeLanguage('ja')
+ close()
+ }}
+ >
+ 日本語
+
+ >
+ )}
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/utils/ToastMessage.tsx b/typescript/skeet-graphql/src/components/utils/ToastMessage.tsx
new file mode 100644
index 000000000000..a2688c2889f7
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/utils/ToastMessage.tsx
@@ -0,0 +1,107 @@
+import { Fragment, useEffect } from 'react'
+import { Transition } from '@headlessui/react'
+import {
+ CheckCircleIcon,
+ ExclamationCircleIcon,
+ InformationCircleIcon,
+} from '@heroicons/react/24/outline'
+import { XMarkIcon } from '@heroicons/react/20/solid'
+import { useRecoilState } from 'recoil'
+import { toastsState } from '@/store/toasts'
+
+export default function ToastMessage() {
+ const [toasts, setToasts] = useRecoilState(toastsState)
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (toasts.length > 0) {
+ setToasts(toasts.filter((toast) => toast.createdAt > Date.now() - 4000))
+ }
+ }, 200)
+
+ return () => {
+ clearInterval(interval)
+ }
+ }, [toasts, setToasts])
+
+ return (
+ <>
+
+
+ {toasts.map((toast, toastIdx) => (
+
Date.now() - 4000}
+ as={Fragment}
+ enter="transform ease-out duration-300 transition"
+ enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
+ enterTo="translate-y-0 opacity-100 sm:translate-x-0"
+ leave="transition ease-in duration-100"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+
+
+
+
+ {toast.type === 'success' && (
+
+ )}
+ {toast.type === 'error' && (
+
+ )}
+ {toast.type === 'warning' && (
+
+ )}
+ {toast.type === 'info' && (
+
+ )}
+
+
+
+ {toast.title}
+
+
+ {toast.description}
+
+
+
+ {
+ const newToasts = toasts.filter(
+ (_, idx) => idx !== toastIdx
+ )
+ setToasts(newToasts)
+ }}
+ >
+ Close
+
+
+
+
+
+
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/components/utils/YouTubeEmbed.tsx b/typescript/skeet-graphql/src/components/utils/YouTubeEmbed.tsx
new file mode 100644
index 000000000000..85721f1e28ef
--- /dev/null
+++ b/typescript/skeet-graphql/src/components/utils/YouTubeEmbed.tsx
@@ -0,0 +1,19 @@
+type Props = {
+ embedId: string
+}
+export default function YouTubeEmbed({ embedId }: Props) {
+ return (
+ <>
+
+ VIDEO
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/config/navs.ts b/typescript/skeet-graphql/src/config/navs.ts
new file mode 100644
index 000000000000..17685eccfe96
--- /dev/null
+++ b/typescript/skeet-graphql/src/config/navs.ts
@@ -0,0 +1,89 @@
+import {
+ BookOpenIcon,
+ ChatBubbleLeftRightIcon,
+ Cog8ToothIcon,
+ HeartIcon,
+ HomeIcon,
+ RocketLaunchIcon,
+} from '@heroicons/react/24/outline'
+
+export const defaultMainNav = [
+ {
+ name: 'navs.defaultMainNav.news',
+ href: '/news/',
+ },
+ {
+ name: 'navs.defaultMainNav.doc',
+ href: '/doc/',
+ },
+]
+
+export const commonFooterNav = [
+ {
+ name: 'navs.commonFooterNav.news',
+ href: '/news/',
+ },
+ {
+ name: 'navs.commonFooterNav.doc',
+ href: '/doc/',
+ },
+ {
+ name: 'navs.commonFooterNav.privacy',
+ href: '/legal/privacy-policy/',
+ },
+]
+
+export const docMenuNav = [
+ { name: 'doc:menuNav.home', href: '/doc/', icon: HomeIcon },
+ {
+ name: 'doc:menuNav.general.groupTitle',
+ children: [
+ {
+ name: 'doc:menuNav.general.motivation',
+ href: '/doc/general/motivation/',
+ icon: HeartIcon,
+ },
+ {
+ name: 'doc:menuNav.general.quickstart',
+ href: '/doc/general/quickstart/',
+ icon: RocketLaunchIcon,
+ },
+ {
+ name: 'doc:menuNav.general.readme',
+ href: '/doc/general/readme/',
+ icon: BookOpenIcon,
+ },
+ ],
+ },
+]
+
+export const docHeaderNav = [
+ {
+ name: 'doc:headerNav.home',
+ href: '/',
+ },
+ {
+ name: 'doc:headerNav.news',
+ href: '/news/',
+ },
+]
+
+export const userMenuNav = [
+ {
+ name: 'user:menuNav.chat',
+ href: '/user/chat/',
+ icon: ChatBubbleLeftRightIcon,
+ },
+ {
+ name: 'user:menuNav.settings',
+ href: '/user/settings/',
+ icon: Cog8ToothIcon,
+ },
+]
+
+export const userHeaderNav = [
+ {
+ name: 'user:headerNav.settings',
+ href: '/user/settings/',
+ },
+]
diff --git a/typescript/skeet-graphql/src/config/site.ts b/typescript/skeet-graphql/src/config/site.ts
new file mode 100644
index 000000000000..038944819598
--- /dev/null
+++ b/typescript/skeet-graphql/src/config/site.ts
@@ -0,0 +1,22 @@
+import skeetCloudConfig from '@root/skeet-cloud.config.json'
+
+const siteConfig = {
+ domain: skeetCloudConfig.app.appDomain,
+ copyright: 'ELSOUL LABO B.V.',
+ sitenameJA: 'Skeet Next.js GraphQL',
+ sitenameEN: 'Skeet Next.js GraphQL',
+ keywordsJA:
+ 'Next.js, Firebase, SSG, テンプレート, SEO, 多言語対応, サーバーレス, TypeScript, PWA, GraphQL',
+ keywordsEN:
+ 'Next.js, Firebase, SSG, Template, SEO, i18n translation, Serverless, TypeScript, PWA, GraphQL',
+ descriptionJA:
+ 'Next.jsのボイラープレート。SEO対応、多言語対応、SSG、PWA。WebAppをすぐに構築開始でき、そのデプロイは保証されています。',
+ descriptionEN:
+ 'Next.js Boilerplate. SEO compatible, i18n translation, SSG, PWA. You can start building your WebApp today, and its deployment is guaranteed.',
+ twitterAccount: '@SkeetDev',
+ instagramAccount: 'elsoul_labo',
+ githubAccount: 'elsoul',
+ discordInvitationLink: 'https://discord.gg/H2HeqRq54J',
+}
+
+export default siteConfig
diff --git a/typescript/skeet-graphql/src/hooks/useChangeLanguage.ts b/typescript/skeet-graphql/src/hooks/useChangeLanguage.ts
new file mode 100644
index 000000000000..d45fb47310b5
--- /dev/null
+++ b/typescript/skeet-graphql/src/hooks/useChangeLanguage.ts
@@ -0,0 +1,25 @@
+import { useCallback, useMemo } from 'react'
+import { useTranslation } from 'next-i18next'
+import { useRouter } from 'next/router'
+
+export default function useChangeLanguage() {
+ const { i18n } = useTranslation()
+ const currentLanguage = useMemo(() => i18n.language, [i18n])
+ const router = useRouter()
+ const changeLanguage = useCallback(
+ async (lang: string) => {
+ const oldLang = currentLanguage.slice()
+ await i18n.changeLanguage(lang)
+ if (router.asPath === '/en' || router.asPath === '/ja') {
+ router.push(`${router.asPath.replace(`/${oldLang}`, `/${lang}`)}`)
+ } else {
+ router.push(`${router.asPath.replace(`/${oldLang}/`, `/${lang}/`)}`)
+ }
+ },
+ [i18n, router, currentLanguage]
+ )
+ return {
+ currentLanguage,
+ changeLanguage,
+ }
+}
diff --git a/typescript/skeet-graphql/src/hooks/useI18nRouter.ts b/typescript/skeet-graphql/src/hooks/useI18nRouter.ts
new file mode 100644
index 000000000000..3855e46ee703
--- /dev/null
+++ b/typescript/skeet-graphql/src/hooks/useI18nRouter.ts
@@ -0,0 +1,20 @@
+import { useCallback, useMemo } from 'react'
+import { useTranslation } from 'next-i18next'
+import { useRouter } from 'next/router'
+
+export default function useI18nRouter() {
+ const { i18n } = useTranslation()
+ const currentLanguage = useMemo(() => i18n.language, [i18n])
+ const router = useRouter()
+ const routerPush = useCallback(
+ (path: string) => {
+ router.push(`/${currentLanguage}${path}`)
+ },
+ [router, currentLanguage]
+ )
+ return {
+ router,
+ currentLanguage,
+ routerPush,
+ }
+}
diff --git a/typescript/skeet-graphql/src/hooks/useRedirect.ts b/typescript/skeet-graphql/src/hooks/useRedirect.ts
new file mode 100644
index 000000000000..29f12979af78
--- /dev/null
+++ b/typescript/skeet-graphql/src/hooks/useRedirect.ts
@@ -0,0 +1,19 @@
+import { useEffect } from 'react'
+import { useRouter } from 'next/router'
+import languageDetector from '@/lib/languageDetector'
+
+export default function useRedirect(to?: string) {
+ const router = useRouter()
+ to = to || router.asPath
+
+ useEffect(() => {
+ const detectedLng = languageDetector.detect() as string
+ if (to?.startsWith('/' + detectedLng) && router.route === '/404') {
+ router.replace('/' + detectedLng + router.route)
+ return
+ }
+
+ languageDetector.cache?.(detectedLng)
+ router.replace('/' + detectedLng + to)
+ })
+}
diff --git a/typescript/skeet-graphql/src/hooks/useToastMessage.ts b/typescript/skeet-graphql/src/hooks/useToastMessage.ts
new file mode 100644
index 000000000000..06b45530e57e
--- /dev/null
+++ b/typescript/skeet-graphql/src/hooks/useToastMessage.ts
@@ -0,0 +1,25 @@
+import { Toast, toastsState } from '@/store/toasts'
+import { useRecoilState } from 'recoil'
+import { useCallback } from 'react'
+
+type ToastMessage = {
+ title: string
+ description: string
+ type: 'success' | 'error' | 'warning' | 'info'
+}
+
+export default function useToastMessage() {
+ const [toasts, setToasts] = useRecoilState(toastsState)
+
+ const addToast = useCallback(
+ (toastMessage: ToastMessage) => {
+ const toast: Toast = {
+ ...toastMessage,
+ createdAt: Date.now(),
+ }
+ setToasts([...toasts, toast])
+ },
+ [toasts, setToasts]
+ )
+ return addToast
+}
diff --git a/typescript/skeet-graphql/src/layouts/Layout.tsx b/typescript/skeet-graphql/src/layouts/Layout.tsx
new file mode 100644
index 000000000000..bcc388691360
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/Layout.tsx
@@ -0,0 +1,62 @@
+import { useState, useEffect } from 'react'
+import AppLoading from '@/components/loading/AppLoading'
+import AgreeToPolicy from '@/components/utils/AgreeToPolicy'
+import { AppPropsWithLayout } from '@/pages/_app'
+import { Suspense } from 'react'
+import ToastMessage from '@/components/utils/ToastMessage'
+
+export default function Layout({ Component, pageProps }: AppPropsWithLayout) {
+ const [mounted, setMounted] = useState(false)
+ const getLayout = Component.getLayout ?? ((page) => page)
+
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ useEffect(() => {
+ const setVh = () => {
+ const vh = window.innerHeight * 0.01
+ document.documentElement.style.setProperty('--vh', `${vh}px`)
+ }
+
+ setVh()
+
+ window.addEventListener('resize', setVh)
+
+ return () => {
+ window.removeEventListener('resize', setVh)
+ }
+ }, [])
+
+ if (!mounted) {
+ return (
+ <>
+
+ >
+ )
+ }
+
+ return (
+ <>
+ {!getLayout ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+
}>
+ {getLayout(
)}
+
+
+
+
+ >
+ )}
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/action/ActionHeader.tsx b/typescript/skeet-graphql/src/layouts/action/ActionHeader.tsx
new file mode 100644
index 000000000000..de7a20f0f75a
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/action/ActionHeader.tsx
@@ -0,0 +1,168 @@
+import { Popover, Transition } from '@headlessui/react'
+import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
+import { useCallback, useEffect, useState, Fragment } from 'react'
+import { useTranslation } from 'next-i18next'
+import LanguageChanger from '@/components/utils/LanguageChanger'
+import ColorModeChanger from '@/components/utils/ColorModeChanger'
+import Link from '@/components/routing/Link'
+import LogoHorizontalLink from '@/components/common/atoms/LogoHorizontalLink'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
+import siteConfig from '@/config/site'
+import Button from '@/components/common/atoms/Button'
+
+export default function ActionHeader() {
+ const { t } = useTranslation()
+
+ const [scrollY, setScrollY] = useState(0)
+ const [isScrollingUp, setIsScrollingUp] = useState(false)
+
+ const handleScroll = useCallback(() => {
+ setScrollY(window.scrollY)
+ if (window.scrollY > 104 && scrollY > window.scrollY) {
+ setIsScrollingUp(true)
+ } else {
+ setIsScrollingUp(false)
+ }
+ }, [setScrollY, setIsScrollingUp, scrollY])
+
+ useEffect(() => {
+ window.addEventListener('scroll', handleScroll)
+ return () => {
+ window.removeEventListener('scroll', handleScroll)
+ }
+ }, [handleScroll])
+
+ return (
+ <>
+ {isScrollingUp && (
+
+ )}
+
+
+ {({ close }) => (
+ <>
+
+
+
+
+
+
+ {t('openMenu')}
+
+
+
+
+
+
+ {t('login')}
+
+
+ {t('register')}
+
+
+
+
+
+
+
+
+
+
+
+ close()}
+ />
+
+
+
+ {t('closeMenu')}
+
+
+
+
+
+
+
+
close()}
+ >
+ {t('login')}
+
+
close()}
+ >
+ {t('register')}
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/action/ActionLayout.tsx b/typescript/skeet-graphql/src/layouts/action/ActionLayout.tsx
new file mode 100644
index 000000000000..128aa08de84f
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/action/ActionLayout.tsx
@@ -0,0 +1,43 @@
+import type { ReactNode } from 'react'
+import { useEffect, useCallback } from 'react'
+import CommonFooter from '@/layouts/common/CommonFooter'
+
+import { useRouter } from 'next/router'
+import ActionHeader from './ActionHeader'
+
+type Props = {
+ children: ReactNode
+}
+
+const mainContentId = 'actionMainContent'
+
+export default function ActionLayout({ children }: Props) {
+ const router = useRouter()
+
+ const resetWindowScrollPosition = useCallback(() => {
+ const element = document.getElementById(mainContentId)
+ if (element) {
+ element.scrollIntoView({ block: 'start' })
+ }
+ }, [])
+ useEffect(() => {
+ ;(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ if (!router.asPath.includes('#')) {
+ resetWindowScrollPosition()
+ }
+ })()
+ }, [router.asPath, resetWindowScrollPosition])
+
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/auth/AuthHeader.tsx b/typescript/skeet-graphql/src/layouts/auth/AuthHeader.tsx
new file mode 100644
index 000000000000..f59470ccb6b4
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/auth/AuthHeader.tsx
@@ -0,0 +1,168 @@
+import { Popover, Transition } from '@headlessui/react'
+import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
+import { useCallback, useEffect, useState, Fragment } from 'react'
+import { useTranslation } from 'next-i18next'
+import LanguageChanger from '@/components/utils/LanguageChanger'
+import ColorModeChanger from '@/components/utils/ColorModeChanger'
+import Link from '@/components/routing/Link'
+import LogoHorizontalLink from '@/components/common/atoms/LogoHorizontalLink'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
+import siteConfig from '@/config/site'
+import Button from '@/components/common/atoms/Button'
+
+export default function AuthHeader() {
+ const { t } = useTranslation()
+
+ const [scrollY, setScrollY] = useState(0)
+ const [isScrollingUp, setIsScrollingUp] = useState(false)
+
+ const handleScroll = useCallback(() => {
+ setScrollY(window.scrollY)
+ if (window.scrollY > 104 && scrollY > window.scrollY) {
+ setIsScrollingUp(true)
+ } else {
+ setIsScrollingUp(false)
+ }
+ }, [setScrollY, setIsScrollingUp, scrollY])
+
+ useEffect(() => {
+ window.addEventListener('scroll', handleScroll)
+ return () => {
+ window.removeEventListener('scroll', handleScroll)
+ }
+ }, [handleScroll])
+
+ return (
+ <>
+ {isScrollingUp && (
+
+ )}
+
+
+ {({ close }) => (
+ <>
+
+
+
+
+
+
+ {t('openMenu')}
+
+
+
+
+
+
+ {t('login')}
+
+
+ {t('register')}
+
+
+
+
+
+
+
+
+
+
+
+ close()}
+ />
+
+
+
+ {t('closeMenu')}
+
+
+
+
+
+
+
+
close()}
+ >
+ {t('login')}
+
+
close()}
+ >
+ {t('register')}
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/auth/AuthLayout.tsx b/typescript/skeet-graphql/src/layouts/auth/AuthLayout.tsx
new file mode 100644
index 000000000000..77510a1d3a21
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/auth/AuthLayout.tsx
@@ -0,0 +1,101 @@
+import type { ReactNode } from 'react'
+import { useEffect, useCallback } from 'react'
+import CommonFooter from '@/layouts/common/CommonFooter'
+import { User, signOut } from 'firebase/auth'
+
+import { fetchQuery, graphql } from 'react-relay'
+
+import { useRouter } from 'next/router'
+import AuthHeader from './AuthHeader'
+import { useRecoilState } from 'recoil'
+import { defaultUser, userState } from '@/store/user'
+import { auth } from '@/lib/firebase'
+import { createEnvironment } from '@/lib/relayEnvironment'
+import { AuthLayoutQuery } from '@/__generated__/AuthLayoutQuery.graphql'
+
+type Props = {
+ children: ReactNode
+}
+
+const mainContentId = 'authMainContent'
+
+export const authLayoutQuery = graphql`
+ query AuthLayoutQuery {
+ me {
+ id
+ iconUrl
+ username
+ }
+ }
+`
+
+export default function AuthLayout({ children }: Props) {
+ const router = useRouter()
+
+ const resetWindowScrollPosition = useCallback(() => {
+ const element = document.getElementById(mainContentId)
+ if (element) {
+ element.scrollIntoView({ block: 'start' })
+ }
+ }, [])
+ useEffect(() => {
+ ;(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ if (!router.asPath.includes('#')) {
+ resetWindowScrollPosition()
+ }
+ })()
+ }, [router.asPath, resetWindowScrollPosition])
+
+ const [_user, setUser] = useRecoilState(userState)
+
+ const onAuthStateChanged = useCallback(
+ async (fbUser: User | null) => {
+ if (auth && fbUser && fbUser.emailVerified) {
+ const user = await fetchQuery(
+ createEnvironment(),
+ authLayoutQuery,
+ {}
+ ).toPromise()
+ if (user?.me?.id) {
+ setUser({
+ id: user.me.id,
+ uid: fbUser.uid,
+ email: fbUser.email ?? '',
+ username: user.me.username ?? '',
+ iconUrl: user.me.iconUrl ?? '',
+ emailVerified: fbUser.emailVerified,
+ })
+ router.push('/user/chat')
+ } else {
+ setUser(defaultUser)
+ signOut(auth)
+ }
+ } else {
+ setUser(defaultUser)
+ }
+ },
+ [setUser, router]
+ )
+
+ useEffect(() => {
+ let subscriber = () => {}
+
+ if (auth) {
+ subscriber = auth.onAuthStateChanged(onAuthStateChanged)
+ }
+ return () => subscriber()
+ }, [onAuthStateChanged])
+
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/common/CommonFooter.tsx b/typescript/skeet-graphql/src/layouts/common/CommonFooter.tsx
new file mode 100644
index 000000000000..bbfd91d3c34d
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/common/CommonFooter.tsx
@@ -0,0 +1,108 @@
+import { useTranslation } from 'next-i18next'
+import Link from '@/components/routing/Link'
+import Container from '@/components/common/atoms/Container'
+import LogoHorizontalLink from '@/components/common/atoms/LogoHorizontalLink'
+import siteConfig from '@/config/site'
+import { commonFooterNav } from '@/config/navs'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+ faDiscord,
+ faInstagram,
+ faGithub,
+ faTwitter,
+} from '@fortawesome/free-brands-svg-icons'
+
+export default function CommonFooter() {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {commonFooterNav.map((nav) => (
+
+ {t(nav.name)}
+
+ ))}
+
+
+
+
+
+
+ © {new Date().getFullYear()} {siteConfig.copyright} All
+ rights reserved.
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/default/DefaultHeader.tsx b/typescript/skeet-graphql/src/layouts/default/DefaultHeader.tsx
new file mode 100644
index 000000000000..7c2703a59864
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/default/DefaultHeader.tsx
@@ -0,0 +1,189 @@
+import { Popover, Transition } from '@headlessui/react'
+import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
+import { useCallback, useEffect, useState, Fragment } from 'react'
+import { useTranslation } from 'next-i18next'
+import LanguageChanger from '@/components/utils/LanguageChanger'
+import ColorModeChanger from '@/components/utils/ColorModeChanger'
+import Link from '@/components/routing/Link'
+import LogoHorizontalLink from '@/components/common/atoms/LogoHorizontalLink'
+import { defaultMainNav } from '@/config/navs'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
+import siteConfig from '@/config/site'
+import Button from '@/components/common/atoms/Button'
+
+export default function DefaultHeader() {
+ const { t } = useTranslation()
+
+ const [scrollY, setScrollY] = useState(0)
+ const [isScrollingUp, setIsScrollingUp] = useState(false)
+
+ const handleScroll = useCallback(() => {
+ setScrollY(window.scrollY)
+ if (window.scrollY > 104 && scrollY > window.scrollY) {
+ setIsScrollingUp(true)
+ } else {
+ setIsScrollingUp(false)
+ }
+ }, [setScrollY, setIsScrollingUp, scrollY])
+
+ useEffect(() => {
+ window.addEventListener('scroll', handleScroll)
+ return () => {
+ window.removeEventListener('scroll', handleScroll)
+ }
+ }, [handleScroll])
+
+ return (
+ <>
+ {isScrollingUp && (
+
+ )}
+
+
+ {({ close }) => (
+ <>
+
+
+
+
+
+
+ {t('openMenu')}
+
+
+
+
+ {defaultMainNav.map((nav) => (
+
+ {t(nav.name)}
+
+ ))}
+
+
+
+ {t('login')}
+
+
+ {t('register')}
+
+
+
+
+
+
+
+
+
+
+
+ close()}
+ />
+
+
+
+ {t('closeMenu')}
+
+
+
+
+
+
+
+ {defaultMainNav.map((nav) => (
+
close()}
+ >
+ {t(nav.name)}
+
+ ))}
+
close()}
+ >
+ {t('login')}
+
+
close()}
+ >
+ {t('register')}
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/default/DefaultLayout.tsx b/typescript/skeet-graphql/src/layouts/default/DefaultLayout.tsx
new file mode 100644
index 000000000000..9d2321f2edaa
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/default/DefaultLayout.tsx
@@ -0,0 +1,43 @@
+import type { ReactNode } from 'react'
+import { useEffect, useCallback } from 'react'
+import DefaultHeader from './DefaultHeader'
+import CommonFooter from '@/layouts/common/CommonFooter'
+
+import { useRouter } from 'next/router'
+
+type Props = {
+ children: ReactNode
+}
+
+const mainContentId = 'defaultMainContent'
+
+export default function DefaultLayout({ children }: Props) {
+ const router = useRouter()
+
+ const resetWindowScrollPosition = useCallback(() => {
+ const element = document.getElementById(mainContentId)
+ if (element) {
+ element.scrollIntoView({ block: 'start' })
+ }
+ }, [])
+ useEffect(() => {
+ ;(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ if (!router.asPath.includes('#')) {
+ resetWindowScrollPosition()
+ }
+ })()
+ }, [router.asPath, resetWindowScrollPosition])
+
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/doc/DocLayout.tsx b/typescript/skeet-graphql/src/layouts/doc/DocLayout.tsx
new file mode 100644
index 000000000000..3aaa4ecd0b72
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/doc/DocLayout.tsx
@@ -0,0 +1,346 @@
+import type { ReactNode } from 'react'
+import { useMemo, useCallback, useEffect, useState, Fragment } from 'react'
+import CommonFooter from '@/layouts/common/CommonFooter'
+import { Transition, Dialog, Menu } from '@headlessui/react'
+import {
+ XMarkIcon,
+ Bars3BottomLeftIcon,
+ EllipsisVerticalIcon,
+} from '@heroicons/react/24/outline'
+import { useRouter } from 'next/router'
+import LogoHorizontalLink from '@/components/common/atoms/LogoHorizontalLink'
+import clsx from 'clsx'
+import { docHeaderNav, docMenuNav } from '@/config/navs'
+import { useTranslation } from 'next-i18next'
+import Link from '@/components/routing/Link'
+import LanguageChanger from '@/components/utils/LanguageChanger'
+import ColorModeChanger from '@/components/utils/ColorModeChanger'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
+import siteConfig from '@/config/site'
+
+type Props = {
+ children: ReactNode
+}
+
+const mainContentId = 'docMainContent'
+
+export default function DocLayout({ children }: Props) {
+ const router = useRouter()
+ const [sidebarOpen, setSidebarOpen] = useState(false)
+ const { t } = useTranslation()
+
+ const asPathWithoutLang = useMemo(() => {
+ return router.asPath.replace('/ja/', '/').replace('/en/', '/')
+ }, [router.asPath])
+
+ const resetWindowScrollPosition = useCallback(() => {
+ const element = document.getElementById(mainContentId)
+ if (element) {
+ element.scrollIntoView({ block: 'start' })
+ }
+ }, [])
+ useEffect(() => {
+ ;(async () => {
+ setSidebarOpen(false)
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ if (!router.asPath.includes('#')) {
+ resetWindowScrollPosition()
+ }
+ })()
+ }, [router.asPath, resetWindowScrollPosition])
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ setSidebarOpen(false)}
+ >
+ Close sidebar
+
+
+
+
+
+
+
+
+
+ {docMenuNav.map((item) =>
+ !item.href && item.children ? (
+
+
+ {t(item.name)}
+
+ {item.children.map((nav) => (
+
+
+ {t(nav.name)}
+
+ ))}
+
+ ) : (
+
+ {item.icon && (
+
+ )}
+ {t(item.name)}
+
+ )
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {docMenuNav.map((item) =>
+ !item.href && item.children ? (
+
+
+ {t(item.name)}
+
+ {item.children.map((nav) => (
+
+
+ {t(nav.name)}
+
+ ))}
+
+ ) : (
+
+ {item.icon && (
+
+ )}
+ {t(item.name)}
+
+ )
+ )}
+
+
+
+
+
+
+
setSidebarOpen(true)}
+ >
+ Open sidebar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Open other menu
+
+
+
+
+
+ {docHeaderNav.map((item) => (
+
+ {({ active }) => (
+
+ {t(item.name)}
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/layouts/user/UserLayout.tsx b/typescript/skeet-graphql/src/layouts/user/UserLayout.tsx
new file mode 100644
index 000000000000..bea9262eb103
--- /dev/null
+++ b/typescript/skeet-graphql/src/layouts/user/UserLayout.tsx
@@ -0,0 +1,327 @@
+import type { ReactNode } from 'react'
+import { useMemo, useCallback, useEffect, useState, Fragment } from 'react'
+import { Transition, Dialog, Menu } from '@headlessui/react'
+import { XMarkIcon, Bars3BottomLeftIcon } from '@heroicons/react/24/outline'
+import { useRouter } from 'next/router'
+import clsx from 'clsx'
+import { userHeaderNav, userMenuNav } from '@/config/navs'
+import { useTranslation } from 'next-i18next'
+import Link from '@/components/routing/Link'
+import { User, signOut } from 'firebase/auth'
+import { useRecoilState } from 'recoil'
+import { defaultUser, userState } from '@/store/user'
+import { auth } from '@/lib/firebase'
+import LogoHorizontal from '@/components/common/atoms/LogoHorizontal'
+import Image from 'next/image'
+import { fetchQuery, graphql } from 'react-relay'
+import { UserLayoutQuery } from '@/__generated__/UserLayoutQuery.graphql'
+import { createEnvironment } from '@/lib/relayEnvironment'
+
+type Props = {
+ children: ReactNode
+}
+
+const mainContentId = 'userMainContent'
+
+export const userLayoutQuery = graphql`
+ query UserLayoutQuery {
+ me {
+ id
+ iconUrl
+ username
+ }
+ }
+`
+
+export default function UserLayout({ children }: Props) {
+ const router = useRouter()
+ const [sidebarOpen, setSidebarOpen] = useState(false)
+ const { t } = useTranslation()
+
+ const asPathWithoutLang = useMemo(() => {
+ return router.asPath.replace('/ja/', '/').replace('/en/', '/')
+ }, [router.asPath])
+
+ const resetWindowScrollPosition = useCallback(() => {
+ const element = document.getElementById(mainContentId)
+ if (element) {
+ element.scrollIntoView({ block: 'start' })
+ }
+ }, [])
+ useEffect(() => {
+ ;(async () => {
+ setSidebarOpen(false)
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ if (!router.asPath.includes('#')) {
+ resetWindowScrollPosition()
+ }
+ })()
+ }, [router.asPath, resetWindowScrollPosition])
+
+ const [user, setUser] = useRecoilState(userState)
+
+ const onAuthStateChanged = useCallback(
+ async (fbUser: User | null) => {
+ if (auth && fbUser && fbUser.emailVerified) {
+ const user = await fetchQuery(
+ createEnvironment(),
+ userLayoutQuery,
+ {}
+ ).toPromise()
+ if (user?.me?.id) {
+ setUser({
+ id: user.me.id,
+ uid: fbUser.uid,
+ email: fbUser.email ?? '',
+ username: user.me.username ?? '',
+ iconUrl: user.me.iconUrl ?? '',
+ emailVerified: fbUser.emailVerified,
+ })
+ } else {
+ setUser(defaultUser)
+ signOut(auth)
+ router.push('/auth/login')
+ }
+ } else {
+ setUser(defaultUser)
+ router.push('/auth/login')
+ }
+ },
+ [setUser, router]
+ )
+
+ useEffect(() => {
+ let subscriber = () => {}
+
+ if (auth) {
+ subscriber = auth.onAuthStateChanged(onAuthStateChanged)
+ }
+ return () => subscriber()
+ }, [onAuthStateChanged])
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ setSidebarOpen(false)}
+ >
+ Close sidebar
+
+
+
+
+
+
+
+
+
+ {userMenuNav.map((item) => (
+
+ {item.icon && (
+
+ )}
+ {t(item.name)}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {userMenuNav.map((item) => (
+
+ {item.icon && (
+
+ )}
+ {t(item.name)}
+
+ ))}
+
+
+
+
+
+
+
setSidebarOpen(true)}
+ >
+ Open sidebar
+
+
+
+
+
+
+
+
+ Open other menu
+ {user.iconUrl && (
+
+ )}
+
+
+
+
+ {userHeaderNav.map((item) => (
+
+ {({ active }) => (
+
+ {t(item.name)}
+
+ )}
+
+ ))}
+
+ {({ active }) => (
+ {
+ if (auth) {
+ setUser(defaultUser)
+ signOut(auth)
+ }
+ }}
+ className={clsx(
+ active
+ ? 'bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white'
+ : '',
+ 'block px-4 py-2 text-sm text-gray-700 hover:cursor-pointer dark:text-gray-50'
+ )}
+ >
+ {t('logout')}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/typescript/skeet-graphql/src/lib/firebase.ts b/typescript/skeet-graphql/src/lib/firebase.ts
new file mode 100644
index 000000000000..93cc8232c38e
--- /dev/null
+++ b/typescript/skeet-graphql/src/lib/firebase.ts
@@ -0,0 +1,41 @@
+import firebaseConfig from '@lib/firebaseConfig'
+import { initializeApp, getApp, getApps } from 'firebase/app'
+import { connectAuthEmulator, getAuth } from 'firebase/auth'
+import { getStorage, connectStorageEmulator } from 'firebase/storage'
+import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'
+
+export const firebaseApp = !getApps().length
+ ? initializeApp(firebaseConfig)
+ : getApp()
+
+const getFirebaseAuth = () => {
+ const firebaseAuth = getAuth(firebaseApp)
+ if (process.env.NODE_ENV !== 'production') {
+ connectAuthEmulator(firebaseAuth, 'http://127.0.0.1:9099', {
+ disableWarnings: true,
+ })
+ }
+ return firebaseAuth
+}
+
+export const auth = firebaseApp ? getFirebaseAuth() : undefined
+
+const getFirebaseStorage = () => {
+ const firebaseStorage = getStorage(firebaseApp)
+ if (process.env.NODE_ENV !== 'production') {
+ connectStorageEmulator(firebaseStorage, '127.0.0.1', 9199)
+ }
+ return firebaseStorage
+}
+
+export const storage = firebaseApp ? getFirebaseStorage() : undefined
+
+const getFirebaseFirestore = () => {
+ const firestoreDb = getFirestore(firebaseApp)
+ if (process.env.NODE_ENV !== 'production') {
+ connectFirestoreEmulator(firestoreDb, '127.0.0.1', 8080)
+ }
+ return firestoreDb
+}
+
+export const db = firebaseApp ? getFirebaseFirestore() : undefined
diff --git a/typescript/skeet-graphql/src/lib/getStatic.ts b/typescript/skeet-graphql/src/lib/getStatic.ts
new file mode 100644
index 000000000000..4180c8753ec8
--- /dev/null
+++ b/typescript/skeet-graphql/src/lib/getStatic.ts
@@ -0,0 +1,84 @@
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import i18nextConfig from '../../next-i18next.config'
+import { GetStaticPropsContext } from 'next'
+import siteConfig from '@/config/site'
+
+type Seo = {
+ pathname: string
+ title: {
+ ja: string
+ en: string
+ }
+ description: {
+ ja: string
+ en: string
+ }
+ img: string | null
+}
+
+export type SeoData = {
+ property?: string
+ name?: string
+ content?: string
+}
+
+export const getI18nPaths = () =>
+ i18nextConfig.i18n.locales.map((lng) => ({
+ params: {
+ locale: lng,
+ },
+ }))
+
+export const getStaticPaths = () => ({
+ fallback: false,
+ paths: getI18nPaths(),
+})
+
+export async function getI18nProps(
+ ctx: GetStaticPropsContext,
+ ns = ['common'],
+ seo: Seo
+) {
+ const locale = ctx?.params?.locale as 'ja' | 'en'
+
+ const { pathname, img } = seo
+ const title = seo.title[locale]
+ const description = seo.description[locale]
+ const siteName =
+ locale === 'ja' ? siteConfig.sitenameJA : siteConfig.sitenameEN
+
+ const ogImage = img
+ ? `https://${siteConfig.domain}${img}`
+ : `https://${siteConfig.domain}/ogp.png`
+
+ const seoData: SeoData[] = [
+ { property: 'og:title', content: `${title} | ${siteName}` },
+ { name: 'twitter:title', content: `${title} | ${siteName}` },
+ { name: 'twitter:text:title', content: `${title} | ${siteName}` },
+ { name: 'description', content: `${description}` },
+ { property: 'og:description', content: `${description}` },
+ { name: 'twitter:description', content: `${description}` },
+ {
+ property: 'og:url',
+ content: `https://${siteConfig.domain}/${locale}${pathname}`,
+ },
+ { property: 'og:image', content: ogImage },
+ { property: 'og:image:secure', content: ogImage },
+ { name: 'twitter:image', content: ogImage },
+ ]
+
+ const props = {
+ title: `${title} | ${siteName}`,
+ seoData,
+ ...(await serverSideTranslations(locale, ns)),
+ }
+ return props
+}
+
+export function makeStaticProps(ns: string[] = [], seo: Seo) {
+ return async function getStaticProps(ctx: GetStaticPropsContext) {
+ return {
+ props: await getI18nProps(ctx, ns, seo),
+ }
+ }
+}
diff --git a/typescript/skeet-graphql/src/lib/languageDetector.ts b/typescript/skeet-graphql/src/lib/languageDetector.ts
new file mode 100644
index 000000000000..7bb6578c2e84
--- /dev/null
+++ b/typescript/skeet-graphql/src/lib/languageDetector.ts
@@ -0,0 +1,7 @@
+import languageDetector from 'next-language-detector'
+import i18nextConfig from '../../next-i18next.config'
+
+export default languageDetector({
+ supportedLngs: i18nextConfig.i18n.locales,
+ fallbackLng: i18nextConfig.i18n.defaultLocale,
+})
diff --git a/typescript/skeet-graphql/src/lib/relayEnvironment.ts b/typescript/skeet-graphql/src/lib/relayEnvironment.ts
new file mode 100755
index 000000000000..e8214884477e
--- /dev/null
+++ b/typescript/skeet-graphql/src/lib/relayEnvironment.ts
@@ -0,0 +1,50 @@
+import 'regenerator-runtime/runtime'
+import { Environment, RecordSource, Store } from 'relay-runtime'
+import {
+ authMiddleware,
+ cacheMiddleware,
+ RelayNetworkLayer,
+ urlMiddleware,
+ retryMiddleware,
+} from 'react-relay-network-modern'
+
+import skeetCloudConfig from '@root/skeet-cloud.config.json'
+import { auth } from './firebase'
+
+const source = new RecordSource()
+const store = new Store(source)
+
+let storeEnvironment: Environment | null = null
+
+const getToken = async () => {
+ return (await auth?.currentUser?.getIdToken()) ?? ''
+}
+
+export const createEnvironment: () => Environment = () => {
+ if (storeEnvironment) return storeEnvironment
+ storeEnvironment = new Environment({
+ store,
+ network: new RelayNetworkLayer([
+ cacheMiddleware({
+ size: 1000,
+ ttl: 15 * 60 * 1000,
+ allowMutations: true,
+ allowFormData: true,
+ clearOnMutation: true,
+ }),
+ typeof window !== 'undefined'
+ ? authMiddleware({
+ token: getToken,
+ })
+ : null,
+ retryMiddleware(),
+ urlMiddleware({
+ url: () =>
+ process.env.NODE_ENV !== 'production'
+ ? 'http://localhost:3000/graphql'
+ : `${skeetCloudConfig.cloudRun.url}/graphql`,
+ }),
+ ]),
+ })
+ return storeEnvironment
+}
diff --git a/typescript/skeet-graphql/src/lib/skeet.ts b/typescript/skeet-graphql/src/lib/skeet.ts
new file mode 100644
index 000000000000..0ad870e708f8
--- /dev/null
+++ b/typescript/skeet-graphql/src/lib/skeet.ts
@@ -0,0 +1,40 @@
+import skeetCloudConfig from '@root/skeet-cloud.config.json'
+import { toKebabCase } from '@/utils/character'
+import { auth } from '@/lib/firebase'
+import { signOut } from 'firebase/auth'
+
+export const fetchSkeetFunctions = async (
+ functionName: string,
+ methodName: string,
+ params: T
+) => {
+ try {
+ const url =
+ process.env.NODE_ENV === 'production'
+ ? `https://${
+ skeetCloudConfig.app.functionsDomain
+ }/${functionName}/${toKebabCase(methodName)}`
+ : `http://127.0.0.1:5001/${skeetCloudConfig.app.projectId}/${skeetCloudConfig.app.region}/${methodName}`
+ const skeetToken = await auth?.currentUser?.getIdToken()
+ const res = await fetch(`${url}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${skeetToken}`,
+ },
+ body: JSON.stringify(params),
+ })
+ return res
+ } catch (err) {
+ console.error(err)
+ if (
+ err instanceof Error &&
+ (err.message.includes('Firebase ID token has expired.') ||
+ err.message.includes('Error: getUserAuth'))
+ ) {
+ if (auth) {
+ signOut(auth)
+ }
+ }
+ }
+}
diff --git a/typescript/skeet-graphql/src/pages/404.tsx b/typescript/skeet-graphql/src/pages/404.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/404.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/[locale]/404.tsx b/typescript/skeet-graphql/src/pages/[locale]/404.tsx
new file mode 100644
index 000000000000..f404ceff3d3f
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/404.tsx
@@ -0,0 +1,57 @@
+import type { ReactElement } from 'react'
+import DefaultLayout from '@/layouts/default/DefaultLayout'
+
+import { useTranslation } from 'next-i18next'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import Link from '@/components/routing/Link'
+
+const seo = {
+ pathname: '/404',
+ title: {
+ ja: '404',
+ en: '404',
+ },
+ description: {
+ ja: 'ページが見つかりませんでした',
+ en: 'Not found',
+ },
+ img: null,
+}
+
+const getStaticProps = makeStaticProps(['common'], seo)
+export { getStaticPaths, getStaticProps }
+
+export default function Custom404() {
+ const { t } = useTranslation(['common'])
+ return (
+ <>
+
+
+
+
+ 404
+
+
+ {t('404title')}
+
+
+ {t('404body')}
+
+
+
+ {t('backToTop')}
+
+
+
+
+
+ >
+ )
+}
+
+Custom404.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/action.tsx b/typescript/skeet-graphql/src/pages/[locale]/action.tsx
new file mode 100644
index 000000000000..ed046656e520
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/action.tsx
@@ -0,0 +1,56 @@
+import { ReactElement, useMemo } from 'react'
+
+import ResetPasswordAction from '@/components/pages/action/ResetPasswordAction'
+import VerifyEmailAction from '@/components/pages/action/VerifyEmailAction'
+import InvalidParamsError from '@/components/error/InvalidParamsError'
+import { useRouter } from 'next/router'
+import siteConfig from '@/config/site'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import ActionLayout from '@/layouts/action/ActionLayout'
+
+const seo = {
+ pathname: '/action',
+ title: {
+ ja: 'アクション',
+ en: 'Action',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+export default function Action() {
+ const router = useRouter()
+ const mode = useMemo(
+ () => (router.query.mode as string) ?? undefined,
+ [router]
+ )
+ const oobCode = useMemo(
+ () => (router.query.oobCode as string) ?? undefined,
+ [router]
+ )
+
+ if (!mode || !oobCode) {
+ return
+ }
+
+ if (mode !== 'resetPassword' && mode !== 'verifyEmail') {
+ return
+ }
+
+ return (
+ <>
+ {mode === 'resetPassword' && }
+ {mode === 'verifyEmail' && }
+ >
+ )
+}
+
+const getStaticProps = makeStaticProps(['common', 'auth'], seo)
+export { getStaticPaths, getStaticProps }
+
+Action.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/auth/check-email.tsx b/typescript/skeet-graphql/src/pages/[locale]/auth/check-email.tsx
new file mode 100644
index 000000000000..7daf750670e7
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/auth/check-email.tsx
@@ -0,0 +1,33 @@
+import { ReactElement } from 'react'
+import AuthLayout from '@/layouts/auth/AuthLayout'
+import siteConfig from '@/config/site'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import CheckEmailScreen from '@/components/pages/auth/CheckEmailScreen'
+
+const seo = {
+ pathname: '/auth/check-email',
+ title: {
+ ja: 'Emailを確認してください',
+ en: 'Check your email',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+const getStaticProps = makeStaticProps(['common', 'auth'], seo)
+export { getStaticPaths, getStaticProps }
+
+export default function CheckEmail() {
+ return (
+ <>
+
+ >
+ )
+}
+
+CheckEmail.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/auth/login.tsx b/typescript/skeet-graphql/src/pages/[locale]/auth/login.tsx
new file mode 100644
index 000000000000..4210c3a8d7e8
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/auth/login.tsx
@@ -0,0 +1,33 @@
+import { ReactElement } from 'react'
+import AuthLayout from '@/layouts/auth/AuthLayout'
+import siteConfig from '@/config/site'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import LoginScreen from '@/components/pages/auth/LoginScreen'
+
+const seo = {
+ pathname: '/auth/login',
+ title: {
+ ja: 'ログイン',
+ en: 'Sign in',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+const getStaticProps = makeStaticProps(['common', 'auth'], seo)
+export { getStaticPaths, getStaticProps }
+
+export default function Login() {
+ return (
+ <>
+
+ >
+ )
+}
+
+Login.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/auth/register.tsx b/typescript/skeet-graphql/src/pages/[locale]/auth/register.tsx
new file mode 100644
index 000000000000..8594ea4af899
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/auth/register.tsx
@@ -0,0 +1,33 @@
+import { ReactElement } from 'react'
+import AuthLayout from '@/layouts/auth/AuthLayout'
+import siteConfig from '@/config/site'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import RegisterScreen from '@/components/pages/auth/RegisterScreen'
+
+const seo = {
+ pathname: '/auth/register',
+ title: {
+ ja: 'アカウント登録',
+ en: 'Register an account',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+const getStaticProps = makeStaticProps(['common', 'auth'], seo)
+export { getStaticPaths, getStaticProps }
+
+export default function Register() {
+ return (
+ <>
+
+ >
+ )
+}
+
+Register.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/auth/reset-password.tsx b/typescript/skeet-graphql/src/pages/[locale]/auth/reset-password.tsx
new file mode 100644
index 000000000000..bf9577ed12e4
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/auth/reset-password.tsx
@@ -0,0 +1,33 @@
+import { ReactElement } from 'react'
+import AuthLayout from '@/layouts/auth/AuthLayout'
+import siteConfig from '@/config/site'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import ResetPasswordScreen from '@/components/pages/auth/ResetPasswordScreen'
+
+const seo = {
+ pathname: '/auth/reset-password',
+ title: {
+ ja: 'パスワードリセット',
+ en: 'Reset your password',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+const getStaticProps = makeStaticProps(['common', 'auth'], seo)
+export { getStaticPaths, getStaticProps }
+
+export default function ResetPassword() {
+ return (
+ <>
+
+ >
+ )
+}
+
+ResetPassword.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/doc/[...slug].tsx b/typescript/skeet-graphql/src/pages/[locale]/doc/[...slug].tsx
new file mode 100644
index 000000000000..039e63fc0cee
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/doc/[...slug].tsx
@@ -0,0 +1,124 @@
+import type { ReactElement } from 'react'
+import type {
+ GetStaticProps,
+ InferGetStaticPropsType,
+ GetStaticPaths,
+} from 'next'
+import { unified } from 'unified'
+import remarkParse from 'remark-parse'
+import remark2Rehype from 'remark-rehype'
+import rehypeHighlight from 'rehype-highlight'
+import rehypeStringify from 'rehype-stringify'
+import rehypeCodeTitles from 'rehype-code-titles'
+import remarkSlug from 'remark-slug'
+import remarkGfm from 'remark-gfm'
+import remarkDirective from 'remark-directive'
+import remarkExternalLinks from 'remark-external-links'
+
+import { getAllArticles, getArticleBySlug } from '@/utils/article'
+import DocLayout from '@/layouts/doc/DocLayout'
+import { getI18nProps } from '@/lib/getStatic'
+import DocContents from '@/components/articles/doc/DocContents'
+
+const articleDirName = 'doc'
+
+export default function Doc({
+ article,
+ articleHtml,
+}: InferGetStaticPropsType) {
+ return (
+ <>
+
+ >
+ )
+}
+
+Doc.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+const articleDirPrefix = `articles/${articleDirName}/`
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ const { params } = ctx
+ if (params?.slug == null)
+ return {
+ props: {
+ article: {
+ title: '',
+ description: '',
+ content: '',
+ },
+ },
+ }
+
+ const article = getArticleBySlug(
+ typeof params.slug == 'string' ? [params.slug] : params.slug,
+ ['title', 'description', 'content'],
+ articleDirPrefix,
+ (params.locale as string) ?? 'en'
+ )
+ console.log(article.content)
+
+ const articleHtml = await unified()
+ .use(remarkParse)
+ .use(remarkDirective)
+ .use(remarkGfm)
+ .use(remarkSlug)
+ .use(remarkExternalLinks, {
+ target: '_blank',
+ rel: ['noopener noreferrer'],
+ })
+ .use(remark2Rehype)
+ .use(rehypeCodeTitles)
+ .use(rehypeHighlight)
+ .use(rehypeStringify)
+ .process(article.content as string)
+
+ const slug = params.slug as string[]
+ const pathname = `/${articleDirName}/${slug.join('/')}`
+
+ const seo = {
+ pathname,
+ title: {
+ ja: article.title as string,
+ en: article.title as string,
+ },
+ description: {
+ ja: `${article.description}`,
+ en: `${article.description}`,
+ },
+ img: null,
+ }
+
+ return {
+ props: {
+ article,
+ articleHtml: articleHtml.value,
+ ...(await getI18nProps(ctx, ['common', articleDirName], seo)),
+ },
+ }
+}
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const articles = getAllArticles(articleDirPrefix)
+ return {
+ paths: articles.map((article) => {
+ if (article[0] === 'ja') {
+ return {
+ params: {
+ slug: article.filter((_, index) => index !== 0),
+ locale: 'ja',
+ },
+ }
+ }
+ return {
+ params: {
+ slug: article.filter((_, index) => index !== 0),
+ locale: 'en',
+ },
+ }
+ }),
+ fallback: false,
+ }
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/doc/index.tsx b/typescript/skeet-graphql/src/pages/[locale]/doc/index.tsx
new file mode 100644
index 000000000000..a08b1bbb03b8
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/doc/index.tsx
@@ -0,0 +1,43 @@
+import type { GetStaticProps, InferGetStaticPropsType } from 'next'
+import type { ReactElement } from 'react'
+import DocLayout from '@/layouts/doc/DocLayout'
+import { getStaticPaths } from '@/lib/getStatic'
+import { getI18nProps } from '@/lib/getStatic'
+import DocIndex from '@/components/articles/doc/DocIndex'
+
+const articleDirName = 'doc'
+
+const seo = {
+ pathname: `/${articleDirName}`,
+ title: {
+ ja: 'ドキュメントトップページ',
+ en: 'Doc top page',
+ },
+ description: {
+ ja: 'Next.js Template ドキュメントトップページ',
+ en: 'Next.js Doc top page',
+ },
+ img: null,
+}
+
+export default function DocIndexPage() {
+ return (
+ <>
+
+ >
+ )
+}
+
+DocIndexPage.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ return {
+ props: {
+ ...(await getI18nProps(ctx, ['common', articleDirName], seo)),
+ },
+ }
+}
+
+export { getStaticPaths }
diff --git a/typescript/skeet-graphql/src/pages/[locale]/index.tsx b/typescript/skeet-graphql/src/pages/[locale]/index.tsx
new file mode 100644
index 000000000000..44fce4412a3b
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/index.tsx
@@ -0,0 +1,78 @@
+import { ReactElement } from 'react'
+import { getStaticPaths } from '@/lib/getStatic'
+import DefaultLayout from '@/layouts/default/DefaultLayout'
+import HeroRow from '@/components/pages/home/HeroRow'
+import DiscordRow from '@/components/pages/common/DiscordRow'
+import siteConfig from '@/config/site'
+import { getAllArticles, getArticleBySlug } from '@/utils/article'
+import { getI18nProps } from '@/lib/getStatic'
+import TopNewsRow from '@/components/articles/news/TopNewsRow'
+import { GetStaticProps, InferGetStaticPropsType } from 'next'
+
+const articleDirName = 'news'
+
+const seo = {
+ pathname: '/',
+ title: {
+ ja: 'トップページ',
+ en: 'Top page',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+export default function Home({
+ urls,
+ articles,
+}: InferGetStaticPropsType) {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+Home.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+const articleDirPrefix = `articles/${articleDirName}/`
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ const slugs = getAllArticles(articleDirPrefix).filter(
+ (article) => article[0] !== 'ja'
+ )
+ const articles = slugs
+ .map((slug) =>
+ getArticleBySlug(
+ slug.filter((_, index) => index !== 0),
+ ['title', 'category', 'thumbnail', 'date', 'content'],
+ articleDirPrefix,
+ (ctx.params?.locale as string) ?? 'en'
+ )
+ )
+ .reverse()
+ .slice(0, 4)
+
+ const urls = slugs
+ .map(
+ (slug) => `/${articleDirName}/${slug[1]}/${slug[2]}/${slug[3]}/${slug[4]}`
+ )
+ .reverse()
+ .slice(0, 4)
+
+ return {
+ props: {
+ urls,
+ articles,
+ ...(await getI18nProps(ctx, ['common', 'home', articleDirName], seo)),
+ },
+ }
+}
+
+export { getStaticPaths }
diff --git a/typescript/skeet-graphql/src/pages/[locale]/legal/[...slug].tsx b/typescript/skeet-graphql/src/pages/[locale]/legal/[...slug].tsx
new file mode 100644
index 000000000000..0e96e20e37cc
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/legal/[...slug].tsx
@@ -0,0 +1,119 @@
+import type { ReactElement } from 'react'
+import type {
+ GetStaticProps,
+ InferGetStaticPropsType,
+ GetStaticPaths,
+} from 'next'
+import { unified } from 'unified'
+import remarkParse from 'remark-parse'
+import remark2Rehype from 'remark-rehype'
+import rehypeHighlight from 'rehype-highlight'
+import rehypeStringify from 'rehype-stringify'
+import remarkSlug from 'remark-slug'
+import remarkGfm from 'remark-gfm'
+import remarkDirective from 'remark-directive'
+import remarkExternalLinks from 'remark-external-links'
+
+import { getAllArticles, getArticleBySlug } from '@/utils/article'
+import DefaultLayout from '@/layouts/default/DefaultLayout'
+import { getI18nProps } from '@/lib/getStatic'
+import LegalContents from '@/components/articles/legal/LegalContents'
+
+const articleDirName = 'legal'
+
+export default function Legal({
+ article,
+ articleHtml,
+}: InferGetStaticPropsType) {
+ return (
+ <>
+
+ >
+ )
+}
+
+Legal.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+const articleDirPrefix = `articles/${articleDirName}/`
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ const { params } = ctx
+
+ if (params?.slug == null)
+ return {
+ props: {
+ article: {
+ title: '',
+ description: '',
+ content: '',
+ },
+ },
+ }
+
+ const article = getArticleBySlug(
+ typeof params.slug == 'string' ? [params.slug] : params.slug,
+ ['title', 'description', 'content', 'id'],
+ articleDirPrefix,
+ (params.locale as string) ?? 'en'
+ )
+
+ const articleHtml = await unified()
+ .use(remarkParse)
+ .use(remarkDirective)
+ .use(remarkGfm)
+ .use(remarkSlug)
+ .use(remarkExternalLinks, {
+ target: '_blank',
+ rel: ['noopener noreferrer'],
+ })
+ .use(remark2Rehype)
+ .use(rehypeHighlight)
+ .use(rehypeStringify)
+ .process(article.content as string)
+
+ const seo = {
+ pathname: `/${articleDirName}/${article.id}`,
+ title: {
+ ja: article.title as string,
+ en: article.title as string,
+ },
+ description: {
+ ja: article.description as string,
+ en: article.description as string,
+ },
+ img: null,
+ }
+
+ return {
+ props: {
+ article,
+ articleHtml: articleHtml.value,
+ ...(await getI18nProps(ctx, ['common', articleDirName], seo)),
+ },
+ }
+}
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const articles = getAllArticles(articleDirPrefix)
+ return {
+ paths: articles.map((article) => {
+ if (article[0] === 'ja') {
+ return {
+ params: {
+ slug: article.filter((_, index) => index !== 0),
+ locale: 'ja',
+ },
+ }
+ }
+ return {
+ params: {
+ slug: article.filter((_, index) => index !== 0),
+ locale: 'en',
+ },
+ }
+ }),
+ fallback: false,
+ }
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/news/[...slug].tsx b/typescript/skeet-graphql/src/pages/[locale]/news/[...slug].tsx
new file mode 100644
index 000000000000..b056bc6fe475
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/news/[...slug].tsx
@@ -0,0 +1,153 @@
+import type { ReactElement } from 'react'
+import type {
+ GetStaticProps,
+ InferGetStaticPropsType,
+ GetStaticPaths,
+} from 'next'
+import { unified } from 'unified'
+import remarkParse from 'remark-parse'
+import remark2Rehype from 'remark-rehype'
+import rehypeHighlight from 'rehype-highlight'
+import rehypeStringify from 'rehype-stringify'
+import rehypeCodeTitles from 'rehype-code-titles'
+import remarkSlug from 'remark-slug'
+import remarkGfm from 'remark-gfm'
+import remarkDirective from 'remark-directive'
+import remarkExternalLinks from 'remark-external-links'
+
+import { getAllArticles, getArticleBySlug } from '@/utils/article'
+import DefaultLayout from '@/layouts/default/DefaultLayout'
+import { getI18nProps } from '@/lib/getStatic'
+import NewsContents from '@/components/articles/news/NewsContents'
+import NewsPageIndex from '@/components/articles/news/NewsPageIndex'
+
+const articleDirName = 'news'
+
+export default function News({
+ article,
+ articleHtml,
+ urls,
+ articles,
+}: InferGetStaticPropsType) {
+ return (
+ <>
+
+
+ >
+ )
+}
+
+News.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+const articleDirPrefix = `articles/${articleDirName}/`
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ const { params } = ctx
+ if (params?.slug == null)
+ return {
+ props: {
+ article: {
+ title: '',
+ category: '',
+ thumbnail: '',
+ content: '',
+ date: '',
+ },
+ },
+ }
+
+ const article = getArticleBySlug(
+ typeof params.slug == 'string' ? [params.slug] : params.slug,
+ ['title', 'category', 'thumbnail', 'content', 'date', 'id'],
+ articleDirPrefix,
+ (params.locale as string) ?? 'en'
+ )
+
+ const articleHtml = await unified()
+ .use(remarkParse)
+ .use(remarkDirective)
+ .use(remarkGfm)
+ .use(remarkSlug)
+ .use(remarkExternalLinks, {
+ target: '_blank',
+ rel: ['noopener noreferrer'],
+ })
+ .use(remark2Rehype)
+ .use(rehypeCodeTitles)
+ .use(rehypeHighlight)
+ .use(rehypeStringify)
+ .process(article.content as string)
+
+ const slugs = getAllArticles(articleDirPrefix).filter(
+ (article) => article[0] !== 'ja'
+ )
+ const articles = slugs
+ .map((slug) =>
+ getArticleBySlug(
+ slug.filter((_, index) => index !== 0),
+ ['title', 'category', 'thumbnail', 'date', 'content'],
+ articleDirPrefix,
+ (ctx.params?.locale as string) ?? 'en'
+ )
+ )
+ .reverse()
+ .slice(0, 3)
+
+ const urls = slugs
+ .map(
+ (slug) => `/${articleDirName}/${slug[1]}/${slug[2]}/${slug[3]}/${slug[4]}`
+ )
+ .reverse()
+ .slice(0, 3)
+
+ const slug = params.slug as string[]
+ const pathname = `/${articleDirName}/${slug.join('/')}`
+
+ const seo = {
+ pathname,
+ title: {
+ ja: article.title as string,
+ en: article.title as string,
+ },
+ description: {
+ ja: `${article.content.slice(0, 120)} ...`,
+ en: `${article.content.slice(0, 120)} ...`,
+ },
+ img: article.thumbnail as string,
+ }
+
+ return {
+ props: {
+ urls,
+ articles,
+ article,
+ articleHtml: articleHtml.value,
+ ...(await getI18nProps(ctx, ['common', articleDirName], seo)),
+ },
+ }
+}
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const articles = getAllArticles(articleDirPrefix)
+ return {
+ paths: articles.map((article) => {
+ if (article[0] === 'ja') {
+ return {
+ params: {
+ slug: article.filter((_, index) => index !== 0),
+ locale: 'ja',
+ },
+ }
+ }
+ return {
+ params: {
+ slug: article.filter((_, index) => index !== 0),
+ locale: 'en',
+ },
+ }
+ }),
+ fallback: false,
+ }
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/news/index.tsx b/typescript/skeet-graphql/src/pages/[locale]/news/index.tsx
new file mode 100644
index 000000000000..2a8d5c79dd23
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/news/index.tsx
@@ -0,0 +1,72 @@
+import type { GetStaticProps, InferGetStaticPropsType } from 'next'
+import type { ReactElement } from 'react'
+import DefaultLayout from '@/layouts/default/DefaultLayout'
+import { getStaticPaths } from '@/lib/getStatic'
+
+import { getAllArticles, getArticleBySlug } from '@/utils/article'
+import { getI18nProps } from '@/lib/getStatic'
+import NewsIndex from '@/components/articles/news/NewsIndex'
+
+const articleDirName = 'news'
+
+const seo = {
+ pathname: `/${articleDirName}`,
+ title: {
+ ja: 'ニュース',
+ en: 'News',
+ },
+ description: {
+ ja: 'ニュース',
+ en: 'News',
+ },
+ img: null,
+}
+
+export default function NewsIndexPage({
+ urls,
+ articles,
+}: InferGetStaticPropsType) {
+ return (
+ <>
+
+ >
+ )
+}
+
+NewsIndexPage.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+const articleDirPrefix = `articles/${articleDirName}/`
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ const slugs = getAllArticles(articleDirPrefix).filter(
+ (article) => article[0] !== 'ja'
+ )
+ const articles = slugs
+ .map((slug) =>
+ getArticleBySlug(
+ slug.filter((_, index) => index !== 0),
+ ['title', 'category', 'thumbnail', 'date', 'content'],
+ articleDirPrefix,
+ (ctx.params?.locale as string) ?? 'en'
+ )
+ )
+ .reverse()
+
+ const urls = slugs
+ .map(
+ (slug) => `/${articleDirName}/${slug[1]}/${slug[2]}/${slug[3]}/${slug[4]}`
+ )
+ .reverse()
+
+ return {
+ props: {
+ urls,
+ articles,
+ ...(await getI18nProps(ctx, ['common', articleDirName], seo)),
+ },
+ }
+}
+
+export { getStaticPaths }
diff --git a/typescript/skeet-graphql/src/pages/[locale]/user/chat.tsx b/typescript/skeet-graphql/src/pages/[locale]/user/chat.tsx
new file mode 100644
index 000000000000..1dff999df2ad
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/user/chat.tsx
@@ -0,0 +1,79 @@
+import { ReactElement, Suspense, useCallback, useEffect } from 'react'
+import UserLayout from '@/layouts/user/UserLayout'
+import siteConfig from '@/config/site'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import ChatScreen, {
+ chatScreenQuery,
+} from '@/components/pages/user/chat/ChatScreen'
+import UserScreenLoading from '@/components/loading/UserScreenLoading'
+import UserScreenErrorBoundary from '@/components/error/UserScreenErrorBoundary'
+import {
+ ChatScreenQuery,
+ ChatScreenQuery$variables,
+} from '@/__generated__/ChatScreenQuery.graphql'
+import { useQueryLoader } from 'react-relay'
+import { sleep } from '@/utils/time'
+import RefetchChat from '@/components/pages/user/chat/RefetchChat'
+
+const seo = {
+ pathname: '/user/chat',
+ title: {
+ ja: 'AIチャット',
+ en: 'AI Chat',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+const getStaticProps = makeStaticProps(['common', 'user', 'chat'], seo)
+export { getStaticPaths, getStaticProps }
+
+export default function Chat() {
+ const [queryReference, loadQuery] =
+ useQueryLoader(chatScreenQuery)
+
+ useEffect(() => {
+ ;(async () => {
+ await sleep(250)
+ loadQuery({
+ first: 15,
+ after: null,
+ })
+ })()
+ }, [loadQuery])
+
+ const refetch = useCallback(
+ (variables: ChatScreenQuery$variables) => {
+ loadQuery(variables, { fetchPolicy: 'network-only' })
+ },
+ [loadQuery]
+ )
+
+ if (queryReference == null) {
+ return (
+ <>
+
+ >
+ )
+ }
+ return (
+ <>
+
+ }>
+ }
+ >
+
+
+
+
+ >
+ )
+}
+
+Chat.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/[locale]/user/settings.tsx b/typescript/skeet-graphql/src/pages/[locale]/user/settings.tsx
new file mode 100644
index 000000000000..366fa045c834
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/[locale]/user/settings.tsx
@@ -0,0 +1,33 @@
+import { ReactElement } from 'react'
+import UserLayout from '@/layouts/user/UserLayout'
+import siteConfig from '@/config/site'
+import { getStaticPaths, makeStaticProps } from '@/lib/getStatic'
+import SettingsScreen from '@/components/pages/user/settings/SettingsScreen'
+
+const seo = {
+ pathname: '/user/settings',
+ title: {
+ ja: 'ユーザー設定',
+ en: 'User Settings',
+ },
+ description: {
+ ja: siteConfig.descriptionJA,
+ en: siteConfig.descriptionEN,
+ },
+ img: null,
+}
+
+const getStaticProps = makeStaticProps(['common', 'user', 'settings'], seo)
+export { getStaticPaths, getStaticProps }
+
+export default function Settings() {
+ return (
+ <>
+
+ >
+ )
+}
+
+Settings.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
diff --git a/typescript/skeet-graphql/src/pages/_app.tsx b/typescript/skeet-graphql/src/pages/_app.tsx
new file mode 100644
index 000000000000..52cf65ca52b1
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/_app.tsx
@@ -0,0 +1,50 @@
+import '@/lib/firebase'
+import { type ReactElement, type ReactNode } from 'react'
+import type { NextPage } from 'next'
+import type { AppProps } from 'next/app'
+import { appWithTranslation } from 'next-i18next'
+import Layout from '@/layouts/Layout'
+import { RecoilRoot } from 'recoil'
+import Head from 'next/head'
+import type { SeoData } from '@/lib/getStatic'
+import 'highlight.js/styles/github-dark.css'
+import '@/assets/styles/globals.css'
+import { ThemeProvider } from 'next-themes'
+import { RelayEnvironmentProvider } from 'react-relay'
+import { createEnvironment } from '@/lib/relayEnvironment'
+
+export type NextPageWithLayout = NextPage & {
+ getLayout?: (page: ReactElement) => ReactNode
+}
+
+export type AppPropsWithLayout = AppProps & {
+ Component: NextPageWithLayout
+}
+
+function MyApp({ Component, pageProps, router }: AppProps) {
+ return (
+ <>
+
+ {pageProps.title}
+ {pageProps.seoData?.map((seo: SeoData, index: number) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default appWithTranslation(MyApp)
diff --git a/typescript/skeet-graphql/src/pages/_document.tsx b/typescript/skeet-graphql/src/pages/_document.tsx
new file mode 100644
index 000000000000..b52ca4fe3bd6
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/_document.tsx
@@ -0,0 +1,74 @@
+import Document, { Html, Head, Main, NextScript } from 'next/document'
+import siteConfig from '@/config/site'
+
+export default class MyDocument extends Document {
+ render() {
+ const { locale } = this.props.__NEXT_DATA__.query
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {locale === 'ja' ? (
+
+ ) : (
+
+ )}
+
+
+ {locale === 'ja' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/typescript/skeet-graphql/src/pages/action.tsx b/typescript/skeet-graphql/src/pages/action.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/action.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/auth/login.tsx b/typescript/skeet-graphql/src/pages/auth/login.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/auth/login.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/auth/register.tsx b/typescript/skeet-graphql/src/pages/auth/register.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/auth/register.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/doc/[...slug].tsx b/typescript/skeet-graphql/src/pages/doc/[...slug].tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/doc/[...slug].tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/doc/index.tsx b/typescript/skeet-graphql/src/pages/doc/index.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/doc/index.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/index.tsx b/typescript/skeet-graphql/src/pages/index.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/index.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/legal/[...slug].tsx b/typescript/skeet-graphql/src/pages/legal/[...slug].tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/legal/[...slug].tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/news/[...slug].tsx b/typescript/skeet-graphql/src/pages/news/[...slug].tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/news/[...slug].tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/news/index.tsx b/typescript/skeet-graphql/src/pages/news/index.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/news/index.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/user/chat.tsx b/typescript/skeet-graphql/src/pages/user/chat.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/user/chat.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/pages/user/settings.tsx b/typescript/skeet-graphql/src/pages/user/settings.tsx
new file mode 100644
index 000000000000..b53a0a69aa7c
--- /dev/null
+++ b/typescript/skeet-graphql/src/pages/user/settings.tsx
@@ -0,0 +1,2 @@
+import Redirect from '@/components/routing/Redirect'
+export default Redirect
diff --git a/typescript/skeet-graphql/src/store/policy.ts b/typescript/skeet-graphql/src/store/policy.ts
new file mode 100644
index 000000000000..b3dd3e8e4069
--- /dev/null
+++ b/typescript/skeet-graphql/src/store/policy.ts
@@ -0,0 +1,11 @@
+import { atom } from 'recoil'
+import { recoilPersist } from 'recoil-persist'
+
+const { persistAtom } = recoilPersist()
+type PolicyAgreedState = boolean
+
+export const policyAgreedState = atom({
+ key: 'policyAgreedState',
+ default: false,
+ effects_UNSTABLE: [persistAtom],
+})
diff --git a/typescript/skeet-graphql/src/store/toasts.ts b/typescript/skeet-graphql/src/store/toasts.ts
new file mode 100644
index 000000000000..d119d15f65d2
--- /dev/null
+++ b/typescript/skeet-graphql/src/store/toasts.ts
@@ -0,0 +1,15 @@
+import { atom } from 'recoil'
+
+export type Toast = {
+ title: string
+ description: string
+ type: 'success' | 'error' | 'warning' | 'info'
+ createdAt: number
+}
+
+export type Toasts = Toast[]
+
+export const toastsState = atom({
+ key: 'toasts',
+ default: [],
+})
diff --git a/typescript/skeet-graphql/src/store/user.ts b/typescript/skeet-graphql/src/store/user.ts
new file mode 100644
index 000000000000..3ca119993762
--- /dev/null
+++ b/typescript/skeet-graphql/src/store/user.ts
@@ -0,0 +1,24 @@
+import { atom } from 'recoil'
+
+export type UserState = {
+ id: string // GraphQL ID
+ uid: string // Firebase UID
+ email: string
+ username: string
+ iconUrl: string
+ emailVerified: boolean
+}
+
+export const defaultUser = {
+ id: '',
+ uid: '',
+ email: '',
+ username: '',
+ iconUrl: '',
+ emailVerified: false,
+}
+
+export const userState = atom({
+ key: 'userState',
+ default: defaultUser,
+})
diff --git a/typescript/skeet-graphql/src/types/article.ts b/typescript/skeet-graphql/src/types/article.ts
new file mode 100644
index 000000000000..f64fb9196f52
--- /dev/null
+++ b/typescript/skeet-graphql/src/types/article.ts
@@ -0,0 +1,33 @@
+export type NewsIndex = {
+ title: string
+ category: string
+ thumbnail: string
+ date: string
+ content: string
+}
+export type NewsContent = {
+ title: string
+ asPath: string
+ category: string
+ thumbnail: string
+ date: string
+ content: string
+}
+
+export type DocIndex = {
+ title: string
+ description: string
+}
+
+export type DocContent = {
+ title: string
+ description: string
+ content: string
+}
+
+export type LegalContent = {
+ title: string
+ asPath: string
+ description: string
+ content: string
+}
diff --git a/typescript/skeet-graphql/src/types/http/openai/createStreamChatMessageParams.ts b/typescript/skeet-graphql/src/types/http/openai/createStreamChatMessageParams.ts
new file mode 100644
index 000000000000..1400d80ccd25
--- /dev/null
+++ b/typescript/skeet-graphql/src/types/http/openai/createStreamChatMessageParams.ts
@@ -0,0 +1,18 @@
+export type CreateStreamChatMessageParams = {
+ chatRoomId: string
+ content: string
+}
+
+export type GetChatRoomParams = {
+ id: string
+}
+
+export type GetChatMessagesParams = {
+ chatRoomId: string
+}
+
+export type CreateChatMessageParams = {
+ role: string
+ content: string
+ chatRoomId: string
+}
diff --git a/typescript/skeet-graphql/src/types/http/openai/rootParams.ts b/typescript/skeet-graphql/src/types/http/openai/rootParams.ts
new file mode 100644
index 000000000000..843ede34c3d5
--- /dev/null
+++ b/typescript/skeet-graphql/src/types/http/openai/rootParams.ts
@@ -0,0 +1,3 @@
+export type RootParams = {
+ name?: string
+}
diff --git a/typescript/skeet-graphql/src/types/models/README.md b/typescript/skeet-graphql/src/types/models/README.md
new file mode 100644
index 000000000000..01c5bb81c8d9
--- /dev/null
+++ b/typescript/skeet-graphql/src/types/models/README.md
@@ -0,0 +1 @@
+Skeet creates functions model types files here.
diff --git a/typescript/skeet-graphql/src/utils/article.ts b/typescript/skeet-graphql/src/utils/article.ts
new file mode 100644
index 000000000000..b1e56430a03f
--- /dev/null
+++ b/typescript/skeet-graphql/src/utils/article.ts
@@ -0,0 +1,48 @@
+import fs from 'fs'
+import glob from 'glob'
+import { join } from 'path'
+import matter from 'gray-matter'
+
+export const getArticleBySlug = (
+ slugArray: string[],
+ fields: string[] = [],
+ articleDirPrefix: string,
+ locale: string
+) => {
+ const articlesDirectory = join(process.cwd(), `${articleDirPrefix}/${locale}`)
+ const matchedSlug = slugArray.join('/')
+ const realSlug = matchedSlug.replace(/\.md$/, '')
+ const fullPath = join(articlesDirectory, `${realSlug}.md`)
+ const fileContents = fs.readFileSync(fullPath, 'utf8')
+ const { data, content } = matter(fileContents)
+
+ type Items = {
+ [key: string]: string | string[]
+ }
+
+ const items: Items = {}
+
+ fields.forEach((field) => {
+ if (field === 'content') {
+ items[field] = content
+ }
+
+ if (field === 'date') {
+ const date = slugArray[0] + '.' + slugArray[1] + '.' + slugArray[2]
+ items[field] = date
+ }
+
+ if (data[field]) {
+ items[field] = data[field]
+ }
+ })
+
+ return items
+}
+
+export const getAllArticles = (articleDirPrefix: string) => {
+ const entries = glob.sync(`${articleDirPrefix}/**/*.md`)
+ return entries
+ .map((file) => file.split(articleDirPrefix).pop())
+ .map((slug) => (slug as string).replace(/\.md$/, '').split('/'))
+}
diff --git a/typescript/skeet-graphql/src/utils/character.ts b/typescript/skeet-graphql/src/utils/character.ts
new file mode 100644
index 000000000000..d5f973c59295
--- /dev/null
+++ b/typescript/skeet-graphql/src/utils/character.ts
@@ -0,0 +1,27 @@
+export function toCamelCase(str: string): string {
+ return str
+ .replace(/[-_\s](\w)/g, (_, char) => {
+ return char.toUpperCase()
+ })
+ .replace(/^\w/, (firstChar) => {
+ return firstChar.toLowerCase()
+ })
+}
+
+export function toKebabCase(str: string): string {
+ return str
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
+ .replace(/\s+/g, '-')
+ .replace(/_/g, '-')
+ .toLowerCase()
+}
+
+export function toPascalCase(str: string): string {
+ return str.replace(/[-_\s](\w)|(\w+)/g, (_, char, word) => {
+ if (word) {
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
+ } else {
+ return char.toUpperCase()
+ }
+ })
+}
diff --git a/typescript/skeet-graphql/src/utils/form.ts b/typescript/skeet-graphql/src/utils/form.ts
new file mode 100644
index 000000000000..2264623c6bd1
--- /dev/null
+++ b/typescript/skeet-graphql/src/utils/form.ts
@@ -0,0 +1,18 @@
+import { z } from 'zod'
+
+export const emailSchema = z.string().email()
+export const passwordSchema = z.string().min(8)
+export const usernameSchema = z.string().min(1).max(20)
+export const privacySchema = z.boolean().refine((v) => v === true)
+
+export type GPTModel = 'gpt-3.5-turbo' | 'gpt-4'
+export const allowedGPTModel: GPTModel[] = ['gpt-3.5-turbo', 'gpt-4']
+export const gptModelSchema = z.union([
+ z.literal('gpt-3.5-turbo'),
+ z.literal('gpt-4'),
+])
+
+export const maxTokensSchema = z.number().int().min(100).max(4096)
+export const temperatureSchema = z.number().min(0).max(2)
+export const systemContentSchema = z.string().min(1).max(1000)
+export const chatContentSchema = z.string().min(1).max(100000)
diff --git a/typescript/skeet-graphql/src/utils/time.ts b/typescript/skeet-graphql/src/utils/time.ts
new file mode 100644
index 000000000000..440dc9e7296c
--- /dev/null
+++ b/typescript/skeet-graphql/src/utils/time.ts
@@ -0,0 +1,3 @@
+export const sleep = async (mSec: number) => {
+ await new Promise((resolve) => setTimeout(resolve, mSec))
+}
diff --git a/typescript/skeet-graphql/src/utils/userAction.ts b/typescript/skeet-graphql/src/utils/userAction.ts
new file mode 100644
index 000000000000..0501cdc1661d
--- /dev/null
+++ b/typescript/skeet-graphql/src/utils/userAction.ts
@@ -0,0 +1,5 @@
+export const copyToClipboard = (content: string | null | undefined) => {
+ if (content == null) return false
+ if (navigator == null) return false
+ navigator.clipboard.writeText(content)
+}
diff --git a/typescript/skeet-graphql/storage.rules b/typescript/skeet-graphql/storage.rules
new file mode 100644
index 000000000000..6d9f4028655a
--- /dev/null
+++ b/typescript/skeet-graphql/storage.rules
@@ -0,0 +1,8 @@
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /User/{userId}/profileIcon/{allPaths=**} {
+ allow read: if request.auth != null;
+ allow write: if request.auth.uid == userId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/typescript/skeet-graphql/tailwind.config.js b/typescript/skeet-graphql/tailwind.config.js
new file mode 100644
index 000000000000..1fdbda4ab3ab
--- /dev/null
+++ b/typescript/skeet-graphql/tailwind.config.js
@@ -0,0 +1,25 @@
+/** @type {import('tailwindcss').Config} */
+const { fontFamily } = require('tailwindcss/defaultTheme')
+module.exports = {
+ darkMode: 'class',
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
+ theme: {
+ container: {
+ center: true,
+ },
+ extend: {
+ fontFamily: {
+ sans: ['Outfit', 'Noto Sans JP', ...fontFamily.sans],
+ },
+ colors: {
+ discord: '#5865f2',
+ twitter: '#1da1f2',
+ },
+ },
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ require('@tailwindcss/typography'),
+ require('tailwind-scrollbar-hide'),
+ ],
+}
diff --git a/typescript/skeet-graphql/tsconfig.json b/typescript/skeet-graphql/tsconfig.json
new file mode 100644
index 000000000000..156985da0b91
--- /dev/null
+++ b/typescript/skeet-graphql/tsconfig.json
@@ -0,0 +1,40 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"],
+ "@lib/*": ["lib/*"],
+ "@root/*": ["./*"]
+ }
+ },
+ "ts-node": {
+ "compilerOptions": {
+ "module": "NodeNext"
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": [
+ "node_modules",
+ "out",
+ ".next",
+ "build",
+ "dist",
+ "functions",
+ "graphql"
+ ],
+ "compileOnSave": false
+}