環境の準備
①ターミナルでreactアプリケーションを作成する。
$ npx create-react-app <プロジェクト名>
% cd <プロジェクト名>
% npm start
② 必要なパッケージをインストールする。
$ npm install styled-components
$ npm install @mui/material @emotion/react @emotion/styled
$ npm install @mui/icons-material
$ npm install firebase
$ npm install firebase-tools
$ npm install react-router-dom
$ npm install --save @reduxjs/toolkit react-redux @types/react-redux
③ 不要なファイルを削除する。
App.css, App.test.js, logo.svg, setupTest.js
コンポーネント・ファイル構成
public
├── img
src └── logo1.png
├── app
└── store.js
├── components
├── Drive.js
├── FileContainer.js
├── FileList.js
├── Folder.js
├── Header.js
├── Login.js
├── Model.js
├── PhotoDisplay.js
├── photoModel.js
├── SideBar.js
└── SideBarList.js
├── firebase
└── firebase.js
├── slices
├── bool
└── boolSlice.js
├── channel
└── channelSlice.js
├── photodisplay
└── photoSlice.js
└── user
└── userSlice.js
├── App.js
├── index.css
├── index.js
src/app/store.js
import { configureStore } from "@reduxjs/toolkit";
import boolReducer from "../slices/Bool/boolSlice";
import photoReducer from "../slices/photodisplay/photoSlice";
import userReducer from "../slices/user/userSlice";
import folderReducer from "../slices/channel/channelSlice";
export const store = configureStore({
reducer: {
bool: boolReducer,
photos: photoReducer,
user: userReducer,
folder: folderReducer,
},
});
src/components/Drive.js
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import FileList from "./FileList";
import FileContainer from "./FileContainer";
import { useDispatch } from "react-redux";
import { setBoolean } from "../slices/Bool/boolSlice";
import db from "../firebase/firebase";
import { collection, onSnapshot, orderBy, query } from "firebase/firestore";
import { setFolder } from "../slices/channel/channelSlice";
function Drive() {
const dispatch = useDispatch();
const [folders, setFolders] = useState([]);
const [fileData, setFileData] = useState([]);
useEffect(() => {
dispatch(setFolder({ folderId: null, folderName: null }));
}, [dispatch]);
useEffect(() => {
return onSnapshot(
query(collection(db, "folder"), orderBy("timestamp", "asc")),
(snapshot) => setFolders(snapshot.docs)
);
}, []);
useEffect(() => {
return onSnapshot(
query(collection(db, "post"), orderBy("timestamp", "asc")),
(snapshot) => setFileData(snapshot.docs)
);
}, []);
return (
<Container onClick={() => dispatch(setBoolean({ modelBools: false }))}>
<Title>
<span>My Drive</span>
<ArrowDropDownIcon />
</Title>
<FileContent>
<SemiTitle>Suggested</SemiTitle>
<GridContainer>
{fileData?.map((data) => {
return (
<FileList
img={data?.data().Image}
key={data?.id}
id={data?.id}
title={data?.data().photoTitle}
uid={data?.data().uid}
/>
);
})}
</GridContainer>
<Margin>
<SemiTitle>Folders</SemiTitle>
<GridContainer>
{folders?.map((data) => (
<FileContainer
key={data.id}
id={data.id}
title={data?.data().name}
/>
))}
</GridContainer>
</Margin>
</FileContent>
</Container>
);
}
export default Drive;
const Container = styled.div`
flex-grow: 1;
position: relative;
display: flex;
flex-direction: column;
padding: 15px 30px;
`;
const Title = styled.div`
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
padding-bottom: 13px;
svg {
margin-left: 10px;
color: #5f6368;
}
span {
font-family: Google Sans, Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
font-weight: 400;
font-size: 18px;
color: #202124;
}
`;
const FileContent = styled.div`
display: flex;
flex-direction: column;
padding-top: 20px;
overflow-y: scroll;
flex-grow: 1;
max-height: 100vh;
margin-bottom: 30px;
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 20px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 20px;
transition: all 200ms ease-out;
max-height: 100px;
:hover {
background-color: rgba(0, 0, 0, 0.3);
}
}
`;
const SemiTitle = styled.div`
font-size: 14px;
font-weight: 500;
text-transform: capitalize;
color: #5f6368;
`;
const GridContainer = styled.div`
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 20px 0;
`;
const Margin = styled.div``;
src/components/FileContainer.js
import { Folder } from "@mui/icons-material";
import React from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { setFolder } from "../slices/channel/channelSlice";
function FileContainer({ title, id }) {
const dispatch = useDispatch();
const navigate = useNavigate();
const SelectChannel = () => {
if (id) {
dispatch(setFolder({ folderId: id, folderName: title }));
navigate(`/folder/${title}/${id}`);
}
};
return (
<Container onClick={SelectChannel}>
<Folder />
<span>{title}</span>
</Container>
);
}
export default FileContainer;
const Container = styled.div`
width: 287.5px;
height: 48px;
display: flex;
align-items: center;
border: 1px solid rgba(0, 0, 0, 0.35);
border-radius: 4px;
svg {
height: 24px;
width: 24px;
color: rgba(95, 99, 104);
margin-left: 4px;
}
span {
font-size: 13px;
margin-left: 10px;
text-transform: capitalize;
}
`;
src/components/FileList.js
import { InsertPhoto } from "@mui/icons-material";
import { useDispatch } from "react-redux";
import styled from "styled-components";
import { setPhotoDisplay } from "../slices/photodisplay/photoSlice";
const FileList = ({ img, title }) => {
const dispatch = useDispatch();
const PhotoSelector = () => {
dispatch(setPhotoDisplay({ photo: img, title: title }));
};
return (
<Container>
<PhotoContainer onClick={PhotoSelector}>
<img src={img} alt="" />
</PhotoContainer>
<PhotoTitle>
<InsertPhoto />
<span>{title}</span>
</PhotoTitle>
</Container>
);
};
export default FileList;
const Container = styled.div`
max-width: 300px;
max-height: 400px;
height: 209px;
display: flex;
flex-direction: column;
border-radius: 20px;
margin: 10px 0;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 /0.1);
`;
const PhotoContainer = styled.div`
height: 60%;
width: 100%;
background-color: lightgray;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
img {
height: 100%;
width: 100%;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
object-fit: contain;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
`;
const PhotoTitle = styled.div`
display: flex;
align-items: center;
margin-top: 20px;
margin-left: 10px;
svg {
color: #70b5f9;
}
span {
color: rgba(0, 0, 0, 0.72);
margin-left: 28px;
padding-bottom: 4px;
font-size: 13px;
font-weight: 600;
}
`;
src/components/Folder.js
import { collection, onSnapshot, orderBy, query } from "firebase/firestore";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import db from "../firebase/firebase";
import { setBoolean } from "../slices/Bool/boolSlice";
import { selectFolderId } from "../slices/channel/channelSlice";
import FileList from "./FileList";
function Folder() {
const dispatch = useDispatch();
const folderId = useSelector(selectFolderId);
const [filePhoto, setFilePhoto] = useState([]);
const navigate = useNavigate();
useEffect(() => {
if (!folderId) {
navigate("/");
document.location.reload();
}
}, [folderId, navigate]);
useEffect(() => {
return onSnapshot(
query(
collection(db, "folder", folderId, "folderTree"),
orderBy("timestamp", "asc")
),
(snapshot) => {
setFilePhoto(snapshot.docs);
}
);
}, [folderId]);
return (
<Container onClick={() => dispatch(setBoolean({ modelBools: false }))}>
<GridContainer>
{filePhoto?.map((data) => (
<FileList
key={data?.id}
img={data?.data().Image}
title={data?.data().photoTitle}
/>
))}
</GridContainer>
</Container>
);
}
export default Folder;
const Container = styled.div`
flex-grow: 1;
position: relative;
display: flex;
flex-direction: column;
padding: 15px 30px;
`;
const GridContainer = styled.div`
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 20px 0;
`;
src/components/FolderModel.js
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components";
import db from "../firebase/firebase";
import { selectFolderBool, setBoolean } from "../slices/Bool/boolSlice";
import { selectUid } from "../slices/user/userSlice";
function FolderModel() {
const folderBool = useSelector(selectFolderBool);
const dispatch = useDispatch();
const uid = useSelector(selectUid);
const [folderNames, setFolderNames] = useState("");
const [loading, setLoading] = useState(false);
const Submit = async (e) => {
e.preventDefault();
if (loading) return;
setLoading(true);
if (folderNames.length < 1) return;
await addDoc(collection(db, "folder"), {
name: folderNames,
timestamp: serverTimestamp(),
uid: uid,
});
setLoading(false);
dispatch(setBoolean({ folderBool: false }));
setFolderNames("");
};
return (
<Container folder={folderBool}>
<Wrapper onSubmit={Submit}>
<Title>New Folder</Title>
<InputContainer>
<input
type="text"
value={folderNames}
onChange={(e) => setFolderNames(e.target.value)}
placeholder="create Folder"
/>
</InputContainer>
<Button>
<button onClick={() => dispatch(setBoolean({ folderBool: false }))}>
Cancel
</button>
<button className="create" disabled={loading} onClick={Submit}>
{loading ? "Creating" : "CreateFolder"}
</button>
</Button>
</Wrapper>
</Container>
);
}
export default FolderModel;
const Container = styled.div`
z-index: 9999;
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
transition: all 200ms ease-out;
transform: ${(props) =>
props.folder ? "translateY(0)" : "translateY(100%)"};
`;
const Wrapper = styled.div`
width: 500px;
height: 130px;
background-color: white;
border-radius: 20px;
position: relative;
`;
const Title = styled.span`
font-size: 20px;
margin: 20px;
`;
const InputContainer = styled.div`
display: flex;
flex-direction: row;
flex: 1;
margin: 20px;
input {
border: none;
flex: 1;
:focus {
outline: none;
}
}
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
`;
const Button = styled.div`
position: absolute;
bottom: 5px;
right: 10px;
display: flex;
align-items: center;
button {
padding: 10px 20px;
border-radius: 4px;
font-weight: 500;
background-color: #ef4444;
border: none;
color: white;
margin: 0 15px;
cursor: pointer;
}
.create {
background-color: #3b82f6;
}
`;
src/components/Header.js
import { Avatar, ButtonGroup } from "@mui/material";
import styled from "styled-components";
import SearchIcon from "@mui/icons-material/Search";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import AppsOutlinedIcon from "@mui/icons-material/AppsOutlined";
import { useSelector } from "react-redux";
import { selectPhoto } from "../slices/user/userSlice";
import { Link } from "react-router-dom";
function Header() {
const photo = useSelector(selectPhoto);
return (
<Container>
<Wrapper>
<Link to="/">
<Logo>
<img src="/img/logo1.png" alt="" />
<span>Drive</span>
</Logo>
</Link>
<InputContainer>
<SearchContainer>
<ButtonGroup>
<SearchIcon />
</ButtonGroup>
<input type="text" placeholder="Search in Drive" />
</SearchContainer>
</InputContainer>
<RightContainer>
<LeftSection>
<HelpOutlineIcon />
<SettingsOutlinedIcon />
</LeftSection>
<RightSection>
<AppsOutlinedIcon className="app" />
<Avatar src={photo} />
</RightSection>
</RightContainer>
</Wrapper>
</Container>
);
}
export default Header;
const Container = styled.div`
position: sticky;
top: 0;
z-index: 999;
background-color: #ffffff;
padding: 2px;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
`;
const Wrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin: 10px 20px;
`;
const Logo = styled.div`
display: flex;
align-items: center;
img {
width: 40px;
height: 40px;
}
span {
font-family: "Product Sans", Arial, sans-serif;
color: #5f6368;
font-size: 22px;
padding-left: 8px;
}
`;
const InputContainer = styled.div`
flex: 1;
`;
const RightContainer = styled.div`
display: flex;
align-items: center;
`;
const SearchContainer = styled.div`
width: 64%;
height: 50px;
margin: 0 auto;
display: flex;
align-items: center;
background-color: rgba(0, 0, 0, 0.09);
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgb(0 0 0 /0.05);
svg {
margin-left: 10px;
color: #5f6368;
}
input {
font-size: 16px;
width: 90%;
height: 80%;
font-family: Sans, Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
margin: 0 auto;
background-color: transparent;
:focus {
outline: none;
}
border: none;
}
`;
const RightSection = styled.div`
display: flex;
align-items: center;
svg {
color: #5f6368;
padding: 5px;
cursor: pointer;
border-radius: 50%;
transition: all 200ms ease-out;
:hover {
background-color: rgba(0, 0, 0, 0.09);
}
}
.app {
margin-right: 15px;
}
`;
const LeftSection = styled(RightSection)`
margin-right: 40px;
svg {
margin: 0 10px;
}
`;
src/components/Login.js
import { signInWithPopup } from "firebase/auth";
import { useDispatch } from "react-redux";
import styled from "styled-components";
import { auth, provider } from "../firebase/firebase";
import { setLogIn } from "../slices/user/userSlice";
function Login() {
const dispatch = useDispatch();
const LoginwithGoogle = async () => {
await signInWithPopup(auth, provider).then((res) => {
let user = res.user;
dispatch(setLogIn({ uid: user.uid, photo: user.photoURL }));
});
};
return (
<Container>
<Button onClick={LoginwithGoogle}>Sign In with Google</Button>
</Container>
);
}
export default Login;
const Container = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
`;
const Button = styled.button`
font-weight: 600;
padding: 15px 20px;
border: none;
cursor: pointer;
border-radius: 15px;
transition: all 200ms ease-out;
:hover {
transform: scale(1.09);
}
`;
src/components/Model.js
import { AddPhotoAlternate, CreateNewFolder } from "@mui/icons-material";
import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components";
import { selectModelBool, setBoolean } from "../slices/Bool/boolSlice";
function Model() {
const selectModel = useSelector(selectModelBool);
const dispatch = useDispatch();
return (
<Container bool={selectModel}>
<Wrapper>
<Header>
<Wraps onClick={() => dispatch(setBoolean({ folderBool: true }))}>
<CreateNewFolder />
<span>Folder</span>
</Wraps>
</Header>
<Header>
<Wraps onClick={() => dispatch(setBoolean({ photo: true }))}>
<AddPhotoAlternate />
<span>Photo</span>
</Wraps>
</Header>
</Wrapper>
</Container>
);
}
export default Model;
const Container = styled.div`
position: fixed;
top: 100px;
left: 20px;
background-color: white;
height: 200px;
width: 300px;
border-radius: 20px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 /0.1);
transition: all 200ms ease-out;
transform: ${(props) =>
props.bool ? `translateX(-0%)` : `translateX(-200%)`};
`;
const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
const Header = styled.div`
padding-bottom: 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
margin: 5px 0;
`;
const Wraps = styled.div`
display: flex;
align-items: center;
padding: 5px 0;
cursor: pointer;
padding-left: 20px;
margin-top: 10px;
transition: all 200ms ease-out;
&:hover {
background-color: rgba(0, 0, 0, 0.2);
}
svg {
color: rgba(0, 0, 0, 0.5);
}
span {
margin-left: 10px;
}
`;
src/components/PhotoDisplay.js
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components";
import {
selectPhotoDisplay,
selectTitleDisplay,
setPhotoDisplay,
} from "../slices/photodisplay/photoSlice";
function PhotoDisplay() {
const img = useSelector(selectPhotoDisplay);
const title = useSelector(selectTitleDisplay);
const dispatch = useDispatch();
return (
<Container
show={img}
onClick={() => dispatch(setPhotoDisplay({ photo: null }))}
>
<span>{title}</span>
<PhotoDisplays>
<img src={img} alt="" />
</PhotoDisplays>
</Container>
);
}
export default PhotoDisplay;
const Container = styled.div`
z-index: 9999;
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
transform: ${(props) => (props.show ? "translateY(0)" : "translateY(100%)")};
span {
font-size: 40px;
color: white;
position: absolute;
top: 0;
}
`;
const PhotoDisplays = styled.div`
max-width: 760px;
max-height: 475px;
height: 475px;
width: 760px;
img {
width: 100%;
height: 100%;
}
`;
src/components/PhotoModel.js
import { CameraAlt, Close } from "@mui/icons-material";
import {
addDoc,
collection,
doc,
serverTimestamp,
updateDoc,
} from "firebase/firestore";
import { getDownloadURL, ref, uploadString } from "firebase/storage";
import React, { useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components";
import db, { storage } from "../firebase/firebase";
import { selectPhotoBool, setBoolean } from "../slices/Bool/boolSlice";
import { selectFolderId } from "../slices/channel/channelSlice";
import { selectUid } from "../slices/user/userSlice";
function PhotoModel() {
const [input, setInput] = useState("");
const [selectedImage, setSelectedImage] = useState(null);
const ImageRef = useRef(null);
const Photo = useSelector(selectPhotoBool);
const [loading, setLoading] = useState(false);
const uid = useSelector(selectUid);
const folderId = useSelector(selectFolderId);
const dispatch = useDispatch();
const Submit = async (e) => {
e.preventDefault();
if (loading) return;
setLoading(true);
if (input.length < 1) return;
if (folderId) {
const doces = await addDoc(
collection(db, "folder", folderId, "folderTree"),
{
uid: uid,
photoTitle: input,
timestamp: serverTimestamp(),
}
);
const images = ref(storage, `folder/${folderId}/${doces.id}image`);
await uploadString(images, selectedImage, "data_url").then(
async (snapshot) => {
const downloadUrl = await getDownloadURL(images);
await updateDoc(doc(db, "folder", folderId, "folderTree", doces.id), {
Image: downloadUrl,
});
}
);
} else {
const docs = await addDoc(collection(db, "post"), {
uid: uid,
photoTitle: input,
timestamp: serverTimestamp(),
});
const images = ref(storage, `post/${docs.id}/image`);
await uploadString(images, selectedImage, "data_url").then(
async (snapshot) => {
const downloadUrl = await getDownloadURL(images);
await updateDoc(doc(db, "post", docs.id), {
Image: downloadUrl,
});
}
);
}
setSelectedImage(null);
setInput("");
setLoading(false);
dispatch(setBoolean({ photo: false }));
};
const SelectImages = (e) => {
const Reader = new FileReader();
if (e.target.files[0]) {
Reader.readAsDataURL(e.target.files[0]);
Reader.onload = (Event) => {
setSelectedImage(Event.target.result);
};
}
};
return (
<Container show={Photo}>
<CloseIcon>
<Close onClick={() => dispatch(setBoolean({ photo: false }))} />
</CloseIcon>
<Wrapper onSubmit={Submit}>
<ImageContainer>
{selectedImage ? (
<img
src={selectedImage}
alt=""
onClick={() => setSelectedImage(null)}
/>
) : (
<CameraContainer>
<CameraAlt onClick={() => ImageRef.current.click()} />
</CameraContainer>
)}
<input type="file" hidden ref={ImageRef} onChange={SelectImages} />
</ImageContainer>
<TextContainer>
<input
type="text"
placeholder="Enter photo Title"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</TextContainer>
<ButtonContainer>
<button onClick={Submit} disabled={loading}>
{loading ? "Submitting..." : "Submit"}
</button>
</ButtonContainer>
</Wrapper>
</Container>
);
}
export default PhotoModel;
const Container = styled.div`
z-index: 9999;
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
transform: ${(props) => (props.show ? "translateY(0)" : "translateY(100%)")};
`;
const Wrapper = styled.form`
height: 400px;
width: 400px;
background-color: white;
border-radius: 20px;
position: relative;
z-index: 999;
`;
const ImageContainer = styled.div`
height: 50%;
margin-bottom: 20px;
width: 100%;
img {
width: 100%;
height: 100%;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
}
`;
const TextContainer = styled.div`
flex: 1;
border-bottom: 1px solid black;
margin: 0 20px;
margin-top: 27px;
input {
display: flex;
border: none;
font-size: 18px;
text-transform: capitalize;
width: 100%;
border: none;
:focus {
outline: none;
}
}
`;
const ButtonContainer = styled.div`
position: absolute;
bottom: 30px;
right: 30px;
button {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
border: none;
box-shadow: 0 1px 2px 0 rgb(0 0 0 /0.05);
background-color: #3b82f6;
color: white;
transition: all 200ms ease-out;
:hover {
transform: scale(1.001);
}
:active {
transform: scale(1.009);
}
}
`;
const CameraContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.MuiSvgIcon-root {
width: 2.5rem !important;
height: 2.5rem;
color: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
`;
const CloseIcon = styled.div`
position: absolute;
top: 10px;
right: 10px;
svg {
cursor: pointer;
width: 2rem;
height: 2rem;
color: white;
}
`;
src/components/SideBar.js
import styled from "styled-components";
import SideBarList from "./SideBarList";
import TabletAndroidOutlinedIcon from "@mui/icons-material/TabletAndroidOutlined";
import ComputerOutlinedIcon from "@mui/icons-material/ComputerOutlined";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import AccessTimeOutlinedIcon from "@mui/icons-material/AccessTimeOutlined";
import StarBorderOutlinedIcon from "@mui/icons-material/StarBorderOutlined";
import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
import CloudOutlinedIcon from "@mui/icons-material/CloudOutlined";
import { useDispatch } from "react-redux";
import { setBoolean } from "../slices/Bool/boolSlice";
const SideBar = () => {
const dispatch = useDispatch();
return (
<Container>
<Wrapper>
<NewChannel onClick={() => dispatch(setBoolean({ modelBools: true }))}>
<span>Add new</span>
</NewChannel>
<div>
<SideBarList Icon={<TabletAndroidOutlinedIcon />} title="My Drive" />
<SideBarList Icon={<ComputerOutlinedIcon />} title="computer" />
<SideBarList Icon={<PeopleAltOutlinedIcon />} title="share" />
<SideBarList
Icon={<AccessTimeOutlinedIcon />}
title="Recently used items"
/>
<SideBarList Icon={<StarBorderOutlinedIcon />} title="With a star" />
<SideBarList Icon={<DeleteOutlinedIcon />} title="Bin" />
<UnderLine />
<SideBarList Icon={<CloudOutlinedIcon />} title="Storage" />
</div>
</Wrapper>
</Container>
);
};
export default SideBar;
const Container = styled.div`
height: 100%;
display: flex;
flex-direction: column;
padding: 10px;
`;
const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
const NewChannel = styled.div`
width: 118px;
height: 48px;
background-color: white;
border-radius: 24px;
padding: 2px;
cursor: pointer;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 01);
transition: all 200ms ease-out;
&:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
span {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
&::before {
content: ;
height: 36px;
padding-right: 10px;
}
}
`;
const UnderLine = styled.div`
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
`;
src/components/SideBarList.js
import styled from "styled-components";
const SideBarList = ({ title, Icon }) => {
return (
<Container>
{Icon}
<span>{title}</span>
</Container>
);
};
export default SideBarList;
const Container = styled.div`
display: flex;
align-items: center;
font-size: 13px;
font-weight: 500;
margin: 20px 0;
padding: 5px 10px;
border-radius: 20px;
cursor: pointer;
transition: all 200ms ease-out;
:hover {
background-color: rgba(0, 0, 0, 0.07);
}
svg {
margin-left: 10px;
fill: #5f6368;
height: 24px;
}
span {
margin-left: 15px;
height: 24px;
line-height: 26px;
text-transform: capitalize;
color: #5f6368;
}
`;
src/firebase/firebase.js
import { getApp, getApps, initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth, GoogleAuthProvider } from "firebase/auth";
import { getStorage } from "firebase/storage";
const firebaseConfig = {
apiKey: " ",
authDomain: " ",
projectId: " ",
storageBucket: " ",
messagingSenderId: " ",
appId: " ",
};
!getApps().length ? initializeApp(firebaseConfig) : getApp();
const db = getFirestore();
const auth = getAuth();
const storage = getStorage();
const provider = new GoogleAuthProvider();
export default db;
export { auth, storage, provider };
src/slices/Bool/boolSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
folderBool: false,
modeleBool: false,
photo: false,
};
const booleanSlice = createSlice({
name: "bool",
initialState,
reducers: {
setBoolean: (state, action) => {
state.folderBool = action.payload.folderBool;
state.modelBools = action.payload.modelBools;
state.photo = action.payload.photo;
},
},
});
export const { setBoolean } = booleanSlice.actions;
export const selectFolderBool = (state) => state.bool.folderBool;
export const selectModelBool = (state) => state.bool.modelBools;
export const selectPhotoBool = (state) => state.bool.photo;
export default booleanSlice.reducer;
src/slices/channel/channelSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
folderId: null,
folderName: null,
};
const folderSlice = createSlice({
name: "folder",
initialState,
reducers: {
setFolder: (state, action) => {
state.folderId = action.payload.folderId;
state.folderName = action.payload.folderName;
},
},
});
export const { setFolder } = folderSlice.actions;
export const selectFolderId = (state) => state.folder.folderId;
export const selectFolderName = (state) => state.folder.folderName;
export default folderSlice.reducer;
src/slices/photodisplay/photoSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
photo: null,
title: null,
};
const photoSlice = createSlice({
name: "photo",
initialState,
reducers: {
setPhotoDisplay: (state, action) => {
state.title = action.payload.title;
state.photo = action.payload.photo;
},
},
});
export const { setPhotoDisplay } = photoSlice.actions;
export const selectPhotoDisplay = (state) => state.photos.photo;
export const selectTitleDisplay = (state) => state.photos.title;
export default photoSlice.reducer;
src/slices/user/userSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
uid: null,
photo: null,
};
const UserSlice = createSlice({
name: "user",
initialState,
reducers: {
SetLogIn: (state, action) => {
state.uid = action.payload.uid;
state.photo = action.payload.photo;
},
setLogOut: (state) => {
state.uid = null;
state.photo = null;
},
},
});
export const { setLogIn, setLogOut } = UserSlice.actions;
export const selectUid = (state) => state.user.uid;
export const selectPhoto = (state) => state.user.photo;
export default UserSlice.reducer;
App.js
import React, { useEffect } from "react";
import styled from "styled-components";
import Header from "./components/Header";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import SideBar from "./components/SideBar";
import Drive from "./components/Drive";
import Model from "./components/Model";
import FolderModel from "./components/FolderModel";
import Folder from "./components/Folder";
import PhotoModel from "./components/PhotoModel";
import PhotoDisplay from "./components/PhotoDisplay";
import { useSelector, useDispatch } from "react-redux";
import { selectUid, setLogIn, setLogOut } from "./slices/user/userSlice";
import Login from "./components/Login";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./firebase/firebase";
function App() {
const user = useSelector(selectUid);
const dispatch = useDispatch();
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
dispatch(setLogIn({ uid: user.uid, photo: user.photoURL }));
} else {
dispatch(setLogOut({ uid: null, photo: null }));
}
});
});
return (
<Router>
<Header />
{user ? (
<>
<Container>
<SideBar />
<Routes>
<Route path="/" element={<Drive />} />
<Route path="/folder/:name/:id" element={<Folder />} />
</Routes>
</Container>
<Model />
<PhotoModel />
<FolderModel />
<PhotoDisplay />
</>
) : (
<Login />
)}
</Router>
);
}
export default App;
const Container = styled.div`
display: flex;
`;
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { store } from "./app/store";
import { Provider } from "react-redux";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
参考サイト
Part 1 - Full Stack Social Video Blog - Reactjs | Firebase - For Beginners