├── .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 | [](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 |
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 |
106 | );
107 | /* return (
108 | *
109 | *
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 |
179 | );
180 |
181 | /* return (
182 | *
183 | *
184 | *
185 | * {(isRegister && 'Register') || 'Login'}
186 | *
187 | *
188 | * {error}
189 | *
190 | *
250 | *
251 | *
252 | * ); */
253 | };
254 |
--------------------------------------------------------------------------------