├── .gitignore ├── client ├── src │ ├── utils.tsx │ ├── react-fittext.d.ts │ ├── conditional-wrap.d.ts │ ├── react-app-env.d.ts │ ├── tailwind.css │ ├── App.css │ ├── jost │ │ ├── jost-200-thin-webfont.woff │ │ ├── jost-200-thin-webfont.woff2 │ │ ├── jost-300-light-webfont.woff │ │ ├── jost-400-book-webfont.ttf │ │ ├── jost-400-book-webfont.woff │ │ ├── jost-400-book-webfont.woff2 │ │ ├── jost-600-semi-webfont.woff │ │ ├── jost-600-semi-webfont.woff2 │ │ ├── jost-700-bold-webfont.woff │ │ ├── jost-700-bold-webfont.woff2 │ │ ├── jost-300-light-webfont.woff2 │ │ ├── jost-500-medium-webfont.woff │ │ ├── jost-500-medium-webfont.woff2 │ │ ├── jost-200-thinitalic-webfont.ttf │ │ ├── jost-200-thinitalic-webfont.woff │ │ ├── jost-400-bookitalic-webfont.woff │ │ ├── jost-600-semiitalic-webfont.woff │ │ ├── jost-700-bolditalic-webfont.woff │ │ ├── jost-200-thinitalic-webfont.woff2 │ │ ├── jost-300-lightitalic-webfont.woff │ │ ├── jost-300-lightitalic-webfont.woff2 │ │ ├── jost-400-bookitalic-webfont.woff2 │ │ ├── jost-500-mediumitalic-webfont.woff │ │ ├── jost-600-semiitalic-webfont.woff2 │ │ ├── jost-700-bolditalic-webfont.woff2 │ │ ├── jost-500-mediumitalic-webfont.woff2 │ │ └── jost.css │ ├── logout.tsx │ ├── types │ │ ├── deletemeow.ts │ │ ├── setname.ts │ │ ├── login.ts │ │ ├── meow.ts │ │ ├── register.ts │ │ ├── likemeow.ts │ │ ├── postmeow.ts │ │ ├── getfeed.ts │ │ ├── getuser.ts │ │ └── getmeow.ts │ ├── loader.tsx │ ├── index.tsx │ ├── feed.tsx │ ├── user-state.tsx │ ├── app.tsx │ ├── settings-name.tsx │ ├── header.tsx │ ├── user.tsx │ ├── create-meow.tsx │ ├── serviceWorker.ts │ ├── meow.tsx │ └── login.tsx ├── prettier.config.js ├── public │ ├── _redirects │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── postcss.config.js ├── tailwind.config.js ├── .gitignore ├── types │ └── globalTypes.ts ├── tsconfig.json ├── .eslintrc.js ├── README.md └── package.json ├── server ├── .eslintignore ├── prettier.config.js ├── tsconfig.json ├── resolvers │ ├── auth-payload.ts │ ├── index.ts │ ├── query.ts │ ├── user.ts │ ├── meow.ts │ └── mutation.ts ├── prisma.yml ├── datamodel.prisma ├── docker-compose.yml ├── utils.ts ├── permissions.ts ├── index.ts ├── .eslintrc.js ├── package.json └── .gitignore ├── renovate.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /.netlify 2 | -------------------------------------------------------------------------------- /client/src/utils.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | -------------------------------------------------------------------------------- /server/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | generated/ 4 | -------------------------------------------------------------------------------- /client/src/react-fittext.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-fittext'; 2 | -------------------------------------------------------------------------------- /client/src/conditional-wrap.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'conditional-wrap'; 2 | -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /server/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /client/public/_redirects: -------------------------------------------------------------------------------- 1 | /graphql https://stratus.daz.cat/graphql 200 2 | /* /index.html 200 3 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .transition-height-\.1 { 2 | transition: height 0.1s cubic-bezier(0.4, 0, 0.2, 1); 3 | } 4 | -------------------------------------------------------------------------------- /client/src/jost/jost-200-thin-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-200-thin-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-200-thin-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-200-thin-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-300-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-300-light-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-400-book-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-400-book-webfont.ttf -------------------------------------------------------------------------------- /client/src/jost/jost-400-book-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-400-book-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-400-book-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-400-book-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-600-semi-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-600-semi-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-600-semi-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-600-semi-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-700-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-700-bold-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-700-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-700-bold-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-300-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-300-light-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-500-medium-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-500-medium-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-500-medium-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-500-medium-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-200-thinitalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-200-thinitalic-webfont.ttf -------------------------------------------------------------------------------- /client/src/jost/jost-200-thinitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-200-thinitalic-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-400-bookitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-400-bookitalic-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-600-semiitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-600-semiitalic-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-700-bolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-700-bolditalic-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-200-thinitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-200-thinitalic-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-300-lightitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-300-lightitalic-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-300-lightitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-300-lightitalic-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-400-bookitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-400-bookitalic-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-500-mediumitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-500-mediumitalic-webfont.woff -------------------------------------------------------------------------------- /client/src/jost/jost-600-semiitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-600-semiitalic-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-700-bolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-700-bolditalic-webfont.woff2 -------------------------------------------------------------------------------- /client/src/jost/jost-500-mediumitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ar1a/Catter/HEAD/client/src/jost/jost-500-mediumitalic-webfont.woff2 -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss')('./tailwind.config.js'), 4 | require('autoprefixer') 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "dist", 5 | "lib": ["esnext", "dom"], 6 | "skipLibCheck": true 7 | }, 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /server/resolvers/auth-payload.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from 'nexus'; 2 | 3 | export const AuthPayload = objectType({ 4 | name: 'AuthPayload', 5 | definition(t) { 6 | t.string('token'); 7 | t.field('user', { type: 'User' }); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/logout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | import { useDispatch } from './user-state'; 5 | 6 | export const Logout = () => { 7 | const dispatch = useDispatch(); 8 | dispatch({ type: 'logout' }); 9 | return ; 10 | }; 11 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | fontFamily: { 4 | sans: ['-apple-system', 'Jost', 'sans-serif', '"Noto Color Emoji"'] 5 | }, 6 | extend: { 7 | maxWidth: { 8 | xs: '380px' 9 | } 10 | } 11 | }, 12 | variants: {}, 13 | plugins: [] 14 | }; 15 | -------------------------------------------------------------------------------- /server/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: http://localhost:4466 2 | datamodel: datamodel.prisma 3 | 4 | hooks: 5 | post-deploy: 6 | - yarn nexus-prisma-generate --client ./generated/prisma-client --output ./generated/nexus-prisma 7 | 8 | generate: 9 | - generator: typescript-client 10 | output: ./generated/prisma-client/ 11 | -------------------------------------------------------------------------------- /server/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | import { Query } from './query'; 3 | import { Mutation } from './mutation'; 4 | import { Meow } from './meow'; 5 | import { AuthPayload } from './auth-payload'; 6 | 7 | export const resolvers = { 8 | User, 9 | Query, 10 | Mutation, 11 | Meow, 12 | AuthPayload 13 | }; 14 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/types/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | //============================================================== 6 | // START Enums and Input Objects 7 | //============================================================== 8 | 9 | //============================================================== 10 | // END Enums and Input Objects 11 | //============================================================== 12 | -------------------------------------------------------------------------------- /client/src/types/deletemeow.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: deletemeow 7 | // ==================================================== 8 | 9 | export interface deletemeow_deleteMeow { 10 | __typename: "Meow"; 11 | id: string; 12 | } 13 | 14 | export interface deletemeow { 15 | deleteMeow: deletemeow_deleteMeow | null; 16 | } 17 | 18 | export interface deletemeowVariables { 19 | id: string; 20 | } 21 | -------------------------------------------------------------------------------- /client/src/loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, createStyles } from '@material-ui/styles'; 3 | import { CircularProgress } from '@material-ui/core'; 4 | 5 | const useStyles = makeStyles( 6 | createStyles({ 7 | container: { 8 | display: 'flex', 9 | width: '100%', 10 | justifyContent: 'center', 11 | padding: 16 12 | } 13 | }) 14 | ); 15 | 16 | export const Loader = () => { 17 | const classes = useStyles({}); 18 | return ( 19 |
20 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /client/src/types/setname.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: setname 7 | // ==================================================== 8 | 9 | export interface setname_setName { 10 | __typename: "User"; 11 | id: string; 12 | username: string; 13 | name: string; 14 | } 15 | 16 | export interface setname { 17 | setName: setname_setName; 18 | } 19 | 20 | export interface setnameVariables { 21 | name: string; 22 | } 23 | -------------------------------------------------------------------------------- /server/datamodel.prisma: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! @id 3 | username: String! @unique 4 | name: String! 5 | password: String! 6 | meows: [Meow!]! @relation(name: "MeowRelation") 7 | likes: [Meow!]! @relation(name: "LikeRelation") 8 | } 9 | 10 | type Meow { 11 | id: ID! @id 12 | content: String! 13 | author: User! @relation(link: INLINE, name: "MeowRelation") 14 | createdAt: DateTime! @createdAt 15 | likedBy: [User!]! @relation(link: TABLE, name: "LikeRelation") 16 | replies: [Meow!]! @relation(name: "ReplyRelation") 17 | replyingTo: Meow @relation(name: "ReplyRelation") 18 | } 19 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /server/resolvers/query.ts: -------------------------------------------------------------------------------- 1 | import { prismaObjectType } from 'nexus-prisma'; 2 | 3 | import { Context, getUserId } from '../utils'; 4 | 5 | export const Query = prismaObjectType({ 6 | name: 'Query', 7 | definition(t) { 8 | t.prismaFields(['meow', 'user']); 9 | t.list.field('feed', { 10 | type: 'Meow', 11 | resolve: (_, __, ctx: Context) => 12 | ctx.prisma.meows({ orderBy: 'createdAt_DESC' }) 13 | }); 14 | 15 | t.field('me', { 16 | type: 'User', 17 | resolve: (_, __, ctx: Context) => { 18 | const id = getUserId(ctx); 19 | return ctx.prisma.user({ id }); 20 | } 21 | }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/types/login.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: login 7 | // ==================================================== 8 | 9 | export interface login_login_user { 10 | __typename: "User"; 11 | id: string; 12 | } 13 | 14 | export interface login_login { 15 | __typename: "AuthPayload"; 16 | token: string; 17 | user: login_login_user; 18 | } 19 | 20 | export interface login { 21 | login: login_login; 22 | } 23 | 24 | export interface loginVariables { 25 | username: string; 26 | password: string; 27 | } 28 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | prisma: 4 | image: prismagraphql/prisma:1.34 5 | restart: always 6 | ports: 7 | - '127.0.0.1:4466:4466' 8 | environment: 9 | PRISMA_CONFIG: | 10 | port: 4466 11 | databases: 12 | default: 13 | connector: postgres 14 | host: postgres 15 | port: 5432 16 | user: prisma 17 | password: prisma 18 | postgres: 19 | image: postgres:10.9 20 | restart: always 21 | environment: 22 | POSTGRES_USER: prisma 23 | POSTGRES_PASSWORD: prisma 24 | volumes: 25 | - postgres:/var/lib/postgresql/data 26 | 27 | volumes: 28 | postgres: ~ 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Catter, a shitty twitter clone 2 | 3 | [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com/) 4 | 5 | ## Getting started 6 | 7 | Setting up server: 8 | 9 | ``` sh 10 | cd server 11 | yarn 12 | docker-compose up -d 13 | prisma deploy 14 | yarn dev 15 | ``` 16 | 17 | Setting up client: 18 | 19 | ``` sh 20 | cd client 21 | yarn 22 | yarn start 23 | ``` 24 | 25 | ## Cool things 26 | 27 | - Graphql backend, with Apollo on the front end 28 | - Functional style programming, making use of fun stuff like Monads and currying 29 | - server/permisisons.ts, a cool package i don't see much about. no more checking 30 | auth inside resolvers 31 | - a clean commit history 32 | -------------------------------------------------------------------------------- /client/src/types/meow.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL fragment: meow 7 | // ==================================================== 8 | 9 | export interface meow_author { 10 | __typename: "User"; 11 | id: string; 12 | username: string; 13 | name: string; 14 | } 15 | 16 | export interface meow_likedBy { 17 | __typename: "User"; 18 | id: string; 19 | username: string; 20 | name: string; 21 | } 22 | 23 | export interface meow { 24 | __typename: "Meow"; 25 | id: string; 26 | content: string; 27 | author: meow_author; 28 | likedBy: meow_likedBy[]; 29 | } 30 | -------------------------------------------------------------------------------- /client/src/types/register.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: register 7 | // ==================================================== 8 | 9 | export interface register_signup_user { 10 | __typename: "User"; 11 | id: string; 12 | } 13 | 14 | export interface register_signup { 15 | __typename: "AuthPayload"; 16 | token: string; 17 | user: register_signup_user; 18 | } 19 | 20 | export interface register { 21 | signup: register_signup; 22 | } 23 | 24 | export interface registerVariables { 25 | username: string; 26 | password: string; 27 | name: string; 28 | } 29 | -------------------------------------------------------------------------------- /client/src/types/likemeow.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: likemeow 7 | // ==================================================== 8 | 9 | export interface likemeow_likeMeow_likedBy { 10 | __typename: "User"; 11 | id: string; 12 | username: string; 13 | name: string; 14 | } 15 | 16 | export interface likemeow_likeMeow { 17 | __typename: "Meow"; 18 | id: string; 19 | likedBy: likemeow_likeMeow_likedBy[]; 20 | } 21 | 22 | export interface likemeow { 23 | likeMeow: likemeow_likeMeow | null; 24 | } 25 | 26 | export interface likemeowVariables { 27 | id: string; 28 | } 29 | -------------------------------------------------------------------------------- /server/resolvers/user.ts: -------------------------------------------------------------------------------- 1 | import { prismaObjectType } from 'nexus-prisma'; 2 | import { RootValue } from 'nexus/dist/core'; 3 | 4 | import { Context } from '../utils'; 5 | 6 | export const User = prismaObjectType({ 7 | name: 'User', 8 | definition(t) { 9 | t.prismaFields(['id', 'username', 'name']); 10 | t.list.field('meows', { 11 | type: 'Meow', 12 | resolve: (parent: RootValue<'User'>, __, ctx: Context) => 13 | ctx.prisma.user({ id: parent.id }).meows({ orderBy: 'createdAt_DESC' }) 14 | }); 15 | t.list.field('likes', { 16 | type: 'Meow', 17 | resolve: (parent: RootValue<'User'>, __, ctx: Context) => 18 | ctx.prisma.user({ id: parent.id }).likes({ orderBy: 'createdAt_DESC' }) 19 | }); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Aria Edmonds 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /server/resolvers/meow.ts: -------------------------------------------------------------------------------- 1 | import { prismaObjectType } from 'nexus-prisma'; 2 | import { RootValue } from 'nexus/dist/core'; 3 | 4 | import { Context } from '../utils'; 5 | 6 | export const Meow = prismaObjectType({ 7 | name: 'Meow', 8 | definition(t) { 9 | t.prismaFields(['id', 'content', 'author', 'createdAt', 'replyingTo']); 10 | t.list.field('likedBy', { 11 | type: 'User', 12 | resolve: (parent: RootValue<'Meow'>, __, ctx: Context) => 13 | ctx.prisma.meow({ id: parent.id }).likedBy() 14 | }); 15 | 16 | t.list.field('replies', { 17 | type: 'Meow', 18 | resolve: (parent: RootValue<'Meow'>, __, ctx: Context) => 19 | ctx.prisma 20 | .meow({ id: parent.id }) 21 | .replies({ orderBy: 'createdAt_DESC' }) 22 | }); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'react-app', 4 | 'plugin:unicorn/recommended', 5 | 'plugin:import/recommended', 6 | 'plugin:import/typescript', 7 | 'prettier', 8 | 'prettier/@typescript-eslint' 9 | ], 10 | plugins: ['@typescript-eslint', 'unicorn', 'import'], 11 | rules: { 12 | 'arrow-body-style': ['warn', 'as-needed'], 13 | 'unicorn/prevent-abbreviations': 'off', 14 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 15 | // if someone is using any, chances are the typechecker is fucking them 16 | '@typescript-eslint/no-explicit-any': 'off', 17 | 'import/order': ['error', { 'newlines-between': 'always' }], 18 | 'prefer-const': 'error', 19 | 'no-var': 'error', 20 | 'prefer-destructuring': 'error' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /server/utils.ts: -------------------------------------------------------------------------------- 1 | import { verify } from 'jsonwebtoken'; 2 | 3 | import { Prisma } from './generated/prisma-client'; 4 | 5 | // TODO: Change in production!! 6 | export const APP_SECRET = 7 | process.env.APP_SECRET || 8 | 'xldjNnUWSKnBcnBuLBcghlzRBWf6slu9j8xiwTH1OWDkEhfWHTadrLG1or1v9qJE'; 9 | 10 | export function getUserId(ctx: Context) { 11 | const Authorization = ctx.request.get('Authorization'); 12 | if (Authorization) { 13 | const token = Authorization.replace('Bearer ', ''); 14 | const verifiedToken = verify(token, APP_SECRET) as Token; 15 | return verifiedToken && verifiedToken.userId; 16 | } 17 | 18 | throw new Error('Unauthorized! This should be unreachable.'); 19 | } 20 | 21 | interface Token { 22 | userId: string; 23 | } 24 | 25 | export interface Context { 26 | prisma: Prisma; 27 | request: any; 28 | } 29 | -------------------------------------------------------------------------------- /server/permissions.ts: -------------------------------------------------------------------------------- 1 | import { rule, shield, and } from 'graphql-shield'; 2 | 3 | import { Context, getUserId } from './utils'; 4 | 5 | const isAuthenticatedUser = rule()((_, __, ctx: Context) => { 6 | const userId = getUserId(ctx); 7 | return Boolean(userId); 8 | }); 9 | 10 | const isMeowOwner = rule()(async (_, { id }, ctx: Context) => { 11 | const userId = getUserId(ctx); 12 | const author = await ctx.prisma.meow({ id }).author(); 13 | 14 | return userId === author.id; 15 | }); 16 | 17 | const isAuthenticatedAndOwnsMeow = and(isAuthenticatedUser, isMeowOwner); 18 | 19 | export default shield({ 20 | Query: { 21 | me: isAuthenticatedUser 22 | }, 23 | Mutation: { 24 | postMeow: isAuthenticatedUser, 25 | deleteMeow: isAuthenticatedAndOwnsMeow, 26 | likeMeow: isAuthenticatedUser, 27 | setName: isAuthenticatedUser 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { makePrismaSchema } from 'nexus-prisma'; 4 | import { GraphQLServer } from 'graphql-yoga'; 5 | 6 | import { prisma } from './generated/prisma-client'; 7 | import datamodelInfo from './generated/nexus-prisma'; 8 | import * as allTypes from './resolvers'; 9 | import permissions from './permissions'; 10 | 11 | const schema = makePrismaSchema({ 12 | types: allTypes, 13 | prisma: { 14 | datamodelInfo, 15 | client: prisma 16 | }, 17 | outputs: { 18 | schema: path.join(__dirname, './generated/schema.graphql'), 19 | typegen: path.join(__dirname, './generated/nexus.ts') 20 | } 21 | }); 22 | 23 | const server = new GraphQLServer({ 24 | schema, 25 | middlewares: [permissions], 26 | context: request => ({ prisma, ...request }) 27 | }); 28 | 29 | // eslint-disable-next-line no-console 30 | server.start( 31 | { 32 | endpoint: '/graphql', 33 | playground: process.env.NODE_ENV == 'production' ? false : '/' 34 | }, 35 | () => console.log('Server running on http://localhost:4000') 36 | ); 37 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-boost'; 2 | import React from 'react'; 3 | import { ApolloProvider } from 'react-apollo-hooks'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import App from './app'; 7 | import './index.css'; 8 | import * as serviceWorker from './serviceWorker'; 9 | 10 | const client = new ApolloClient({ 11 | uri: '/graphql', 12 | request: operation => { 13 | const token = localStorage.getItem('token'); 14 | if (token) { 15 | operation.setContext({ 16 | headers: { 17 | Authorization: `Bearer ${token}` 18 | } 19 | }); 20 | } 21 | } 22 | }); 23 | 24 | const WrappedApp = () => ( 25 | 26 | 27 | 28 | ); 29 | 30 | ReactDOM.render(, document.querySelector('#root')); 31 | 32 | // If you want your app to work offline and load faster, you can change 33 | // unregister() to register() below. Note this comes with some pitfalls. 34 | // Learn more about service workers: https://bit.ly/CRA-PWA 35 | serviceWorker.unregister(); 36 | -------------------------------------------------------------------------------- /client/src/types/postmeow.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: postmeow 7 | // ==================================================== 8 | 9 | export interface postmeow_postMeow_author { 10 | __typename: "User"; 11 | id: string; 12 | username: string; 13 | } 14 | 15 | export interface postmeow_postMeow_replyingTo_replies { 16 | __typename: "Meow"; 17 | id: string; 18 | } 19 | 20 | export interface postmeow_postMeow_replyingTo { 21 | __typename: "Meow"; 22 | id: string; 23 | replies: postmeow_postMeow_replyingTo_replies[]; 24 | } 25 | 26 | export interface postmeow_postMeow { 27 | __typename: "Meow"; 28 | id: string; 29 | author: postmeow_postMeow_author; 30 | replyingTo: postmeow_postMeow_replyingTo | null; 31 | } 32 | 33 | export interface postmeow { 34 | postMeow: postmeow_postMeow; 35 | } 36 | 37 | export interface postmeowVariables { 38 | content: string; 39 | replyingTo?: string | null; 40 | } 41 | -------------------------------------------------------------------------------- /client/src/types/getfeed.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: getfeed 7 | // ==================================================== 8 | 9 | export interface getfeed_feed_author { 10 | __typename: "User"; 11 | id: string; 12 | username: string; 13 | name: string; 14 | } 15 | 16 | export interface getfeed_feed_likedBy { 17 | __typename: "User"; 18 | id: string; 19 | name: string; 20 | username: string; 21 | } 22 | 23 | export interface getfeed_feed_replyingTo_author { 24 | __typename: "User"; 25 | id: string; 26 | username: string; 27 | name: string; 28 | } 29 | 30 | export interface getfeed_feed_replyingTo { 31 | __typename: "Meow"; 32 | id: string; 33 | author: getfeed_feed_replyingTo_author; 34 | } 35 | 36 | export interface getfeed_feed { 37 | __typename: "Meow"; 38 | id: string; 39 | content: string; 40 | author: getfeed_feed_author; 41 | likedBy: getfeed_feed_likedBy[]; 42 | replyingTo: getfeed_feed_replyingTo | null; 43 | } 44 | 45 | export interface getfeed { 46 | feed: getfeed_feed[]; 47 | } 48 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/eslint-recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:unicorn/recommended', 11 | 'plugin:import/recommended', 12 | 'plugin:import/typescript', 13 | 'prettier', 14 | 'prettier/@typescript-eslint' 15 | ], 16 | plugins: ['@typescript-eslint', 'unicorn', 'import'], 17 | globals: { 18 | Atomics: 'readonly', 19 | SharedArrayBuffer: 'readonly' 20 | }, 21 | parserOptions: { 22 | ecmaVersion: 2018, 23 | sourceType: 'module' 24 | }, 25 | rules: { 26 | 'arrow-body-style': ['warn', 'as-needed'], 27 | '@typescript-eslint/explicit-function-return-type': 'off', 28 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 29 | // if someone is using any, chances are the typechecker is fucking them 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | 'unicorn/prevent-abbreviations': [ 32 | 'error', 33 | { whitelist: { ctx: true, err: true, args: true } } 34 | ], 35 | 'import/order': ['error', { 'newlines-between': 'always' }], 36 | 'prefer-const': 'error', 37 | 'no-var': 'error', 38 | 'prefer-destructuring': 'error' 39 | }, 40 | parser: '@typescript-eslint/parser' 41 | }; 42 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catter-server", 3 | "version": "0.0.1", 4 | "main": "index.ts", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "ts-node index.ts", 8 | "dev": "ts-node-dev --no-notify --respawn ./", 9 | "build": "tsc", 10 | "lint": "eslint '**/*.ts'" 11 | }, 12 | "dependencies": { 13 | "@types/zxcvbn": "^4.4.0", 14 | "argon2": "^0.24.0", 15 | "fp-ts": "^2.0.0", 16 | "graphql": "^14.3.1", 17 | "graphql-shield": "^6.0.0", 18 | "graphql-yoga": "^1.17.4", 19 | "jsonwebtoken": "^8.5.1", 20 | "nexus": "^0.11.7", 21 | "nexus-prisma": "^0.3.7", 22 | "prisma-client-lib": "^1.34.0", 23 | "ramda": "^0.26.1", 24 | "the-big-username-blacklist": "^1.5.2", 25 | "yup": "^0.27.0", 26 | "zxcvbn": "^4.4.2" 27 | }, 28 | "devDependencies": { 29 | "@types/graphql": "14.2.3", 30 | "@types/jsonwebtoken": "8.3.2", 31 | "@types/ramda": "types/npm-ramda#dist", 32 | "@typescript-eslint/eslint-plugin": "1.13.0", 33 | "@typescript-eslint/parser": "1.13.0", 34 | "eslint": "6.0.1", 35 | "eslint-config-prettier": "6.0.0", 36 | "eslint-plugin-unicorn": "10.0.0", 37 | "eslint-plugin-import": "2.18.2", 38 | "nexus-prisma-generate": "0.3.7", 39 | "ts-node": "8.3.0", 40 | "ts-node-dev": "1.0.0-pre.40", 41 | "typescript": "3.4.5" 42 | }, 43 | "resolutions": { 44 | "graphql": "^14.3.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/types/getuser.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: getuser 7 | // ==================================================== 8 | 9 | export interface getuser_user_meows_author { 10 | __typename: "User"; 11 | id: string; 12 | username: string; 13 | name: string; 14 | } 15 | 16 | export interface getuser_user_meows_likedBy { 17 | __typename: "User"; 18 | id: string; 19 | username: string; 20 | name: string; 21 | } 22 | 23 | export interface getuser_user_meows_replyingTo_author { 24 | __typename: "User"; 25 | id: string; 26 | username: string; 27 | name: string; 28 | } 29 | 30 | export interface getuser_user_meows_replyingTo { 31 | __typename: "Meow"; 32 | id: string; 33 | author: getuser_user_meows_replyingTo_author; 34 | } 35 | 36 | export interface getuser_user_meows { 37 | __typename: "Meow"; 38 | id: string; 39 | content: string; 40 | author: getuser_user_meows_author; 41 | likedBy: getuser_user_meows_likedBy[]; 42 | replyingTo: getuser_user_meows_replyingTo | null; 43 | } 44 | 45 | export interface getuser_user { 46 | __typename: "User"; 47 | id: string; 48 | name: string; 49 | meows: getuser_user_meows[]; 50 | } 51 | 52 | export interface getuser { 53 | user: getuser_user | null; 54 | } 55 | 56 | export interface getuserVariables { 57 | username: string; 58 | } 59 | -------------------------------------------------------------------------------- /client/src/feed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import gql from 'graphql-tag'; 3 | import { useQuery } from 'react-apollo-hooks'; 4 | 5 | import { getfeed, getfeed_feed } from './types/getfeed'; 6 | import { Meow } from './meow'; 7 | import { CreateMeow } from './create-meow'; 8 | import { useUserState } from './user-state'; 9 | import { Loader } from './loader'; 10 | 11 | const GET_FEED = gql` 12 | query getfeed { 13 | feed { 14 | id 15 | content 16 | author { 17 | id 18 | username 19 | name 20 | } 21 | likedBy { 22 | id 23 | name 24 | username 25 | } 26 | replyingTo { 27 | id 28 | author { 29 | id 30 | username 31 | name 32 | } 33 | } 34 | } 35 | } 36 | `; 37 | 38 | export const Feed = () => { 39 | const { data, error, loading } = useQuery(GET_FEED, { 40 | fetchPolicy: 'cache-and-network' 41 | } as any); 42 | 43 | const loggedIn = Boolean(useUserState('token')); 44 | if (loading) { 45 | return ; 46 | } 47 | 48 | if (error) { 49 | return
Error! {error.message}
; 50 | } 51 | 52 | if (!data) { 53 | return
Unreachable error! Please report. id: 1
; 54 | } 55 | 56 | return ( 57 |
58 | {loggedIn && } 59 | {data.feed.map((meow: getfeed_feed) => ( 60 | 61 | ))} 62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Catter 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /client/src/user-state.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useReducer, useContext, useEffect } from 'react'; 2 | 3 | export const initialState = { 4 | token: localStorage.getItem('token'), 5 | username: localStorage.getItem('username') 6 | }; 7 | 8 | export type State = typeof initialState; 9 | 10 | export type Action = 11 | | { type: 'login'; token: string; username: string } 12 | | { type: 'logout' }; 13 | 14 | export const reducer = (state: State, action: Action): State => { 15 | switch (action.type) { 16 | case 'login': 17 | return { 18 | ...state, 19 | token: action.token, 20 | username: action.username 21 | }; 22 | case 'logout': 23 | return { 24 | ...state, 25 | token: null, 26 | username: null 27 | }; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | const stateCtx = createContext(initialState); 34 | const dispatchCtx = createContext((() => 0) as React.Dispatch); 35 | 36 | export const Provider: React.FC = ({ children }) => { 37 | const [state, dispatch] = useReducer(reducer, initialState); 38 | 39 | useEffect(() => { 40 | if (state.token) localStorage.setItem('token', state.token); 41 | else localStorage.removeItem('token'); 42 | }, [state.token]); 43 | 44 | useEffect(() => { 45 | if (state.username) localStorage.setItem('username', state.username); 46 | else localStorage.removeItem('username'); 47 | }, [state.username]); 48 | 49 | return ( 50 | 51 | {children} 52 | 53 | ); 54 | }; 55 | 56 | export const useDispatch = () => useContext(dispatchCtx); 57 | 58 | export const useUserState = (property: K) => { 59 | const state = useContext(stateCtx); 60 | return state[property]; 61 | }; 62 | -------------------------------------------------------------------------------- /client/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Redirect, 5 | Route, 6 | RouteProps, 7 | Switch 8 | } from 'react-router-dom'; 9 | 10 | import './App.css'; 11 | import { Header } from './header'; 12 | import './jost/jost.css'; 13 | import { Login } from './login'; 14 | import { Logout } from './logout'; 15 | import { Provider, useUserState } from './user-state'; 16 | import { SettingsName } from './settings-name'; 17 | import { Feed } from './feed'; 18 | import { SingleMeow } from './meow'; 19 | import { User } from './user'; 20 | 21 | const PrivateRoute: React.FC< 22 | { 23 | component: any /*forgive me for i have sinned*/; 24 | } & RouteProps 25 | > = ({ component: Component, ...rest }) => { 26 | const token = useUserState('token'); 27 | const isAuthenticated = Boolean(token); 28 | return ( 29 | 32 | isAuthenticated ? ( 33 | 34 | ) : ( 35 | 38 | ) 39 | } 40 | /> 41 | ); 42 | }; 43 | 44 | const App: React.FC = () => ( 45 | 46 |
47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 |
61 |
62 | ); 63 | 64 | export default App; 65 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | 92 | 93 | # prisma generated config 94 | generated/ 95 | 96 | # typescript production generation 97 | dist/ 98 | -------------------------------------------------------------------------------- /client/src/settings-name.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import useForm from 'react-hook-form'; 3 | import { useMutation } from 'react-apollo-hooks'; 4 | import gql from 'graphql-tag'; 5 | import { Redirect } from 'react-router-dom'; 6 | 7 | import { setname } from './types/setname'; 8 | import { useUserState } from './user-state'; 9 | 10 | interface Schema { 11 | name: string; 12 | } 13 | 14 | // react-hook-form doesn't export it's Error type so we just make do with any 15 | const errorMessage = (e: any) => { 16 | // awful i know but react doesnt render anything if it tries to render null so 17 | // blame react 18 | if (!e) return null; 19 | switch (e.type) { 20 | case 'required': 21 | return 'Name is required'; 22 | case 'minLength': 23 | return 'Name must be at least 3 characters'; 24 | default: 25 | return 'Unknown error ' + e.type; 26 | } 27 | }; 28 | 29 | const SET_NAME = gql` 30 | mutation setname($name: String!) { 31 | setName(name: $name) { 32 | id 33 | username 34 | name 35 | } 36 | } 37 | `; 38 | 39 | export const SettingsName = () => { 40 | // TODO: default value set to current name 41 | const { register, handleSubmit, errors } = useForm(); 42 | const [setName] = useMutation(SET_NAME); 43 | const [redirect, setRedirect] = useState(false); 44 | const username = useUserState('username'); 45 | const onSubmit = useCallback( 46 | ({ name }: Schema) => 47 | setName({ variables: { name } }).then(() => setRedirect(true)), 48 | [setName] 49 | ); 50 | 51 | if (redirect) { 52 | return ; 53 | } 54 | 55 | return ( 56 |
57 |
58 |

59 | {errorMessage(errors.name)} 60 |

61 | 67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "16.9.1", 7 | "@types/jest": "24.0.17", 8 | "@material-ui/core": "4.3.1", 9 | "@material-ui/styles": "4.3.0", 10 | "@types/node": "12.7.1", 11 | "@types/react-dom": "16.8.5", 12 | "apollo": "2.17.2", 13 | "apollo-boost": "0.4.3", 14 | "classnames": "2.2.6", 15 | "conditional-wrap": "1.0.0", 16 | "fp-ts": "2.0.5", 17 | "graphql": "14.4.2", 18 | "ramda": "0.26.1", 19 | "react": "16.9.0", 20 | "react-apollo": "2.5.8", 21 | "react-apollo-hooks": "0.5.0", 22 | "react-circular-progressbar": "2.0.1", 23 | "react-dom": "16.9.0", 24 | "react-fittext": "1.0.0", 25 | "react-hook-form": "3.21.11", 26 | "react-router-dom": "5.0.1", 27 | "react-scripts": "3.1.0", 28 | "react-textarea-autosize": "7.1.0", 29 | "typescript": "3.5.1" 30 | }, 31 | "scripts": { 32 | "start": "npm-run-all --parallel watch:css start:react", 33 | "build": "npm-run-all build:css build:react", 34 | "build:css": "postcss src/tailwind.css -o src/index.css", 35 | "watch:css": "postcss src/tailwind.css -o src/index.css -w", 36 | "start:react": "react-scripts start", 37 | "build:react": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject", 40 | "lint": "eslint 'src/**/*.tsx'", 41 | "generate": "apollo codegen:generate --target typescript --endpoint=http://localhost:4000 types" 42 | }, 43 | "proxy": "http://localhost:4000", 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [">0.2%", "not dead", "not op_mini all"], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "resolutions": { 56 | "graphql": "14.4.2" 57 | }, 58 | "devDependencies": { 59 | "@types/classnames": "2.2.9", 60 | "@types/ramda": "types/npm-ramda#dist", 61 | "@types/react-router-dom": "4.3.4", 62 | "@types/react-textarea-autosize": "4.3.4", 63 | "@typescript-eslint/eslint-plugin": "1.13.0", 64 | "@typescript-eslint/parser": "1.13.0", 65 | "autoprefixer": "9.6.1", 66 | "babel-eslint": "10.0.2", 67 | "eslint": "6.1.0", 68 | "eslint-config-prettier": "6.0.0", 69 | "eslint-config-react-app": "5.0.0", 70 | "eslint-plugin-flowtype": "2.50.3", 71 | "eslint-plugin-import": "2.18.2", 72 | "eslint-plugin-jsx-a11y": "6.2.2", 73 | "eslint-plugin-react-hooks": "1.7.0", 74 | "eslint-plugin-react": "7.14.3", 75 | "eslint-plugin-unicorn": "10.0.0", 76 | "npm-run-all": "4.1.5", 77 | "postcss-cli": "6.1.3", 78 | "tailwindcss": "1.0.5" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { useUserState } from './user-state'; 5 | 6 | const HeaderButtons = ({ 7 | authorized, 8 | username 9 | }: { 10 | authorized: boolean; 11 | username: string | null; 12 | }) => 13 | authorized ? ( 14 | <> 15 | 19 | profile 20 | 21 | 25 | logout 26 | 27 | 28 | ) : ( 29 | 33 | login 34 | 35 | ); 36 | 37 | export const Header = () => { 38 | const token = useUserState('token'); 39 | const username = useUserState('username'); 40 | const authorized = Boolean(token); 41 | return ( 42 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /client/src/types/getmeow.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: getmeow 7 | // ==================================================== 8 | 9 | export interface getmeow_meow_author { 10 | __typename: "User"; 11 | id: string; 12 | username: string; 13 | name: string; 14 | } 15 | 16 | export interface getmeow_meow_likedBy { 17 | __typename: "User"; 18 | id: string; 19 | username: string; 20 | name: string; 21 | } 22 | 23 | export interface getmeow_meow_replies_author { 24 | __typename: "User"; 25 | id: string; 26 | username: string; 27 | name: string; 28 | } 29 | 30 | export interface getmeow_meow_replies_likedBy { 31 | __typename: "User"; 32 | id: string; 33 | username: string; 34 | name: string; 35 | } 36 | 37 | export interface getmeow_meow_replies_replyingTo_author { 38 | __typename: "User"; 39 | id: string; 40 | username: string; 41 | name: string; 42 | } 43 | 44 | export interface getmeow_meow_replies_replyingTo_likedBy { 45 | __typename: "User"; 46 | id: string; 47 | username: string; 48 | name: string; 49 | } 50 | 51 | export interface getmeow_meow_replies_replyingTo { 52 | __typename: "Meow"; 53 | id: string; 54 | content: string; 55 | author: getmeow_meow_replies_replyingTo_author; 56 | likedBy: getmeow_meow_replies_replyingTo_likedBy[]; 57 | } 58 | 59 | export interface getmeow_meow_replies { 60 | __typename: "Meow"; 61 | id: string; 62 | content: string; 63 | author: getmeow_meow_replies_author; 64 | likedBy: getmeow_meow_replies_likedBy[]; 65 | replyingTo: getmeow_meow_replies_replyingTo | null; 66 | } 67 | 68 | export interface getmeow_meow_replyingTo_author { 69 | __typename: "User"; 70 | id: string; 71 | username: string; 72 | name: string; 73 | } 74 | 75 | export interface getmeow_meow_replyingTo_likedBy { 76 | __typename: "User"; 77 | id: string; 78 | username: string; 79 | name: string; 80 | } 81 | 82 | export interface getmeow_meow_replyingTo { 83 | __typename: "Meow"; 84 | id: string; 85 | content: string; 86 | author: getmeow_meow_replyingTo_author; 87 | likedBy: getmeow_meow_replyingTo_likedBy[]; 88 | } 89 | 90 | export interface getmeow_meow { 91 | __typename: "Meow"; 92 | id: string; 93 | content: string; 94 | author: getmeow_meow_author; 95 | likedBy: getmeow_meow_likedBy[]; 96 | replies: getmeow_meow_replies[]; 97 | replyingTo: getmeow_meow_replyingTo | null; 98 | } 99 | 100 | export interface getmeow { 101 | meow: getmeow_meow | null; 102 | } 103 | 104 | export interface getmeowVariables { 105 | id: string; 106 | } 107 | -------------------------------------------------------------------------------- /client/src/jost/jost.css: -------------------------------------------------------------------------------- 1 | /*! Generated by Font Squirrel (https://www.fontsquirrel.com) on June 9, 2019 */ 2 | 3 | 4 | 5 | @font-face { 6 | font-family: 'jost'; 7 | src: url('./jost-700-bolditalic-webfont.woff2') format('woff2'), 8 | url('./jost-700-bolditalic-webfont.woff') format('woff'); 9 | font-weight: 700; 10 | font-style: italic; 11 | 12 | } 13 | 14 | 15 | 16 | 17 | @font-face { 18 | font-family: 'jost_700_bold'; 19 | src: url('./jost-700-bold-webfont.woff2') format('woff2'), 20 | url('./jost-700-bold-webfont.woff') format('woff'); 21 | font-weight: 700; 22 | font-style: normal; 23 | 24 | } 25 | 26 | 27 | 28 | 29 | @font-face { 30 | font-family: 'jost'; 31 | src: url('./jost-600-semiitalic-webfont.woff2') format('woff2'), 32 | url('./jost-600-semiitalic-webfont.woff') format('woff'); 33 | font-weight: 600; 34 | font-style: italic; 35 | 36 | } 37 | 38 | 39 | 40 | 41 | @font-face { 42 | font-family: 'jost'; 43 | src: url('./jost-600-semi-webfont.woff2') format('woff2'), 44 | url('./jost-600-semi-webfont.woff') format('woff'); 45 | font-weight: 600; 46 | font-style: normal; 47 | 48 | } 49 | 50 | 51 | 52 | 53 | @font-face { 54 | font-family: 'jost'; 55 | src: url('./jost-500-mediumitalic-webfont.woff2') format('woff2'), 56 | url('./jost-500-mediumitalic-webfont.woff') format('woff'); 57 | font-weight: 500; 58 | font-style: italic; 59 | 60 | } 61 | 62 | 63 | 64 | 65 | @font-face { 66 | font-family: 'jost'; 67 | src: url('./jost-500-medium-webfont.woff2') format('woff2'), 68 | url('./jost-500-medium-webfont.woff') format('woff'); 69 | font-weight: 500; 70 | font-style: normal; 71 | 72 | } 73 | 74 | 75 | 76 | 77 | @font-face { 78 | font-family: 'jost'; 79 | src: url('./jost-400-bookitalic-webfont.woff2') format('woff2'), 80 | url('./jost-400-bookitalic-webfont.woff') format('woff'); 81 | font-weight: 400; 82 | font-style: italic; 83 | 84 | } 85 | 86 | 87 | 88 | 89 | @font-face { 90 | font-family: 'jost'; 91 | src: url('./jost-400-book-webfont.woff2') format('woff2'), 92 | url('./jost-400-book-webfont.woff') format('woff'); 93 | font-weight: 400; 94 | font-style: normal; 95 | 96 | } 97 | 98 | 99 | 100 | 101 | @font-face { 102 | font-family: 'jost'; 103 | src: url('./jost-300-lightitalic-webfont.woff2') format('woff2'), 104 | url('./jost-300-lightitalic-webfont.woff') format('woff'); 105 | font-weight: 300; 106 | font-style: italic; 107 | 108 | } 109 | 110 | 111 | 112 | 113 | @font-face { 114 | font-family: 'jost'; 115 | src: url('./jost-300-light-webfont.woff2') format('woff2'), 116 | url('./jost-300-light-webfont.woff') format('woff'); 117 | font-weight: 300; 118 | font-style: normal; 119 | 120 | } 121 | 122 | 123 | 124 | 125 | @font-face { 126 | font-family: 'jost'; 127 | src: url('./jost-200-thinitalic-webfont.woff2') format('woff2'), 128 | url('./jost-200-thinitalic-webfont.woff') format('woff'); 129 | font-weight: 200; 130 | font-style: italic; 131 | 132 | } 133 | 134 | 135 | 136 | 137 | @font-face { 138 | font-family: 'jost'; 139 | src: url('./jost-200-thin-webfont.woff2') format('woff2'), 140 | url('./jost-200-thin-webfont.woff') format('woff'); 141 | font-weight: 200; 142 | font-style: normal; 143 | 144 | } 145 | -------------------------------------------------------------------------------- /client/src/user.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import gql from 'graphql-tag'; 3 | import { useQuery } from 'react-apollo-hooks'; 4 | import { RouteComponentProps } from 'react-router'; 5 | import ReactFitText from 'react-fittext'; 6 | import { Link } from 'react-router-dom'; 7 | import ConditionalWrap from 'conditional-wrap'; 8 | 9 | import { Meow } from './meow'; 10 | import { Loader } from './loader'; 11 | import { getuser } from './types/getuser'; 12 | import { useUserState } from './user-state'; 13 | 14 | const GET_USER = gql` 15 | query getuser($username: String!) { 16 | user(where: { username: $username }) { 17 | id 18 | name 19 | 20 | meows { 21 | id 22 | content 23 | author { 24 | id 25 | username 26 | name 27 | } 28 | likedBy { 29 | id 30 | username 31 | name 32 | } 33 | replyingTo { 34 | id 35 | author { 36 | id 37 | username 38 | name 39 | } 40 | } 41 | } 42 | } 43 | } 44 | `; 45 | 46 | export const User: React.FC> = ({ 47 | match: { 48 | params: { username } 49 | } 50 | }) => { 51 | const myUsername = useUserState('username'); 52 | const { data, error, loading } = useQuery(GET_USER, { 53 | variables: { username }, 54 | fetchPolicy: 'cache-and-network' 55 | } as any); 56 | 57 | if (loading) { 58 | return ; 59 | } 60 | 61 | if (error) { 62 | return
An error occured! {error.message}
; 63 | } 64 | if (!data) { 65 | return
An unreachable error occured! id: 2
; 66 | } 67 | 68 | if (!data.user) { 69 | return
User not found!
; // TODO: go back and show notification 70 | } 71 | return ( 72 |
73 |
74 |
75 | 76 |

77 | ( 80 | {children} 81 | )} 82 | > 83 | {data.user.name || 'INVALID NAME'} 84 | 85 |

86 |
87 | 88 |

@{username}

89 |
90 |
91 |
92 |
93 | {(data.user.meows || []).map(meow => ( 94 | 95 | ))} 96 |
97 |
98 | ); 99 | /* return ( 100 | * 101 | * 102 | * 103 | * 104 | * 105 | * 106 | * ( 109 | * 110 | * {children} 111 | * 112 | * )} 113 | * > 114 | * {data.user.name || 'INVALID NAME'} 115 | * 116 | * 117 | * 118 | * 119 | * 120 | * @{username} 121 | * 122 | * 123 | * 124 | * 125 | * 126 | * 127 | * {(data.user.meows || []).map(meow => ( 128 | * 129 | * ))} 130 | * 131 | * 132 | * ); */ 133 | }; 134 | -------------------------------------------------------------------------------- /client/src/create-meow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import useForm from 'react-hook-form'; 3 | import gql from 'graphql-tag'; 4 | import { useMutation } from 'react-apollo-hooks'; 5 | import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'; 6 | import 'react-circular-progressbar/dist/styles.css'; 7 | import classNames from 'classnames'; 8 | import TextareaAutosize from 'react-textarea-autosize'; 9 | 10 | import { postmeow } from './types/postmeow'; 11 | 12 | const POST_MEOW = gql` 13 | mutation postmeow($content: String!, $replyingTo: ID) { 14 | postMeow(content: $content, replyingTo: $replyingTo) { 15 | id 16 | author { 17 | id 18 | username 19 | } 20 | replyingTo { 21 | id 22 | replies { 23 | id 24 | } 25 | } 26 | } 27 | } 28 | `; 29 | 30 | interface Data { 31 | content: string; 32 | } 33 | 34 | export const CreateMeow: React.FC<{ replyingTo?: string }> = ({ 35 | replyingTo 36 | }) => { 37 | const { register, handleSubmit, watch } = useForm(); 38 | const [postMeow, { loading }] = useMutation(POST_MEOW, { 39 | refetchQueries: ['getfeed', 'getmeow'] 40 | }); 41 | 42 | const onSubmit = useCallback( 43 | ({ content }: Data) => 44 | !loading && postMeow({ variables: { content, replyingTo } }), 45 | [postMeow, replyingTo, loading] 46 | ); 47 | 48 | const content = watch('content') || ''; 49 | 50 | return ( 51 |
52 |
53 |
54 |
55 | 73 |
74 | 86 | 101 |
102 |
103 |
104 |
105 |
106 | ); 107 | /* return ( 108 | * 109 | *
110 | * 111 | * 120 | * 121 | * 122 | * 132 | * 133 | *
134 | *
135 | * ); */ 136 | }; 137 | -------------------------------------------------------------------------------- /client/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /client/src/meow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Redirect, RouteComponentProps } from 'react-router'; 3 | import gql from 'graphql-tag'; 4 | import { useQuery, useMutation } from 'react-apollo-hooks'; 5 | import classNames from 'classnames'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | import { getmeow } from './types/getmeow'; 9 | import { deletemeow } from './types/deletemeow'; 10 | import { likemeow } from './types/likemeow'; 11 | import { Loader } from './loader'; 12 | import { useUserState } from './user-state'; 13 | import { CreateMeow } from './create-meow'; 14 | 15 | const useMeowRedirect = (): [boolean, ((e: React.MouseEvent) => void)] => { 16 | const [toMeow, setToMeow] = useState(false); 17 | 18 | const onCardClick = useCallback( 19 | (e: React.MouseEvent) => { 20 | e.stopPropagation(); 21 | e.preventDefault(); 22 | setToMeow(true); 23 | }, 24 | [setToMeow] 25 | ); 26 | 27 | return [toMeow, onCardClick]; 28 | }; 29 | 30 | export const Meow: React.FC<{ 31 | meow: { 32 | id: string; 33 | content: string; 34 | author: { username: string; name: string }; 35 | likedBy: { username: string; name: string }[]; 36 | replyingTo?: { 37 | id: string; 38 | author: { username: string; id: string; name: string }; 39 | } | null; 40 | }; 41 | noRedirect?: boolean; 42 | noUserRedirect?: boolean; 43 | }> = ({ 44 | meow: { 45 | id, 46 | content, 47 | author: { username, name }, 48 | likedBy, 49 | replyingTo 50 | }, 51 | noRedirect, 52 | noUserRedirect 53 | }) => { 54 | const [toMeow, onCardClick] = useMeowRedirect(); 55 | const [toUser, onUserClick] = useMeowRedirect(); 56 | const myUsername = useUserState('username'); 57 | const [deleteMeow, { loading: deleteLoading }] = useMutation( 58 | DELETE_MEOW, 59 | { 60 | variables: { id }, 61 | refetchQueries: ['getmeow', 'getuser', 'getfeed'] 62 | } 63 | ); 64 | const [likeMeow, { loading: likeLoading }] = useMutation( 65 | LIKE_MEOW, 66 | { variables: { id } } 67 | ); 68 | 69 | const hasLiked = likedBy.filter(u => u.username === myUsername).length > 0; 70 | 71 | const likes = `${hasLiked ? 'un' : ''}like (${likedBy.length})`; 72 | 73 | const onDeleteClick = useCallback( 74 | (e: React.MouseEvent) => { 75 | e.stopPropagation(); 76 | if (!deleteLoading) deleteMeow(); 77 | }, 78 | [deleteMeow, deleteLoading] 79 | ); 80 | 81 | const onLikeClick = useCallback( 82 | (e: React.MouseEvent) => { 83 | e.stopPropagation(); 84 | if (!likeLoading) likeMeow(); 85 | }, 86 | [likeMeow, likeLoading] 87 | ); 88 | 89 | if (!noRedirect) { 90 | if (toMeow) { 91 | return ; 92 | } 93 | } 94 | 95 | if (!noUserRedirect) { 96 | if (toUser) { 97 | return ; 98 | } 99 | } 100 | return ( 101 |
105 | 115 |
116 |

{content}

117 |
118 |
119 | 136 | {myUsername && myUsername === username && ( 137 | 152 | )} 153 |
154 |
155 | ); 156 | }; 157 | 158 | const GET_MEOW = gql` 159 | fragment meow on Meow { 160 | id 161 | content 162 | author { 163 | id 164 | username 165 | name 166 | } 167 | likedBy { 168 | id 169 | username 170 | name 171 | } 172 | } 173 | query getmeow($id: ID!) { 174 | meow(where: { id: $id }) { 175 | ...meow 176 | replies { 177 | ...meow 178 | replyingTo { 179 | ...meow 180 | } 181 | } 182 | replyingTo { 183 | ...meow 184 | } 185 | } 186 | } 187 | `; 188 | 189 | const DELETE_MEOW = gql` 190 | mutation deletemeow($id: ID!) { 191 | deleteMeow(id: $id) { 192 | id 193 | } 194 | } 195 | `; 196 | 197 | const LIKE_MEOW = gql` 198 | mutation likemeow($id: ID!) { 199 | likeMeow(id: $id) { 200 | id 201 | likedBy { 202 | id 203 | username 204 | name 205 | } 206 | } 207 | } 208 | `; 209 | 210 | interface Props { 211 | username: string; 212 | id: string; 213 | } 214 | 215 | export const SingleMeow: React.FC> = ({ match }) => { 216 | const { data, error, loading } = useQuery(GET_MEOW, { 217 | variables: { id: match.params.id }, 218 | fetchPolicy: 'cache-and-network' 219 | } as any); 220 | 221 | if (loading) { 222 | return ; 223 | } 224 | 225 | if (error) { 226 | return
Error! {error.message}
; 227 | } 228 | 229 | if (!data) { 230 | return
Unreachable error! Please report. id: 3
; 231 | } 232 | 233 | if (!data.meow) { 234 | return ( 235 |
236 | Meow not found!{' '} 237 | 238 | To Profile 239 | 240 |
241 | ); 242 | } 243 | 244 | return ( 245 | <> 246 | {data.meow.replyingTo && ( 247 | 248 | )} 249 | 250 | 251 | {data.meow.replies.map(reply => ( 252 | 253 | ))} 254 | 255 | ); 256 | }; 257 | -------------------------------------------------------------------------------- /server/resolvers/mutation.ts: -------------------------------------------------------------------------------- 1 | import { prismaObjectType } from 'nexus-prisma'; 2 | import { stringArg, idArg } from 'nexus'; 3 | import { hash, verify } from 'argon2'; 4 | import { sign } from 'jsonwebtoken'; 5 | import { validate } from 'the-big-username-blacklist'; 6 | import * as zxcvbn from 'zxcvbn'; 7 | import * as yup from 'yup'; 8 | import * as R from 'ramda'; 9 | import { constant, identity, Lazy } from 'fp-ts/lib/function'; 10 | import * as T from 'fp-ts/lib/Task'; 11 | import * as TE from 'fp-ts/lib/TaskEither'; 12 | import { Task } from 'fp-ts/lib/Task'; 13 | import * as E from 'fp-ts/lib/Either'; 14 | import { leftTask, rightTask, TaskEither } from 'fp-ts/lib/TaskEither'; 15 | import { pipe } from 'fp-ts/lib/pipeable'; 16 | import { filter, isEmpty } from 'fp-ts/lib/Array'; 17 | 18 | import { Context, APP_SECRET, getUserId } from '../utils'; 19 | import { User, Meow } from '../generated/prisma-client'; 20 | 21 | const nameSchema = yup 22 | .string() 23 | .required() 24 | .trim() 25 | .min(3); 26 | 27 | const tryCatch = (f: Lazy>): TaskEither => 28 | TE.tryCatch(f, ({ message }: Error) => message); 29 | 30 | type HandleError = (t: TaskEither) => Task; 31 | const handleError: HandleError = T.map( 32 | E.fold(error => { 33 | throw new Error(error); 34 | }, identity) 35 | ); 36 | const signupSchema = yup.object().shape({ 37 | username: yup 38 | .string() 39 | .required() 40 | .lowercase() 41 | .trim() 42 | .min(3) 43 | .max(20) 44 | .matches(/^[a-zA-Z0-9]+$/, { message: '${path} must be alphanumeric' }) 45 | .test({ 46 | name: 'blacklisted', 47 | test: validate, 48 | message: '${path} is in blacklist' 49 | }), 50 | name: nameSchema, 51 | password: yup.string().required() 52 | }); 53 | 54 | interface Schema { 55 | username: string; 56 | name: string; 57 | password: string; 58 | } 59 | 60 | const fetchOrElse = (onNull: Err) => ( 61 | f: Task 62 | ): TaskEither => pipe(f, T.map(E.fromNullable(onNull))); 63 | 64 | const getToken = (user: User) => ({ 65 | token: sign({ userId: user.id }, APP_SECRET), 66 | user 67 | }); 68 | 69 | export const Mutation = prismaObjectType({ 70 | name: 'Mutation', 71 | definition(t) { 72 | t.field('signup', { 73 | type: 'AuthPayload', 74 | args: { 75 | username: stringArg(), 76 | password: stringArg(), 77 | name: stringArg() 78 | }, 79 | resolve: async (_, args, ctx: Context) => { 80 | const validateSchema = tryCatch(() => signupSchema.validate(args)); 81 | 82 | const validatePassword = ({ username, password, name }) => { 83 | const { 84 | score, 85 | feedback: { suggestions, warning } 86 | } = zxcvbn(password, [username]); 87 | const suggestions_ = R.ifElse( 88 | R.isEmpty, 89 | constant(''), 90 | R.pipe(R.join(' '), R.concat('Some suggestions are: ')) 91 | )(suggestions); 92 | 93 | const warning_ = R.ifElse( 94 | R.isEmpty, 95 | constant(''), 96 | R.concat(R.__, '.') 97 | )(warning); 98 | const err = R.join(' ', [ 99 | 'Password too weak.', 100 | warning_, 101 | suggestions_ 102 | ]); 103 | return score < 3 104 | ? leftTask(async () => err) 105 | : rightTask(async () => ({ 106 | username, 107 | password, 108 | name 109 | })); 110 | }; 111 | 112 | const hashPassword = ({ password }: Schema) => 113 | TE.tryCatch(() => hash(password), constant('UNREACHABLE ID: 5')); 114 | 115 | const createUser = (username: string, name: string) => ( 116 | hashedPassword: null 117 | ) => 118 | tryCatch(() => 119 | ctx.prisma.createUser({ 120 | username, 121 | password: hashedPassword, 122 | name 123 | }) 124 | ); 125 | 126 | return pipe( 127 | validateSchema, 128 | TE.chain(validatePassword), 129 | TE.chain(({ username, password, name }) => 130 | pipe( 131 | hashPassword({ username, password, name }), 132 | TE.chain(createUser(username, name)) 133 | ) 134 | ), 135 | TE.map(getToken), 136 | handleError 137 | )(); 138 | } 139 | }); 140 | 141 | t.field('login', { 142 | type: 'AuthPayload', 143 | args: { 144 | username: stringArg(), // TODO: Support email silently 145 | password: stringArg() 146 | }, 147 | resolve: (_, { username, password }, ctx: Context) => { 148 | const getUser = pipe( 149 | () => ctx.prisma.user({ username: username.toLowerCase().trim() }), 150 | fetchOrElse('No user found with that username') 151 | ); 152 | 153 | const verifyPassword = (user: User) => 154 | pipe( 155 | () => verify(user.password, password), 156 | T.map(b => (b ? user : null)), 157 | fetchOrElse('Password invalid') 158 | ); 159 | 160 | return pipe( 161 | getUser, 162 | TE.chain(verifyPassword), 163 | TE.map(getToken), 164 | handleError 165 | )(); 166 | } 167 | }); 168 | 169 | t.field('postMeow', { 170 | type: 'Meow', 171 | args: { 172 | content: stringArg(), 173 | replyingTo: idArg({ required: false }) 174 | }, 175 | resolve: (_, { content, replyingTo }, ctx: Context) => { 176 | const userId = getUserId(ctx); 177 | if (content.length > 280) throw new Error('Content too long'); 178 | if (content.length <= 0) throw new Error('Content too short'); 179 | return ctx.prisma.createMeow({ 180 | content: content.normalize(), 181 | author: { connect: { id: userId } }, 182 | replyingTo: replyingTo ? { connect: { id: replyingTo } } : undefined 183 | }); 184 | } 185 | }); 186 | 187 | t.field('deleteMeow', { 188 | type: 'Meow', 189 | nullable: true, 190 | args: { id: idArg() }, 191 | resolve: (_, { id }, ctx: Context) => ctx.prisma.deleteMeow({ id }) 192 | }); 193 | 194 | t.field('likeMeow', { 195 | type: 'Meow', 196 | nullable: true, 197 | args: { id: idArg() }, 198 | resolve: async (_, { id }, ctx: Context) => { 199 | const userId = getUserId(ctx); 200 | 201 | const getLikedBy = pipe( 202 | () => ctx.prisma.meow({ id }).likedBy(), 203 | fetchOrElse('Meow does not exist') 204 | ); 205 | 206 | const isMeowLiked = (us: User[]) => 207 | !pipe( 208 | us, 209 | filter(x => x.id === userId), 210 | isEmpty 211 | ); 212 | 213 | const updateMeow = (isLiked: boolean): TaskEither => 214 | rightTask(() => 215 | ctx.prisma.updateMeow({ 216 | data: { 217 | likedBy: { 218 | [isLiked ? 'connect' : 'disconnect']: { id: userId } 219 | } 220 | }, 221 | where: { id } 222 | }) 223 | ); 224 | 225 | return pipe( 226 | getLikedBy, 227 | TE.map(isMeowLiked), 228 | TE.chain(updateMeow), 229 | handleError 230 | )(); 231 | } 232 | }); 233 | 234 | t.field('setName', { 235 | type: 'User', 236 | args: { 237 | name: stringArg() 238 | }, 239 | resolve: (_, args, ctx: Context) => 240 | pipe( 241 | tryCatch(() => nameSchema.validate(args.name)), 242 | TE.chain(name => 243 | tryCatch(() => 244 | ctx.prisma.updateUser({ 245 | where: { id: getUserId(ctx) }, 246 | data: { name } 247 | }) 248 | ) 249 | ), 250 | handleError 251 | )() 252 | }); 253 | } 254 | }); 255 | -------------------------------------------------------------------------------- /client/src/login.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import React, { useState, useCallback, useEffect } from 'react'; 3 | import { useMutation } from 'react-apollo-hooks'; 4 | import { Redirect } from 'react-router-dom'; 5 | import { chain, tryCatch, rightTask } from 'fp-ts/es6/TaskEither'; 6 | import { fold } from 'fp-ts/es6/Either'; 7 | import { pipe } from 'fp-ts/es6/pipeable'; 8 | import useForm from 'react-hook-form'; 9 | import classNames from 'classnames'; 10 | 11 | import { useDispatch } from './user-state'; 12 | import { login as LOGIN_TYPE, login_login } from './types/login'; 13 | import { register as REGISTER_TYPE, register_signup } from './types/register'; 14 | 15 | const LOGIN = gql` 16 | mutation login($username: String!, $password: String!) { 17 | login(username: $username, password: $password) { 18 | token 19 | user { 20 | id 21 | } 22 | } 23 | } 24 | `; 25 | 26 | const REGISTER = gql` 27 | mutation register($username: String!, $password: String!, $name: String!) { 28 | signup(username: $username, password: $password, name: $name) { 29 | token 30 | user { 31 | id 32 | } 33 | } 34 | } 35 | `; 36 | 37 | interface Data { 38 | username: string; 39 | name: string; 40 | password: string; 41 | } 42 | 43 | export const Login = () => { 44 | const { unregister, register, handleSubmit, errors, clearError } = useForm< 45 | Data 46 | >(); 47 | const [login, { loading: loginLoading }] = useMutation(LOGIN); 48 | const [doRegister, { loading: registerLoading }] = useMutation( 49 | REGISTER 50 | ); 51 | const [redirect, setRedirect] = useState(false); 52 | const [error, setError] = useState(''); 53 | const [isRegister, setRegister] = useState(false); 54 | const dispatch = useDispatch(); 55 | 56 | useEffect(() => { 57 | if (!isRegister) { 58 | unregister('name'); 59 | clearError('name'); 60 | } 61 | }, [isRegister, unregister, clearError]); 62 | 63 | const onSubmit = useCallback( 64 | async ({ username, password, name }: Data) => { 65 | const dispatchLogin = ({ token }: register_signup | login_login) => { 66 | dispatch({ type: 'login', token, username }); 67 | setRedirect(true); 68 | }; 69 | 70 | const mutate = (): Promise<{ 71 | result: { data: REGISTER_TYPE | LOGIN_TYPE }; 72 | }> => 73 | isRegister 74 | ? doRegister({ variables: { username, password, name } }) 75 | : login({ variables: { username, password } }); 76 | 77 | const sendMutation = tryCatch( 78 | mutate, 79 | (e: any) => e.graphQLErrors[0].message 80 | ); 81 | 82 | // I can't get typechecking working here >:( 83 | const sendDispatch = (result: any) => 84 | rightTask(async () => 85 | dispatchLogin( 86 | result.data.signup ? result.data.signup : result.data.login 87 | ) 88 | ); 89 | 90 | return pipe( 91 | sendMutation, 92 | chain(sendDispatch) 93 | )().then(e => 94 | pipe( 95 | e, 96 | fold(setError, a => a) 97 | ) 98 | ); 99 | }, 100 | [login, dispatch, isRegister, doRegister] 101 | ); 102 | 103 | const onClick = useCallback(() => setRegister(!isRegister), [ 104 | isRegister, 105 | setRegister 106 | ]); 107 | 108 | if (redirect) { 109 | return ; 110 | } 111 | return ( 112 |
113 |
114 |

115 | {isRegister ? 'register' : 'login'} 116 |

117 |

122 | {error || 'see you on the flip side!'} 123 |

124 | 136 | {isRegister && ( 137 | 149 | )} 150 | 163 | 171 | 177 |
178 |
179 | ); 180 | 181 | /* return ( 182 | * 183 | *
184 | * 185 | * {(isRegister && 'Register') || 'Login'} 186 | * 187 | * 188 | * {error} 189 | * 190 | *
191 | * @ 203 | * ) 204 | * }} 205 | * variant="outlined" 206 | * /> 207 | * {isRegister && ( 208 | * 218 | * )} 219 | * 230 | * 241 | * 249 | * 250 | *
251 | *
252 | * ); */ 253 | }; 254 | --------------------------------------------------------------------------------