GraphQLとは?
GraphQLは、APIを定義するための言語。REST APIのような従来のAPIとは異なり、GraphQLではクライアント側で必要なデータのみをリクエストすることができる為、ネットワークのパフォーマンスが向上し、アプリケーションのパフォーマンスが向上する。
アプリの構造
FRONT END | SERVER | DATA |
---|---|---|
Appllo Clinet | GraphQL | Atlas |
React | Node.js | MongoDB |
Bootstarp UI | express-graphql | MongoDB |
プロジェクトの作成
①下記をインストールする
npm init -y
npm i express express-graphql graphql mongoose cors colors
npm i -D nodemon dotenv
②serverフォルダを作成し、index.jsを作成する
const express = require('express');
const port = process.env.PORT || 5000;
const app = express();
③.envを作成する
NODE_ENV = 'development'
PORT = 8000
MONGO_URI = ""
④ package.jsonを編集する
//省略
"scripts": {
"start": "node server/index.js",
"dev": "nodemon server/index.js"
},
npm run dev
serverフォルダ
①serverフォルダにschemaフォルダ、schema.jsを作成する
const Projects = require('../models/Project');
const Clients = require('../models/Client');
const {
GraphQLObjectType,
GraphQLID,
GraphQLString,
GraphOLSchema,
GraphQLList,
GraphQLNonNull,
GraphQLEnumType,
} = require('graphql');
//Project Type
const ProjectType = new GraphQLObjectType({
name: 'Project',
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
description: { type: GraphQLString },
status: { type: GraphQLString },
client: {
type: ClientType,
resolve(parent, args) {
return Client.findById(parent.clientId);
},
},
}),
});
//Client Type
const ClientType = new GraphQLObjectType({
name: 'Clinet',
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
email: { type: GraphQLString },
phone: { type: GraphQLString },
}),
});
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
projects: {
type: new GraphQLList(ProjectType),
resolve(parent, args) {
return Project.find();
},
},
project: {
type: ProjectType,
args: { id: { type: GraphQLID } },
resolve(parent, args) {
return project.findById(args.id);
},
},
clients: {
type: new GraphQLList(ClientType),
resolve(parent, args) {
return Client.find();
},
},
client: {
type: ClientType,
args: { id: { type: GraphQLID } },
resolve(parent, args) {
return Client.findById(args.id);
},
},
},
});
const mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
addClient: {
type: ClientType,
args: {
name: { type: GraphQLNonNull(GraphQLString) },
email: { type: GraphQLNonNull(GraphQLString) },
phone: { type: GraphQLNonNull(GraphQLString) },
},
resolve(parent, args) {
const client = new Client({
name: args.name,
email: args.email,
phone: args.phone,
});
return client.save();
},
deleteClient: {
type: ClientType,
args: {
id: { type: GraphQLNonNull(GraphQLID) },
},
resolve(parent, args) {
Project.find({ clientId: args.id }).then((project) => {
projects.forEach((project) => {
project.remave();
});
});
return Client.findByIdAndRemove(args.id);
},
},
addProject: {
type: ProjectType,
args: {
name: { type: GraphQLNonNull(GraphQLString) },
description: { type: GraphQLNonNull(GraphQLString) },
status: {
type: new GraphQLEnumType({
name: 'ProjectStatus',
values: {
new: { value: 'Not Started' },
progress: { value: 'In Progress' },
Completed: { value: 'Completed' },
},
}),
defaultValue: 'Not Started',
},
clientId: { type: GraphQLNonNull(GraphQLID) },
},
resolve(parent, args) {
const project = new Project({
name: args.name,
description: args.description,
status: args.status,
clientId: args.clientId,
});
return project.save();
},
},
deleteProject: {
type: ProjectType,
args: {
id: { type: GraphQLNonNull(GraphQLID) },
},
resolve(parent, args) {
return Project.findByIdAndRemove(args.id); // Changed PORT to Project
},
},
updateProject: {
type: ProjectType,
args: {
id: { type: GraphQLNonNull(GraphQLID) },
name: { type: GraphQLString },
description: { type: GraphQLString },
tatus: {
type: new GraphQLEnumType({
name: 'ProjectStatusUpdate',
values: {
new: { value: 'Not Started' },
progress: { value: 'In Progress' },
Completed: { value: 'Completed' },
},
}),
},
},
resolve(parent, args) {
return Project.findByIdAndUpdate(
args.id,
{
$set: {
name: args.name,
description: args.description,
status: args.status,
},
},
{ new: true }
);
},
},
},
},
});
module.exports = new GraphOLSchema({
query: RootQuery,
mutation,
});
②serverフォルダにsampleData.jsを作成する
// Projects
const projects = [
{
id: '1',
clientId: '1',
name: 'eCommerce Website',
description:
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu.',
status: 'In Progress',
},
{
id: '2',
clientId: '2',
name: 'Dating App',
description:
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu.',
status: 'In Progress',
},
{
id: '3',
clientId: '3',
name: 'SEO Project',
description:
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu.',
status: 'In Progress',
},
{
id: '4',
clientId: '4',
name: 'Design Prototype',
description:
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu.',
status: 'Done',
},
{
id: '5',
clientId: '5',
name: 'Auction Website',
description:
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu.',
status: 'In Progress',
},
];
// Clients
const clients = [
{
id: '1',
name: 'Tony Stark',
email: 'ironman@gmail.com',
phone: '343-567-4333',
},
{
id: '2',
name: 'Natasha Romanova',
email: 'blackwidow@gmail.com',
phone: '223-567-3322',
},
{
id: '3',
name: 'Thor Odinson',
email: 'thor@gmail.com',
phone: '324-331-4333',
},
{
id: '4',
name: 'Steve Rogers',
email: 'steve@gmail.com',
phone: '344-562-6787',
},
{
id: '5',
name: 'Bruce Banner',
email: 'bruce@gmail.com',
phone: '321-468-8887',
},
];
module.exports = { projects, clients };
③mongoDBにプロジェクトを作成し、『shared』『aws』『Osaka』を選択し、『Create Cluster』を押す
④serverフォルダにconfigフォルダ、db.jsを作成する
const mongoose = require('mongoose');
const connectDB = async () => {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline.bold);
};
module.exports = connectDB;
⑤serverフォルダにmodelsフォルダ、Client.jsを作成する
const mongoose = require('mongoose');
const ClientSchema = new mongoose.Schema({
name: {
type: String,
},
email: {
type: String,
},
phone: {
type: String,
},
});
module.exports = mongoose.model('Client', ClientSchema);
⑥server/modelsにProject.jsを作成する
const mongoose = require('mongoose');
const ProjectSchema = new mongoose.Schema({
name: {
type: String,
},
description: {
type: String,
},
status: {
type: String,
enum: ['Not Started', 'In Progress', 'Completed'],
},
clientId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Client',
},
});
module.exports = mongoose.model('Project', ProjectSchema);
⑦.gitignoreを作成する
#.gitignoreに無視するファイルやディレクトリを記述する
node_module
.env
⑧server/index.jsを編集する
const express = require('express');
const colors = require('colors');
const cors = require('cors');
require('dotenv').config();
const { graphqlHTTP } = require('express-graphql');
const schema = require('./schema/schema');
const connectDB = require('./config/db');
const port = process.env.PORT || 8000;
const app = express();
// Connect to database
connectDB();
app.use(cors());
app.use(
'/graphql',
graphqlHTTP({
schema,
graphiql: process.env.NODE_ENW === 'development',
})
);
app.listen(port, console.log(`Server running on port ${port}`));
clientフォルダ
① clientを作成する
npx create-react-app client
cd client
npm i @apollo/client graphql react-router-dom react-icons
② client/src/App.jsを編集する
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Header from './components/Header';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';
import Home from './pages/Home';
import Project from './pages/Project';
import NotFound from './pages/NotFound';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
clients: {
merge(existing, incoming) {
return incoming;
},
},
projects: {
merge(existing, incoming) {
return incoming;
},
},
},
},
},
});
const client = new ApolloClient({
uri: 'http://localhost:5000/graphql',
cache,
});
function App() {
return (
<ApolloProvider client={client}>
<Router>
<Header />
<div className='container'>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/projects/:id' element={<Project />} />
<Route path='*' element={<NotFound />} />
</Routes>
</div>
</Router>
</ApolloProvider>
);
}
export default App;
③ client/srcに、component/assetsを作成し、graphqlのロゴを入れる
④ client/srcのindex.cssを編集する
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.navbar-brand {
color: #df3ca6;
}
.navbar-brand img {
width: 30px;
margin-right: 10px;
}
.btn {
font-size: 15px;
}
.btn-primary,
.bg-primary {
background-color: #df3ca6 !important;
border: none;
}
.btn-primary:hover {
background-color: #df3ca6;
opacity: 0.9;
}
.btn-secondary {
background-color: #7430f9;
}
.btn-secondary:hover {
background-color: #7430f9;
opacity: 0.9;
}
.icon {
margin-right: 5px;
}
⑤ client/public/index.htmlに下記のコードを加える
//省略
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js"
integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF"
crossorigin="anonymous"
></script>
<title>Project Mgmt</title>
⑥ client/src/compomentsにHeader.jsxを作成する
import logo from './assets/logo.png';
export default function Header() {
return (
<nav className='navbar bg-light mb-4 p-4'>
<div className='container'>
<a className='navbar-brand' href='/'>
<div className='d-flex'>
<img src={logo} alt='logo' className='mr-2' />
<div>ProjectMgmt</div>
</div>
</a>
</div>
</nav>
);
}
⑦ client/src/compomentsにClients.jsxを作成する
import { useQuery } from '@apollo/client';
import ClientRow from './ClientRow';
import Spinner from 'Spinner';
import { GET_CLIENTS } from '../queries/clientQueries';
export default function Clients() {
const { loading, error, data } = useQuery(GET_CLIENTS);
if (loading) return <Spinner />;
if (error) return <p>Something Went Wrong</p>;
return (
<>
{!loading && !error && (
<table className='table table-hover mt-3'>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th></th>
</tr>
</thead>
<tbody>
{data.clients.map((client) => (
<ClientRow key={client.id} client={client} />
))}
</tbody>
</table>
)}
</>
);
}
⑧ client/src/compomentsにClientRow.jsxを作成する
⑨ client/srcに、queriesフォルダとclientQueries.jsを作成する
import { FaTrash } from 'react-icons/fa';
import { useMutation } from '@apollo/client';
import { DELETE_CLIENTS } from '../mutations/clientMutations';
import { GET_CLIENTS } from '../queries/clientQueries';
import { GET_PROJECT } from '../queries/projectQueries';
export default function ClientRow(client) {
const [deleteClient] = useMutation(DELETE_CLIENTS, {
variables: { id: client.id },
refetchQueries: [{ query: GET_CLIENTS }, { query: GET_PROJECT }],
// update(cache, { data: { deleteClient } }) {
// const { clients } = cache.readQuery({ query: GET_CLIENTS });
// cache.writeQuery({
// query: GET_CLIENTS,
// data: {
// clients: clients.filter((client) => client.id !== deleteClient.id),
// },
// });
//},
});
return (
<tr>
<td>{client.name}</td>
<td>{client.email}</td>
<td>{client.phone}</td>
<td>
<button className='btn btn-danger btn-sm' onClick={deleteClient}>
<FaTrash />
</button>
</td>
</tr>
);
}
⑩ client/src/compomentsにSpinner.jsxを作成する
export default function Spinner() {
return (
<div className='d-flex justify-content-center'>
<div className='spinner-border' role='status'>
<span className='sr-only'></span>
</div>
</div>
);
}
⑪ client/srcに、mutationsフォルダとAddClientMutations.jsを作成する
import { useState } from 'react';
import { FaUser } from 'react-icons/fa';
import { useMutation } from '@apollo/client';
import { ADD_CLIENT } from '../mutations/clientMutations';
import { GET_CLIENTS } from '../queries/clientQueries';
export default function AddClientModal() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [addClient] = useMutation(ADD_CLIENT, {
variables: { name, email, phone },
update(cache, { data: { addClient } }) {
const { clients } = cache.readQuery({
query: GET_CLIENTS,
});
cache.writeQuery({
query: GET_CLIENTS,
data: { clients: [...clients, addClient] },
});
},
});
const onSubmit = (e) => {
e.preventDefault();
if (name === '' || email === '' || phone === '') {
return alert('Please fill in all fields');
}
addClient(name, email, phone);
setName('');
setEmail('');
setPhone('');
};
return (
<>
<button
type='button'
className='btn btn-secondary'
data-toggle='modal'
data-target='#addClientModal'
>
<div className='d-flex align-items-center'>
<FaUser className='icon' />
<div>Add Client</div>
</div>
</button>
<div
className='modal fade'
id='exampleModal'
aria-labelledby='AddClientModalLabel'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title' id='AddClientModalLabel'>
Add Client
</h5>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
></button>
</div>
<div className='modal-body'>
<from onSubmit={onSubmit}>
<div className='mb-3'>
<label className='form-label'>
Name
<input
type='name'
className='from-control'
id='name'
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</div>
<div className='mb-3'>
<label className='form-label'>
Email
<input
type='email'
className='from-control'
id='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
</div>
<div className='mb-3'>
<label className='form-label'>
Phone
<input
type='phone'
className='from-control'
id='phone'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</label>
</div>
<button
type='submit'
data-bs-dismiss='modal'
className='btn btn-secondary'
>
Submit
</button>
</from>
</div>
</div>
</div>
</div>
</>
);
}
⑫ client/srcに、mutationsフォルダとAddclientModal.jsxを作成する
import { useState } from 'react';
import { FaList } from 'react-icons/fa';
import { useMutation, useQuery } from '@apollo/client';
import { ADD_PROJECT } from '../mutations/projectMutations';
import { GET_PROJECTS } from '../queries/projectQueries';
import { GET_CLIENTS } from '../queries/clientQueries';
export default function AddClientModal() {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [clientId, setClientId] = useState('');
const [status, setStatus] = useState('new');
const [addProject] = useMutation(ADD_PROJECT, {
variables: { name, description, clientId, status },
update(cache, { data: { addProject } }) {
const { projects } = cache.readQuery({ query: GET_PROJECTS });
cache.writeQuery({
query: GET_CLIENTS,
data: { projects: [...projects, addProject] },
});
},
});
// Get Clients for select
const { loading, error, data } = useQuery(GET_PROJECTS);
const onSubmit = (e) => {
e.preventDefault();
if (name === '' || description === '' || status === '') {
return alert('Please fill in all fields');
}
addProject(name, description, clientId, status);
setName('');
setDescription('');
setStatus('new');
setClientId('');
};
if (loading) return null;
if (error) return `Something Went Wrong`;
return (
<>
{!loading && !error && (
<>
<button
type='button'
className='btn btn-primary'
data-toggle='modal'
data-target='#addProjectModal'
>
<div className='d-flex align-items-center'>
<FaList className='icon' />
<div>New Priject</div>
</div>
</button>
<div
className='modal fade'
id='exampleModal'
aria-labelledby='AddProjectModalLabel'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title' id='AddProjectModalLabel'>
New Project
</h5>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
></button>
</div>
<div className='modal-body'>
<from onSubmit={onSubmit}>
<div className='mb-3'>
<label className='form-label'>
Name
<input
type='name'
className='from-control'
id='name'
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</div>
<div className='mb-3'>
<label className='form-label'>
Description
<textarea
className='from-control'
id='description'
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
</label>
</div>
<div className='mb-3'>
<label className='form-label'>Stutas</label>
<select
id='status'
className='from-select'
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value='new'>Not Started</option>
<option value='progress'>In Progress</option>
<option value='completed'>Completed</option>
</select>
</div>
<div className='mb-3'>
<label className='from-label'>Client</label>
<select
id='clientId'
className='form-select'
value={clientId}
onChange={(e) => setClientId(e.target.value)}
>
<option value=''>Select Client</option>
{data.clients.map((client) => (
<option key={client.id} value={client.id}>
{client.name}
</option>
))}
</select>
</div>
<button
type='submit'
data-bs-dismiss='modal'
className='btn btn-primary'
>
Submit
</button>
</from>
</div>
</div>
</div>
</div>
</>
)}
</>
);
}
⑬ client/src/mutations/AddclientModal.jsxにBoostrap/modalのコードを加える
⑭ queriesにprojectQueries.jsを作成する
import { gql } from '@apollo/client';
const GET_PROJECTS = gql`
query getProjects {
projects {
id
name
status
}
}
`;
const GET_PROJECT = gql`
query getProject($id: ID!) {
project(id: $id) {
id
name
description
status
client {
id
name
email
phone
}
}
}
`;
export { GET_PROJECTS, GET_PROJECT };
⑮ client/src/compomentsにProjects.jsxを作成する
import Spinner from 'Spinner';
import { useQuery } from '@apollo/client';
import { GET_PROJECTS } from '../queries/projectQueries';
import ProjectCard from './ProjectCard';
export default function Projects() {
const { loading, error, data } = useQuery(GET_PROJECTS);
if (loading) return <Spinner />;
if (error) return <p>Something Went Wrong</p>;
return (
<>
{data.projects.length > 0 ? (
<div className='row mt-4'>
{data.projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
) : (
<p>NO PROJECTS</p>
)}
</>
);
}
⑯ client/srcにProjectCard.jsxを作成する
export default function ProjectsCard({ project }) {
return (
<div className='col-md-6'>
<div className='card mb-3'>
<div className='card-body'>
<div className='d-flex justify-content-between-align-items-center'>
<h5 className='card-title'>{project.name}</h5>
<a className='btn btn-light' href={`/projects/${project.id}`}>
View
</a>
</div>
<p className='small'>
Status:<strong>{project.status}</strong>
</p>
</div>
</div>
</div>
);
}
⑰ client/srcにpagesフォルダとHome.jsxを作成する
import Clients from '../components/Clients';
import Projects from '../components/Projects ';
import AddClientModal from '../components/AddClientModal';
import AddProjectModal from '../components/AddProjectModal';
export default function Home() {
return (
<>
<div className='d-flex gap-3 mb-4'>
<AddClientModal />
<AddProjectModal />
</div>
<Projects />
<hr />
<Clients />
</>
);
}
⑱ client/src/pagesにNotFound.jsxを作成する
import { FaExclamationTriangle } from 'react-icons/fa';
import { Link } from 'react-router-dom';
export default function NotFound() {
return (
<>
<div className='d-flex flex-column justify-content-center align-items-center mt-5'>
<FaExclamationTriangle className='text-danger' size='5em' />
</div>
<h1>404</h1>
<p className='lead'>Sorry, this page does not exist</p>
<Link to='/' className='btn btn-primary'>
Go Back
</Link>
</>
);
}
⑲ client/src/pagesにProject.jsxを作成する
import { Link, useParams } from 'react-router-dom';
import Spinner from '../components/Spinner';
import ClientInfo from '../components/ClientInfo';
import DeleteProjectButton from '../components/DeleteProjectButton';
import EditProjectForm from '../components/EditProjectForm';
import { useQuery } from '@apollo/client';
import { GET_PROJECT } from '../queries/projectQueries';
export default function Project() {
const { id } = useParams();
const { loading, error, data } = useQuery(GET_PROJECT, { variables: { id } });
if (loading) return <Spinner />;
if (error) return <p>Something Went Wrong</p>;
return (
<>
{!loading && !error && (
<div className='mx-auto w-75 card p-5'>
<Link to='/' className='btn btn-light btn-sm w-25 d-inline ms-auto'>
Back
</Link>
<h1>{data.project.name}</h1>
<p>{data.project.description}</p>
<h5 className='mt-3'>Project Status</h5>
<p className='lead'>{data.project.status}</p>
<ClientInfo client={data.project.client} />
<EditProjectForm project={data.project} />
<DeleteProjectButton projectId={data.project.id} />
</div>
)}
</>
);
}
⑳ client/src/compomentsにClientInfo.jsxを作成する
import { FaEnvelope, FaPhone, FaId, FaIdBadge } from 'react-icons/fa';
export default function ClientInfo({ client }) {
return (
<>
<h5 className='mt-5'>
Client Information
<ul className='list-group'>
<li className='list-group-item'>
<FaIdBadge className='icon' />
{client.name}
</li>
<li className='list-group-item'>
<FaEnvelope className='icon' />
{client.email}
</li>
<li className='list-group-item'>
<FaPhone className='icon' />
{client.phone}
</li>
</ul>
</h5>
</>
);
}
㉑ client/src/にmutationsフォルダとclientMutations.jsを作成する
import { gql } from '@apollo/client';
const ADD_CLIENT = gql`
mutation AddClient($name: String!, $email: String!, $phone: String!) {
AddClient(name: $name, email: $email, phone: $phone) {
id
name
email
phone
}
}
`;
const DELETE_CLIENTS = gql`
mutation deleteClient($id: ID!){
deleteClient($id: $id){
id
name
email
phone
}
}
`;
export { ADD_CLIENT, DELETE_CLIENTS };
㉒ client/src/mutationsにprojectMutations.jsを作成する
import { gql } from '@apollo/client';
const ADD_PROJECT = `
mutation AddProject($name: String! $description: String! $status: ProjectStatus! $clientId: ID!) {
addProject(name: $name description: $description status: $status clientId: $clientId){
id
name
description
status
client{
id
name
email
phone
}
}
`;
const DELETE_PRPJECT = gql`
mutation DeleteProject($id: ID!) {
deleteProject(id: $id) {
id
}
}
`;
const UPDATE_PROJECT = `
mutation UpdateProject($name: String!, $description: String!,$status: ProjectStatusUpdate!) {
updateProject(name: $name, description: $description, status: $status){
id
name
description
status
client{
id
name
email
phone
}
}
`;
export { ADD_PROJECT, DELETE_PRPJECT, UPDATE_PROJECT };
㉓ client/src/compomentsにAddProjects.jsxを作成する
import { useState } from 'react';
import { FaList } from 'react-icons/fa';
import { useMutation, useQuery } from '@apollo/client';
import { ADD_PROJECT } from '../mutations/projectMutations';
import { GET_PROJECTS } from '../queries/projectQueries';
import { GET_CLIENTS } from '../queries/clientQueries';
export default function AddProjectModal() {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [clientId, setClientId] = useState('');
const [status, setStatus] = useState('new');
const [addProject] = useMutation(ADD_PROJECT, {
variables: { name, description, clientId, status },
update(cache, { data: { addProject } }) {
const { projects } = cache.readQuery({ query: GET_PROJECTS });
cache.writeQuery({
query: GET_PROJECTS,
data: { projects: [...projects, addProject] },
});
},
});
// Get Clients for select
const { loading, error, data } = useQuery(GET_CLIENTS);
const onSubmit = (e) => {
e.preventDefault();
if (name === '' || description === '' || status === '') {
return alert('Please fill in all fields');
}
addProject(name, description, clientId, status);
setName('');
setDescription('');
setStatus('new');
setClientId('');
};
if (loading) return null;
if (error) return 'Something Went Wrong';
return (
<>
{!loading && !error && (
<>
<button
type='button'
className='btn btn-primary'
data-bs-toggle='modal'
data-bs-target='#addProjectModal'
>
<div className='d-flex align-items-center'>
<FaList className='icon' />
<div>New Project</div>
</div>
</button>
<div
className='modal fade'
id='addProjectModal'
aria-labelledby='addProjectModalLabel'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title' id='addProjectModalLabel'>
New Project
</h5>
<button
type='button'
className='btn-close'
data-bs-dismiss='modal'
aria-label='Close'
></button>
</div>
<div className='modal-body'>
<form onSubmit={onSubmit}>
<div className='mb-3'>
<label className='form-label'>Name</label>
<input
type='text'
className='form-control'
id='name'
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className='mb-3'>
<label className='form-label'>Description</label>
<textarea
className='form-control'
id='description'
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
</div>
<div className='mb-3'>
<label className='form-label'>Status</label>
<select
id='status'
className='form-select'
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value='new'>Not Started</option>
<option value='progress'>In Progress</option>
<option value='completed'>Completed</option>
</select>
</div>
<div className='mb-3'>
<label className='form-label'>Client</label>
<select
id='clientId'
className='form-select'
value={clientId}
onChange={(e) => setClientId(e.target.value)}
>
<option value=''>Select Client</option>
{data.clients.map((client) => (
<option key={client.id} value={client.id}>
{client.name}
</option>
))}
</select>
</div>
<button
type='submit'
data-bs-dismiss='modal'
className='btn btn-primary'
>
Submit
</button>
</form>
</div>
</div>
</div>
</div>
</>
)}
</>
);
}
㉔ client/src/compomentsにDeleteProjectButton.jsxを作成する
import { useNavigate } from 'react-router-dom';
import { FaTrash } from 'react-icons/fa';
import { DELETE_PROJECT } from 'mutations/projectMutations';
import { GET_PROJECTS } from '../queries/projectQueries';
import { useMutation } from '@apollo/client';
export default function DeleteProjectButton({ projectId }) {
const navigate = useNavigate();
const [deleteProject] = useMutation(DELETE_PROJECT, {
variables: { id: projectId },
onCompleted: () => navigate('/'),
refetchQueries: [{ query: GET_PROJECTS }],
});
return (
<div className='d-flex mt-5 ms-auto'>
<button className='btn btn-danger m-2' onClick={deleteProject}>
<FaTrash className='icon' />
Delete Project
</button>
</div>
);
}
㉕ client/src/compomentsにEditProjectForm.jsxを作成する
import { useState } from 'react';
import { useMutation } from '@apollo/client';
import { GET_PROJECT } from '../queries/projectQueries';
import { UPDATE_PROJECT } from '../mutations/projectMutations';
export default function EditProjectForm({ project }) {
const [name, setName] = useState(project.name);
const [description, setDescription] = useState(project.description);
const [status, setStatus] = useState('new');
const [updateProject] = useMutation(UPDATE_PROJECT, {
variables: { id: project.id, name, description, status },
refetchQueries: [{ query: GET_PROJECT, variables: { id: project.id } }],
});
const onSubmit = (e) => {
e.preventDefault();
if (!name || !description || !status) {
return alert('Please fill out all fields');
}
updateProject(name, description, status);
};
return (
<div className='mt-5'>
<h3>Update Project Details</h3>
<form onSubmit={onSubmit}>
<div className='mb-3'>
<label className='form-label'>
Name
<input
type='name'
className='from-control'
id='name'
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</div>
<div className='mb-3'>
<label className='form-label'>
Description
<textarea
className='from-control'
id='description'
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
</label>
</div>
<div className='mb-3'>
<label className='form-label'>Stutas</label>
<select
id='status'
className='from-select'
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value='new'>Not Started</option>
<option value='progress'>In Progress</option>
<option value='completed'>Completed</option>
</select>
</div>
<button type='submit' className='btn btn-primary'>
Submit
</button>
</form>
</div>
);
}
参考サイト
Building a Dynamic Portfolio Website with Java, Spring Boot, and Thymeleaf!
GraphQLとRESTの違い