commit
1e6ec47c5e
@ -0,0 +1,12 @@
|
||||
.idea
|
||||
__pycache__
|
||||
db.sqlite3
|
||||
*.py[oc]
|
||||
frontend/.parcel-cache
|
||||
frontend/build
|
||||
frontend/dist
|
||||
frontend/node_modules
|
||||
frontend/yarn-error.log
|
||||
public/static/*
|
||||
public/uploads/*
|
||||
!.gitkeep
|
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'app'
|
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.0.5 on 2022-06-05 21:56
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('is_done', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,12 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
is_done = models.BooleanField(default=False)
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
@ -0,0 +1,21 @@
|
||||
from typing import Optional, List
|
||||
|
||||
import strawberry
|
||||
from graphql import ExecutionContext, GraphQLError
|
||||
|
||||
from articles.utils import AuthExtension, GraphQLError as CustomGraphQLError
|
||||
from .mutations import Mutation
|
||||
from .queries import Query
|
||||
|
||||
|
||||
class CustomSchema(strawberry.Schema):
|
||||
def process_errors(self,
|
||||
errors: List[GraphQLError],
|
||||
execution_context: Optional[ExecutionContext] = None):
|
||||
super().process_errors(
|
||||
[e for e in errors if not isinstance(e.original_error, CustomGraphQLError)],
|
||||
execution_context
|
||||
)
|
||||
|
||||
|
||||
schema = CustomSchema(query=Query, mutation=Mutation, extensions=[AuthExtension])
|
@ -0,0 +1,58 @@
|
||||
import jwt
|
||||
import strawberry
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from strawberry import ID
|
||||
from strawberry.types import Info
|
||||
|
||||
from app.models import Task
|
||||
from app.schema.types import TaskType
|
||||
from articles.utils import GraphQLError, IsAuthenticated
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Mutation:
|
||||
@strawberry.mutation(permission_classes=[IsAuthenticated])
|
||||
def add_task(self, title: str, description: str, info: Info) -> TaskType:
|
||||
task = Task.objects.create(title=title, description=description, user=info.context.user)
|
||||
return task
|
||||
|
||||
@strawberry.mutation(permission_classes=[IsAuthenticated])
|
||||
def change_done(self, task_id: ID) -> TaskType:
|
||||
try:
|
||||
task = Task.objects.get(id=task_id)
|
||||
task.is_done = not task.is_done
|
||||
task.save()
|
||||
return task
|
||||
except Task.DoesNotExist:
|
||||
raise GraphQLError('Task not found')
|
||||
|
||||
@strawberry.mutation(permission_classes=[IsAuthenticated])
|
||||
def delete_task(self, task_id: ID) -> bool:
|
||||
try:
|
||||
task = Task.objects.get(id=task_id)
|
||||
task.delete()
|
||||
return True
|
||||
except Task.DoesNotExist:
|
||||
raise GraphQLError('Task not found')
|
||||
|
||||
@strawberry.mutation
|
||||
def sign_up(self, username: str, password: str) -> bool:
|
||||
if User.objects.filter(username=username).exists():
|
||||
raise GraphQLError('Username is unavailable')
|
||||
user = User(username=username)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return True
|
||||
|
||||
@strawberry.mutation
|
||||
def sign_in(self, username: str, password: str) -> str:
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
if user.check_password(password):
|
||||
token = jwt.encode({'id': user.pk}, settings.SECRET_KEY, algorithm='HS256')
|
||||
return token
|
||||
else:
|
||||
raise User.DoesNotExist
|
||||
except User.DoesNotExist:
|
||||
raise GraphQLError('Username and/or password are incorrect')
|
@ -0,0 +1,13 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
|
||||
from app.models import Task
|
||||
from app.schema.types import TaskType
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Query:
|
||||
@strawberry.field
|
||||
def get_tasks(self) -> List[TaskType]:
|
||||
return Task.objects.all()
|
@ -0,0 +1,11 @@
|
||||
import strawberry.django
|
||||
from strawberry import auto
|
||||
from app.models import Task
|
||||
|
||||
|
||||
@strawberry.django.type(model=Task)
|
||||
class TaskType:
|
||||
title: auto
|
||||
description: auto
|
||||
is_done: auto
|
||||
id: auto
|
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for articles project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'articles.settings')
|
||||
|
||||
application = get_asgi_application()
|
@ -0,0 +1,145 @@
|
||||
"""
|
||||
Django settings for articles project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.0.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
import logging, environ
|
||||
from pathlib import Path
|
||||
|
||||
env = environ.Env()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
env_file = BASE_DIR / '.env'
|
||||
if env_file.exists():
|
||||
env.read_env(str(env_file))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-(v9f%n2$y#k^&hu#@bh6i4_5b21uqrx5y67^sxis$_p=4xtas1'
|
||||
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SECURE_PROXY_SSL_HEADER = 'HTTP_X_FORWARDED_PROTO', 'https'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
|
||||
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['*'])
|
||||
DEBUG = env.bool('DJANGO_DEBUG', True)
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'app',
|
||||
'whitenoise.runserver_nostatic',
|
||||
'corsheaders',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'spa.middleware.SPAMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
'http://localhost:31235'
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'articles.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'articles.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': env.db('DATABASE_URL', default='sqlite:///db.sqlite3')
|
||||
}
|
||||
|
||||
|
||||
STATICFILES_STORAGE = 'spa.storage.SPAStaticFilesStorage'
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
STATICFILES_DIRS = [BASE_DIR/'frontend/build', ]
|
||||
|
||||
STATIC_ROOT = BASE_DIR/"public/static"
|
@ -0,0 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from strawberry.django.views import GraphQLView
|
||||
|
||||
from app.schema import schema
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('graphql', GraphQLView.as_view(schema=schema))
|
||||
]
|
@ -0,0 +1,32 @@
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.http import HttpRequest
|
||||
from strawberry import BasePermission
|
||||
from strawberry.extensions import Extension
|
||||
from strawberry.types import Info
|
||||
|
||||
|
||||
class GraphQLError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthExtension(Extension):
|
||||
def on_request_start(self):
|
||||
request = self.execution_context.context.request
|
||||
auth = request.headers.get('authorization')
|
||||
if auth:
|
||||
try:
|
||||
payload = jwt.decode(auth, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
self.execution_context.context.user = User.objects.get(pk=payload['id'])
|
||||
return
|
||||
except (jwt.DecodeError, User.DoesNotExist):
|
||||
pass
|
||||
self.execution_context.context.user = AnonymousUser()
|
||||
|
||||
|
||||
class IsAuthenticated(BasePermission):
|
||||
def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
|
||||
return info.context.user.is_authenticated
|
@ -0,0 +1,17 @@
|
||||
"""
|
||||
WSGI config for articles project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'articles.settings')
|
||||
|
||||
application = get_wsgi_application()
|
@ -0,0 +1,12 @@
|
||||
overwrite: true
|
||||
schema: "./src/graphql/schema.graphql"
|
||||
documents: "./src/graphql/*.graphql"
|
||||
generates:
|
||||
./src/graphql/tscode.tsx:
|
||||
plugins:
|
||||
- "typescript"
|
||||
- "typescript-operations"
|
||||
- "typescript-react-apollo"
|
||||
./graphql.schema.json:
|
||||
plugins:
|
||||
- "introspection"
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
<title>YourTasks</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,41 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.6.2",
|
||||
"@graphql-codegen/introspection": "2.1.1",
|
||||
"@graphql-codegen/typescript": "2.4.11",
|
||||
"@graphql-codegen/typescript-operations": "2.4.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "3.2.14",
|
||||
"@types/node": "^17.0.42",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"parcel": "^2.6.0",
|
||||
"process": "^0.11.10",
|
||||
"typescript": "^4.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.6",
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.8.3",
|
||||
"@mui/lab": "^5.0.0-alpha.84",
|
||||
"@mui/material": "^5.8.2",
|
||||
"@types/store": "^2.0.2",
|
||||
"graphql": "^16.5.0",
|
||||
"inversify": "^6.0.1",
|
||||
"inversify-react": "^1.0.2",
|
||||
"mobx": "^6.6.0",
|
||||
"mobx-react": "^7.5.0",
|
||||
"mobx-react-lite": "^3.4.0",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-hook-form": "^7.31.3",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"store": "^2.0.12"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "parcel index.html -p 31235",
|
||||
"gqlcg": "graphql-codegen --config codegen.yml",
|
||||
"build": "parcel build index.html --dist-dir build"
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { createTheme, CssBaseline, ThemeProvider } from "@mui/material";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import SignInPage from "./pages/SignInPage";
|
||||
import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache } from "@apollo/client";
|
||||
import SignUpPage from "./pages/SignUpPage";
|
||||
import { setContext } from "@apollo/client/link/context";
|
||||
import TasksPage from "./pages/TasksPage";
|
||||
import TokenCheck from "./components/TokenCheck";
|
||||
import { RootStore } from "./stores/RootStore";
|
||||
import { Provider } from "inversify-react";
|
||||
import Layout from "./components/Layout";
|
||||
|
||||
type AppProps = {}
|
||||
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
},
|
||||
})
|
||||
|
||||
const store = new RootStore()
|
||||
|
||||
const httpLink = createHttpLink({
|
||||
uri: process.env.NODE_ENV == 'development' ? 'http://localhost:31234/graphql' : '/graphql',
|
||||
})
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const token = store.authStore.token
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `${token}` : ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const client = new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache()
|
||||
})
|
||||
|
||||
const App = ({}: AppProps) => {
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<ApolloProvider client={client}>
|
||||
<Provider container={store.container}>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline/>
|
||||
<Routes>
|
||||
<Route path='/' element={<Layout />}>
|
||||
<Route path='signin' element={<TokenCheck required={false}><SignInPage/></TokenCheck>}/>
|
||||
<Route path='signup' element={<TokenCheck required={false}><SignUpPage/></TokenCheck>}/>
|
||||
<Route index element={<TokenCheck><TasksPage/></TokenCheck>}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</ApolloProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
};
|
||||
|
||||
export default App;
|
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { AppBar, Button, Container, Stack, Toolbar, Typography, useTheme } from "@mui/material";
|
||||
import { Link, Outlet, useNavigate } from "react-router-dom";
|
||||
import { useInjection } from "inversify-react";
|
||||
import { AuthStore } from "../stores/AuthStore";
|
||||
import { observer } from "mobx-react";
|
||||
import { UIStore } from "../stores/UIStore";
|
||||
|
||||
type AppBarProps = {}
|
||||
|
||||
const Layout = observer(({}: AppBarProps) => {
|
||||
const navigate = useNavigate();
|
||||
const authStore = useInjection(AuthStore);
|
||||
const uiStore = useInjection(UIStore)
|
||||
|
||||
return (
|
||||
<Container sx={{ pt: 7.8, height: '100vh' }}>
|
||||
<AppBar>
|
||||
<Container>
|
||||
<Toolbar>
|
||||
<Typography variant='h5' sx={{ flexGrow: 1 }}>{uiStore.title}</Typography>
|
||||
{authStore.token ?
|
||||
<Button variant='outlined' color='error' onClick={() => {
|
||||
navigate('/signin');
|
||||
authStore.setToken('');
|
||||
}}>Sign Out</Button> :
|
||||
<Stack spacing={1} direction='row'>
|
||||
<Button component={Link} variant='outlined' to='/signin' >Sign In</Button>
|
||||
<Button component={Link} variant='outlined' to='/signup' >Sign Up</Button>
|
||||
</Stack>
|
||||
}
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
<Outlet />
|
||||
</Container>
|
||||
)
|
||||
});
|
||||
|
||||
export default Layout;
|
@ -0,0 +1,16 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useInjection } from "inversify-react";
|
||||
import { UIStore } from "../stores/UIStore";
|
||||
|
||||
type SetTitleProps = {
|
||||
children: string
|
||||
}
|
||||
|
||||
const SetTitle = ({ children }: SetTitleProps) => {
|
||||
const uiStore = useInjection(UIStore)
|
||||
useEffect(() => uiStore.setTitle(children), [])
|
||||
|
||||
return null
|
||||
};
|
||||
|
||||
export default SetTitle;
|
@ -0,0 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { observer } from "mobx-react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useInjection } from "inversify-react";
|
||||
import { AuthStore } from "../stores/AuthStore";
|
||||
|
||||
type TokenCheckProps = {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const TokenCheck = observer(({ children, required = true }: TokenCheckProps) => {
|
||||
const authStore = useInjection(AuthStore);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('token changed', authStore.token);
|
||||
}, [authStore.token]);
|
||||
|
||||
if (required && !authStore.token)
|
||||
return <Navigate to='/signin' state='not_signed_in' />;
|
||||
else if (!required && authStore.token)
|
||||
return <Navigate to='/' />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
});
|
||||
|
||||
export default TokenCheck;
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Untitled GraphQL Schema",
|
||||
"schemaPath": "schema.graphql",
|
||||
"extensions": {
|
||||
"endpoints": {
|
||||
"Default GraphQL Endpoint": {
|
||||
"url": "http://localhost:31234/graphql",
|
||||
"headers": {
|
||||
"user-agent": "JS GraphQL"
|
||||
},
|
||||
"introspect": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
fragment task on TaskType {
|
||||
description
|
||||
isDone
|
||||
title
|
||||
id
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
mutation addTask($description: String!, $title: String!) {
|
||||
addTask(description: $description, title: $title) {
|
||||
...task
|
||||
}
|
||||
}
|
||||
|
||||
mutation changeDone($id: ID!) {
|
||||
changeDone(taskId: $id) {
|
||||
...task
|
||||
}
|
||||
}
|
||||
|
||||
mutation deleteTask($id: ID!) {
|
||||
deleteTask(taskId: $id)
|
||||
}
|
||||
|
||||
mutation signIn($password: String!, $username: String!) {
|
||||
signIn(password: $password, username: $username)
|
||||
}
|
||||
|
||||
mutation signUp($password: String!, $username: String!) {
|
||||
signUp(password: $password, username: $username)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
query getTasks {
|
||||
getTasks {
|
||||
...task
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
# This file was generated based on ".graphqlconfig". Do not edit manually.
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addTask(description: String!, title: String!): TaskType!
|
||||
changeDone(taskId: ID!): TaskType!
|
||||
deleteTask(taskId: ID!): Boolean!
|
||||
signIn(password: String!, username: String!): String!
|
||||
signUp(password: String!, username: String!): Boolean!
|
||||
}
|
||||
|
||||
type Query {
|
||||
getTasks: [TaskType!]!
|
||||
}
|
||||
|
||||
type TaskType {
|
||||
description: String!
|
||||
id: ID!
|
||||
isDone: Boolean!
|
||||
title: String!
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||
const defaultOptions = {} as const;
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
addTask: TaskType;
|
||||
changeDone: TaskType;
|
||||
deleteTask: Scalars['Boolean'];
|
||||
signIn: Scalars['String'];
|
||||
signUp: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationAddTaskArgs = {
|
||||
description: Scalars['String'];
|
||||
title: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationChangeDoneArgs = {
|
||||
taskId: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteTaskArgs = {
|
||||
taskId: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSignInArgs = {
|
||||
password: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSignUpArgs = {
|
||||
password: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
getTasks: Array<TaskType>;
|
||||
};
|
||||
|
||||
export type TaskType = {
|
||||
__typename?: 'TaskType';
|
||||
description: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
isDone: Scalars['Boolean'];
|
||||
title: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TaskFragment = { __typename?: 'TaskType', description: string, isDone: boolean, title: string, id: string };
|
||||
|
||||
export type AddTaskMutationVariables = Exact<{
|
||||
description: Scalars['String'];
|
||||
title: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AddTaskMutation = { __typename?: 'Mutation', addTask: { __typename?: 'TaskType', description: string, isDone: boolean, title: string, id: string } };
|
||||
|
||||
export type ChangeDoneMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ChangeDoneMutation = { __typename?: 'Mutation', changeDone: { __typename?: 'TaskType', description: string, isDone: boolean, title: string, id: string } };
|
||||
|
||||
export type DeleteTaskMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteTaskMutation = { __typename?: 'Mutation', deleteTask: boolean };
|
||||
|
||||
export type SignInMutationVariables = Exact<{
|
||||
password: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SignInMutation = { __typename?: 'Mutation', signIn: string };
|
||||
|
||||
export type SignUpMutationVariables = Exact<{
|
||||
password: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SignUpMutation = { __typename?: 'Mutation', signUp: boolean };
|
||||
|
||||
export type GetTasksQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetTasksQuery = { __typename?: 'Query', getTasks: Array<{ __typename?: 'TaskType', description: string, isDone: boolean, title: string, id: string }> };
|
||||
|
||||
export const TaskFragmentDoc = gql`
|
||||
fragment task on TaskType {
|
||||
description
|
||||
isDone
|
||||
title
|
||||
id
|
||||
}
|
||||
`;
|
||||
export const AddTaskDocument = gql`
|
||||
mutation addTask($description: String!, $title: String!) {
|
||||
addTask(description: $description, title: $title) {
|
||||
...task
|
||||
}
|
||||
}
|
||||
${TaskFragmentDoc}`;
|
||||
export type AddTaskMutationFn = Apollo.MutationFunction<AddTaskMutation, AddTaskMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useAddTaskMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useAddTaskMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useAddTaskMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [addTaskMutation, { data, loading, error }] = useAddTaskMutation({
|
||||
* variables: {
|
||||
* description: // value for 'description'
|
||||
* title: // value for 'title'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAddTaskMutation(baseOptions?: Apollo.MutationHookOptions<AddTaskMutation, AddTaskMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<AddTaskMutation, AddTaskMutationVariables>(AddTaskDocument, options);
|
||||
}
|
||||
export type AddTaskMutationHookResult = ReturnType<typeof useAddTaskMutation>;
|
||||
export type AddTaskMutationResult = Apollo.MutationResult<AddTaskMutation>;
|
||||
export type AddTaskMutationOptions = Apollo.BaseMutationOptions<AddTaskMutation, AddTaskMutationVariables>;
|
||||
export const ChangeDoneDocument = gql`
|
||||
mutation changeDone($id: ID!) {
|
||||
changeDone(taskId: $id) {
|
||||
...task
|
||||
}
|
||||
}
|
||||
${TaskFragmentDoc}`;
|
||||
export type ChangeDoneMutationFn = Apollo.MutationFunction<ChangeDoneMutation, ChangeDoneMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useChangeDoneMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useChangeDoneMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useChangeDoneMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [changeDoneMutation, { data, loading, error }] = useChangeDoneMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useChangeDoneMutation(baseOptions?: Apollo.MutationHookOptions<ChangeDoneMutation, ChangeDoneMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ChangeDoneMutation, ChangeDoneMutationVariables>(ChangeDoneDocument, options);
|
||||
}
|
||||
export type ChangeDoneMutationHookResult = ReturnType<typeof useChangeDoneMutation>;
|
||||
export type ChangeDoneMutationResult = Apollo.MutationResult<ChangeDoneMutation>;
|
||||
export type ChangeDoneMutationOptions = Apollo.BaseMutationOptions<ChangeDoneMutation, ChangeDoneMutationVariables>;
|
||||
export const DeleteTaskDocument = gql`
|
||||
mutation deleteTask($id: ID!) {
|
||||
deleteTask(taskId: $id)
|
||||
}
|
||||
`;
|
||||
export type DeleteTaskMutationFn = Apollo.MutationFunction<DeleteTaskMutation, DeleteTaskMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteTaskMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteTaskMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteTaskMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [deleteTaskMutation, { data, loading, error }] = useDeleteTaskMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteTaskMutation(baseOptions?: Apollo.MutationHookOptions<DeleteTaskMutation, DeleteTaskMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteTaskMutation, DeleteTaskMutationVariables>(DeleteTaskDocument, options);
|
||||
}
|
||||
export type DeleteTaskMutationHookResult = ReturnType<typeof useDeleteTaskMutation>;
|
||||
export type DeleteTaskMutationResult = Apollo.MutationResult<DeleteTaskMutation>;
|
||||
export type DeleteTaskMutationOptions = Apollo.BaseMutationOptions<DeleteTaskMutation, DeleteTaskMutationVariables>;
|
||||
export const SignInDocument = gql`
|
||||
mutation signIn($password: String!, $username: String!) {
|
||||
signIn(password: $password, username: $username)
|
||||
}
|
||||
`;
|
||||
export type SignInMutationFn = Apollo.MutationFunction<SignInMutation, SignInMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSignInMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSignInMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSignInMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [signInMutation, { data, loading, error }] = useSignInMutation({
|
||||
* variables: {
|
||||
* password: // value for 'password'
|
||||
* username: // value for 'username'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSignInMutation(baseOptions?: Apollo.MutationHookOptions<SignInMutation, SignInMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SignInMutation, SignInMutationVariables>(SignInDocument, options);
|
||||
}
|
||||
export type SignInMutationHookResult = ReturnType<typeof useSignInMutation>;
|
||||
export type SignInMutationResult = Apollo.MutationResult<SignInMutation>;
|
||||
export type SignInMutationOptions = Apollo.BaseMutationOptions<SignInMutation, SignInMutationVariables>;
|
||||
export const SignUpDocument = gql`
|
||||
mutation signUp($password: String!, $username: String!) {
|
||||
signUp(password: $password, username: $username)
|
||||
}
|
||||
`;
|
||||
export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSignUpMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSignUpMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSignUpMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [signUpMutation, { data, loading, error }] = useSignUpMutation({
|
||||
* variables: {
|
||||
* password: // value for 'password'
|
||||
* username: // value for 'username'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSignUpMutation(baseOptions?: Apollo.MutationHookOptions<SignUpMutation, SignUpMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SignUpMutation, SignUpMutationVariables>(SignUpDocument, options);
|
||||
}
|
||||
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
|
||||
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
|
||||
export type SignUpMutationOptions = Apollo.BaseMutationOptions<SignUpMutation, SignUpMutationVariables>;
|
||||
export const GetTasksDocument = gql`
|
||||
query getTasks {
|
||||
getTasks {
|
||||
...task
|
||||
}
|
||||
}
|
||||
${TaskFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useGetTasksQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetTasksQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetTasksQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetTasksQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetTasksQuery(baseOptions?: Apollo.QueryHookOptions<GetTasksQuery, GetTasksQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetTasksQuery, GetTasksQueryVariables>(GetTasksDocument, options);
|
||||
}
|
||||
export function useGetTasksLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetTasksQuery, GetTasksQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetTasksQuery, GetTasksQueryVariables>(GetTasksDocument, options);
|
||||
}
|
||||
export type GetTasksQueryHookResult = ReturnType<typeof useGetTasksQuery>;
|
||||
export type GetTasksLazyQueryHookResult = ReturnType<typeof useGetTasksLazyQuery>;
|
||||
export type GetTasksQueryResult = Apollo.QueryResult<GetTasksQuery, GetTasksQueryVariables>;
|
@ -0,0 +1,18 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "reflect-metadata"
|
||||
import App from "./App";
|
||||
|
||||
const root = createRoot(
|
||||
document.querySelector('#root')!
|
||||
)
|
||||
|
||||
const element = (
|
||||
<App/>
|
||||
);
|
||||
root.render(element)
|
||||
|
||||
// @ts-ignore
|
||||
if (Boolean(module.hot)) {
|
||||
// @ts-ignore
|
||||
module.hot.accept()
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Alert, Box, Button, Collapse, Paper, TextField, Toolbar, Typography } from "@mui/material";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSignInMutation } from "../graphql/tscode";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import { useInjection } from "inversify-react";
|
||||
import { AuthStore } from "../stores/AuthStore";
|
||||
import SetTitle from "../components/SetTitle";
|
||||
|
||||
type LoginPageProps = {}
|
||||
|
||||
type Inputs = {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
const SignInPage = observer(({}: LoginPageProps) => {
|
||||
const authStore = useInjection(AuthStore);
|
||||
const location = useLocation();
|
||||
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<Inputs>()
|
||||
|
||||
const [ signUpSuccess, setSignUpSuccess ] = useState(false);
|
||||
|
||||
const [ notSignedIn, setNotSignedIn ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
switch (location.state) {
|
||||
case 'not_signed_in':
|
||||
setNotSignedIn(true)
|
||||
setTimeout(() => {
|
||||
setNotSignedIn(false)
|
||||
}, 5000)
|
||||
break
|
||||
case 'signup_success':
|
||||
setSignUpSuccess(true)
|
||||
setTimeout(() => {
|
||||
setSignUpSuccess(false)
|
||||
}, 5000)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [ signInFunction, {
|
||||
data,
|
||||
loading,
|
||||
error
|
||||
} ] = useSignInMutation({ onCompleted: (result) => authStore.setToken(result.signIn) })
|
||||
|
||||
const onSubmit = async (data: Inputs) => {
|
||||
try {
|
||||
await signInFunction({ variables: data })
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<SetTitle>Signing In</SetTitle>
|
||||
<Paper
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
component='form'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: 2,
|
||||
width: {
|
||||
xs: '90vw',
|
||||
md: 400,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label='Username'
|
||||
variant='standard'
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
{...register('username', { required: 'Username is required' })}
|
||||
/>
|
||||
<TextField
|
||||
label='Password'
|
||||
type='password'
|
||||
variant='standard'
|
||||
margin='normal'
|
||||
sx={{ mb: 3 }}
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
{...register('password', { required: 'Password is required' })}
|
||||
/>
|
||||
{error && <Alert sx={{ mb: 2, maxWidth: '199px' }} variant='filled' severity='error'>{error?.message}</Alert>}
|
||||
<Collapse in={signUpSuccess}><Alert sx={{ mb: 2, maxWidth: '300px' }} variant='filled' severity='success'>You have signed up!</Alert></Collapse>
|
||||
<Collapse in={notSignedIn}><Alert sx={{ mb: 2, maxWidth: '300 px' }} variant='filled' severity='error'>You should sign in motherfucker!</Alert></Collapse>
|
||||
<LoadingButton loading={loading} variant='outlined' type='submit' sx={{ mb: 1 }}>Sign In</LoadingButton>
|
||||
<div>{authStore.token}</div>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
});
|
||||
|
||||
export default SignInPage;
|
@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { Alert, Box, Button, Paper, TextField, Typography } from "@mui/material";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSignInMutation, useSignUpMutation } from "../graphql/tscode";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import SetTitle from "../components/SetTitle";
|
||||
|
||||
type SignUpPageProps = {}
|
||||
|
||||
type Inputs = {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
const SignUpPage = ({}: SignUpPageProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<Inputs>();
|
||||
|
||||
const [ signUpFunction, {
|
||||
data,
|
||||
loading,
|
||||
error
|
||||
} ] = useSignUpMutation({ onCompleted: (result) => {
|
||||
navigate('/signin', { state: 'signup_success' });
|
||||
} });
|
||||
|
||||
const onSubmit = async (data: Inputs) => {
|
||||
try {
|
||||
await signUpFunction({ variables: data });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<SetTitle>Signing Up</SetTitle>
|
||||
<Paper
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
component='form'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: 2,
|
||||
width: {
|
||||
xs: '90vw',
|
||||
md: 400,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label='Username'
|
||||
variant='standard'
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
{...register('username', { required: 'Username is required' })}
|
||||
/>
|
||||
<TextField
|
||||
label='Password'
|
||||
type='password'
|
||||
variant='standard'
|
||||
margin='normal'
|
||||
sx={{ mb: 3 }}
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
{...register('password', { required: 'Password is required' })}
|
||||
/>
|
||||
{error && <Alert sx={{ mb: 2, maxWidth: '199px' }} variant='filled' severity='error'>{error?.message}</Alert>}
|
||||
<LoadingButton loading={loading} variant='outlined' type='submit' sx={{mb: 1}}>Sign Up</LoadingButton>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
|
||||
export default SignUpPage;
|
@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Checkbox, CircularProgress,
|
||||
Divider, Grid, IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper, Stack,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useAddTaskMutation, useChangeDoneMutation, useDeleteTaskMutation, useGetTasksQuery } from "../graphql/tscode";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Delete } from "@mui/icons-material";
|
||||
import SetTitle from "../components/SetTitle";
|
||||
|
||||
type TasksPageProps = {}
|
||||
|
||||
type Inputs = {
|
||||
title: string,
|
||||
description: string,
|
||||
}
|
||||
|
||||
const TasksPage = ({}: TasksPageProps) => {
|
||||
const { reset: resetForm, register, handleSubmit, formState: { errors } } = useForm<Inputs>()
|
||||
|
||||
const { loading: tasksLoading, error: tasksError, data: tasksData, refetch } = useGetTasksQuery({ onCompleted: (result) => console.log(result) })
|
||||
|
||||
const [ addTaskFunction, { data: addTaskData, loading: addTaskLoading, error: addTaskError } ] = useAddTaskMutation({ onCompleted: result => {console.log(result); refetch()} })
|
||||
|
||||
const [ changeDoneFunction, { data: changeDoneData, loading: changeDoneLoading, error: changeDoneError } ] = useChangeDoneMutation({ onCompleted: result => refetch() })
|
||||
|
||||
const [ deleteTaskFunction, { data: deleteTaskData, loading: deleteTaskLoading, error: deleteTaskError } ] = useDeleteTaskMutation({ onCompleted: result => refetch() })
|
||||
|
||||
const [ idChange, setIdChange ] = useState<string>();
|
||||
|
||||
const [ idDelete, setIdDelete ] = useState<string>();
|
||||
|
||||
const onSubmit = async (data: Inputs) => {
|
||||
try {
|
||||
await addTaskFunction({ variables: data })
|
||||
resetForm()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', margin: '6px', padding: '6px' }}>
|
||||
<SetTitle>Your Tasks</SetTitle>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Paper component='form' onSubmit={handleSubmit(onSubmit)} elevation={2} sx={{ padding: 2, display: 'flex', flexDirection: 'column' }}>
|
||||
<TextField variant='standard' label='Task Title' {...register('title', { required: 'Title is required' })} sx={{ mb: 1 }}/>
|
||||
<TextField variant='standard' label='Task Description' {...register('description')} sx={{ mb: 1 }}/>
|
||||
<LoadingButton loading={addTaskLoading} variant='outlined' type='submit'>Add Task</LoadingButton>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Paper>
|
||||
{tasksLoading && typeof tasksData == 'undefined' ?
|
||||
<Stack alignItems="center">
|
||||
<CircularProgress sx={{ m: 2 }} size={600} />
|
||||
</Stack> :
|
||||
!tasksLoading && tasksData!.getTasks.length == 0 ?
|
||||
<Typography variant='h6' sx={{ textAlign: 'center', py: 3 }}>There's no Tasks</Typography> :
|
||||
<List>
|
||||
{tasksData!.getTasks.map(task => (
|
||||
<ListItem
|
||||
key={task.id}
|
||||
secondaryAction={
|
||||
deleteTaskLoading && idDelete == task.id ?
|
||||
<CircularProgress size={25} /> :
|
||||
<IconButton edge='end' onClick={() => deleteTaskFunction({ variables: { id: task.id } })}>
|
||||
<Delete color='error' />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemIcon sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{changeDoneLoading && idChange == task.id ?
|
||||
<CircularProgress size={25}/> :
|
||||
<Checkbox onClick={() => {
|
||||
setIdChange(task.id);
|
||||
changeDoneFunction({ variables: { id: task.id } })
|
||||
}} checked={task.isDone}/>
|
||||
}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={task.title} secondary={task.description}/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
|
||||
export default TasksPage;
|
@ -0,0 +1,17 @@
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import store from "store";
|
||||
import { RootStore } from "./RootStore";
|
||||
|
||||
export class AuthStore {
|
||||
@observable token: string = '';
|
||||
|
||||
constructor(private readonly rootStore: RootStore) {
|
||||
makeObservable(this);
|
||||
this.token = store.get('token', '')
|
||||
}
|
||||
|
||||
@action setToken(token: string) {
|
||||
this.token = token;
|
||||
store.set('token', this.token)
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { Container } from "inversify";
|
||||
import { AuthStore } from "./AuthStore";
|
||||
import { UIStore } from "./UIStore";
|
||||
|
||||
export class RootStore {
|
||||
container: Container = new Container();
|
||||
authStore: AuthStore = new AuthStore(this);
|
||||
uiStore: UIStore = new UIStore(this);
|
||||
|
||||
constructor() {
|
||||
this.container.bind(AuthStore).toConstantValue(this.authStore);
|
||||
this.container.bind(UIStore).toConstantValue(this.uiStore);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import { RootStore } from "./RootStore";
|
||||
|
||||
export class UIStore {
|
||||
@observable title: string = '';
|
||||
|
||||
constructor(private readonly rootStore: RootStore) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action setTitle(title: string) {
|
||||
this.title = title;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsxdev",
|
||||
"resolveJsonModule": true,
|
||||
"useDefineForClassFields": true,
|
||||
"moduleResolution": "node",
|
||||
"typeRoots": ["./src"],
|
||||
"strictNullChecks": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'articles.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in new issue