diff --git a/.stylelintrc.json b/.stylelintrc.json index ef30add..a3f8ac1 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -4,7 +4,9 @@ "rules": { "order/properties-alphabetical-order": true, "selector-class-pattern": null, - "block-no-empty": null + "block-no-empty": null, + "custom-property-pattern": null, + "value-keyword-case": null }, "ignoreFiles": "node_modules/**/*" } diff --git a/README.md b/README.md index a7f066b..d68adb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# About This Project +TODO REAL is a web application that helps users manage their goals and tasks more effectively. It also works as a social platform, allowing users to share their to-do lists with others. By sending notifications and competing with friends, it makes achieving goals more engaging and easier. +This project is developed using Next.js, Firebase, and TypeScript. +For More information, please refer to the Top Page. +[TODO REAL](https://todo-real-c28fa.web.app/) ## Getting Started First run this command to install required packages: @@ -50,7 +54,7 @@ firebase emulators:start ``` ## Technical Documents -||Document in this project or related PR|Firebase| +||Document in this project or related PR|Firebase/Google Cloud - Official Documents| |-|-|-| |API|[API Document](./Documents/API.md)|[Firebase Cloud Functions](https://firebase.google.com/docs/functions)| |Database||[Firestore](https://firebase.google.com/docs/firestore)| diff --git a/functions/src/tasks.ts b/functions/src/tasks.ts index 63dad0d..76ae49b 100644 --- a/functions/src/tasks.ts +++ b/functions/src/tasks.ts @@ -32,7 +32,7 @@ export const createTasksOnGoalCreate = onDocumentCreated( try { const goalData = event.data.data(); - const marginTime = 2; + const marginTime = 5; // 期限のmarginTime分前にタスクを設定 const deadline = new Date( goalData.deadline.toDate().getTime() - marginTime * 60 * 1000 diff --git a/next.config.ts b/next.config.ts index 6e5e6ea..cd42c6d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,6 +6,9 @@ const nextConfig: NextConfig = { sassOptions: { includePaths: ["./src/styles"], }, + images: { + unoptimized: true, + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 14a4802..4efb17b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,13 @@ "firebase": "^11.0.1", "firebase-admin": "^12.7.0", "firebase-functions": "^6.1.1", + "framer-motion": "^11.16.0", "next": "^15.1.2", "openai": "^4.73.0", "react": "^19.0.0", "react-dom": "^19.0.0", "sass": "^1.80.6", - "styled-components": "^6.1.13" + "styled-components": "^6.1.14" }, "devDependencies": { "@types/node": "^22.9.0", @@ -5827,6 +5828,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.16.0.tgz", + "integrity": "sha512-oL2AWqLQuw0+CNEUa0sz3mWC/n3i147CckvpQn8bLRs30b+HxTxlRi0YR2FpHHhAbWV7DKjNdHU42KHLfBWh/g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.16.0", + "motion-utils": "^11.16.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -7507,6 +7535,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.16.0.tgz", + "integrity": "sha512-4bmEwajSdrljzDAYpu6ceEdtI4J5PH25fmN8YSx7Qxk6OMrC10CXM0D5y+VO/pFZjhmCvm2bGf7Rus482kwhzA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.16.0" + } + }, + "node_modules/motion-utils": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.16.0.tgz", + "integrity": "sha512-ngdWPjg31rD4WGXFi0eZ00DQQqKKu04QExyv/ymlC+3k+WIgYVFbt6gS5JsFPbJODTF/r8XiE/X+SsoT9c0ocw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9186,9 +9229,9 @@ "optional": true }, "node_modules/styled-components": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", - "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.14.tgz", + "integrity": "sha512-KtfwhU5jw7UoxdM0g6XU9VZQFV4do+KrM8idiVCH5h4v49W+3p3yMe0icYwJgZQZepa5DbH04Qv8P0/RdcLcgg==", "license": "MIT", "dependencies": { "@emotion/is-prop-valid": "1.2.2", diff --git a/package.json b/package.json index 044975e..92d05e2 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,13 @@ "firebase": "^11.0.1", "firebase-admin": "^12.7.0", "firebase-functions": "^6.1.1", + "framer-motion": "^11.16.0", "next": "^15.1.2", "openai": "^4.73.0", "react": "^19.0.0", "react-dom": "^19.0.0", "sass": "^1.80.6", - "styled-components": "^6.1.13" + "styled-components": "^6.1.14" }, "devDependencies": { "@types/node": "^22.9.0", diff --git a/public/appIcon.svg b/public/appIcon.svg new file mode 100644 index 0000000..7c4966d --- /dev/null +++ b/public/appIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/app.webp b/public/img/app.webp new file mode 100644 index 0000000..d520b47 Binary files /dev/null and b/public/img/app.webp differ diff --git a/public/img/blur.webp b/public/img/blur.webp new file mode 100644 index 0000000..ae358c3 Binary files /dev/null and b/public/img/blur.webp differ diff --git a/public/img/competition.webp b/public/img/competition.webp new file mode 100644 index 0000000..21dd223 Binary files /dev/null and b/public/img/competition.webp differ diff --git a/public/img/completed1.webp b/public/img/completed1.webp new file mode 100644 index 0000000..6e374c3 Binary files /dev/null and b/public/img/completed1.webp differ diff --git a/public/img/completed2.webp b/public/img/completed2.webp new file mode 100644 index 0000000..cd1a3b1 Binary files /dev/null and b/public/img/completed2.webp differ diff --git a/public/img/copyGoal.webp b/public/img/copyGoal.webp new file mode 100644 index 0000000..e465397 Binary files /dev/null and b/public/img/copyGoal.webp differ diff --git a/public/img/create.webp b/public/img/create.webp new file mode 100644 index 0000000..2257e28 Binary files /dev/null and b/public/img/create.webp differ diff --git a/public/img/failed.webp b/public/img/failed.webp new file mode 100644 index 0000000..b3f21ad Binary files /dev/null and b/public/img/failed.webp differ diff --git a/public/img/notification.webp b/public/img/notification.webp new file mode 100644 index 0000000..990c6ed Binary files /dev/null and b/public/img/notification.webp differ diff --git a/public/img/post.webp b/public/img/post.webp new file mode 100644 index 0000000..1d628d1 Binary files /dev/null and b/public/img/post.webp differ diff --git a/public/img/thumbnail.png b/public/img/thumbnail.png new file mode 100644 index 0000000..6fbef7c Binary files /dev/null and b/public/img/thumbnail.png differ diff --git a/public/img/todoListImage.webp b/public/img/todoListImage.webp new file mode 100644 index 0000000..9ac467e Binary files /dev/null and b/public/img/todoListImage.webp differ diff --git a/src/Components/Account/AuthForm.tsx b/src/Components/Account/AuthForm.tsx index e114896..92dbd31 100644 --- a/src/Components/Account/AuthForm.tsx +++ b/src/Components/Account/AuthForm.tsx @@ -2,13 +2,13 @@ import { signInAsGuest } from "@/utils/Auth/signInAnonymously"; import { signInWithGoogleAccount } from "@/utils/Auth/signInWithGoogleAccount"; import { signInWithMail } from "@/utils/Auth/signInWithMail"; import { signUpWithMail } from "@/utils/Auth/signUpWithMail"; +import Typography from "@mui/joy/Typography"; import Box from "@mui/material/Box"; import CircularProgress from "@mui/material/CircularProgress"; import Divider from "@mui/material/Divider"; import TextField from "@mui/material/TextField"; import ToggleButton from "@mui/material/ToggleButton"; import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; -import Typography from "@mui/material/Typography"; import { styled } from "@mui/material/styles"; import React, { useState } from "react"; import { RoundedButton } from "./LoggedInView"; @@ -216,6 +216,10 @@ export default function AuthForm() { "ゲストログイン" )} + + ゲストログインを使用するとアカウントを作成せずに閲覧できます。 + (投稿機能は使用できません。) + ); } diff --git a/src/Components/Animation/Animation.tsx b/src/Components/Animation/Animation.tsx new file mode 100644 index 0000000..8f0734c --- /dev/null +++ b/src/Components/Animation/Animation.tsx @@ -0,0 +1,29 @@ +import { AnimationConfigs, AnimationType } from "@/types/types"; +import { FC, ReactNode } from "react"; +import BottomIn from "./BottomIn"; +import CenterIn from "./CenterIn"; +import LeftIn from "./LeftIn"; +import RightIn from "./RightIn"; +import TopIn from "./TopIn"; + +interface AnimationProps { + animationType?: AnimationType; + children: ReactNode; +} + +export default function Animation({ + animationType = "center", + children, + ...configs +}: AnimationProps & AnimationConfigs) { + const animationComponents: Record> = { + left: LeftIn, + right: RightIn, + top: TopIn, + bottom: BottomIn, + center: CenterIn, + }; + + const AnimationComponent = animationComponents[animationType] || CenterIn; + return {children}; +} diff --git a/src/Components/Animation/BottomIn.tsx b/src/Components/Animation/BottomIn.tsx new file mode 100644 index 0000000..963074b --- /dev/null +++ b/src/Components/Animation/BottomIn.tsx @@ -0,0 +1,30 @@ +"use client"; +import { AnimationConfigs } from "@/types/types"; +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; + +// 下からフェードイン +export default function BottomIn({ + children, + duration = 0.7, + delay = 0, + distance = 40, + margin = 50, +}: AnimationConfigs) { + const ref = useRef(null); + const isInView = useInView(ref, { + once: true, + margin: `${-margin}px`, // 画面に入ってからmargin分超えたら表示 + }); + + return ( + + {children} + + ); +} diff --git a/src/Components/Animation/CenterIn.tsx b/src/Components/Animation/CenterIn.tsx new file mode 100644 index 0000000..66b7f16 --- /dev/null +++ b/src/Components/Animation/CenterIn.tsx @@ -0,0 +1,29 @@ +"use client"; +import { AnimationConfigs } from "@/types/types"; +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; + +// その場でフェードイン +export default function CenterIn({ + children, + duration = 0.6, + delay = 0, + margin = 50, +}: AnimationConfigs) { + const ref = useRef(null); + const isInView = useInView(ref, { + once: true, + margin: `${-margin}px`, // 画面に入ってからmargin分超えたら表示 + }); + + return ( + + {children} + + ); +} diff --git a/src/Components/Animation/LeftIn.tsx b/src/Components/Animation/LeftIn.tsx new file mode 100644 index 0000000..53971e7 --- /dev/null +++ b/src/Components/Animation/LeftIn.tsx @@ -0,0 +1,30 @@ +"use client"; +import { AnimationConfigs } from "@/types/types"; +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; + +// 左からフェードイン +export default function LeftIn({ + children, + duration = 0.7, + delay = 0, + distance = 60, + margin = 50, +}: AnimationConfigs) { + const ref = useRef(null); + const isInView = useInView(ref, { + once: true, + margin: `${-margin}px`, // 画面に入ってからmargin分超えたら表示 + }); + + return ( + + {children} + + ); +} diff --git a/src/Components/Animation/RightIn.tsx b/src/Components/Animation/RightIn.tsx new file mode 100644 index 0000000..2054888 --- /dev/null +++ b/src/Components/Animation/RightIn.tsx @@ -0,0 +1,30 @@ +"use client"; +import { AnimationConfigs } from "@/types/types"; +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; + +// 右からフェードイン +export default function RightIn({ + children, + duration = 0.7, + delay = 0, + distance = 60, + margin = 50, +}: AnimationConfigs) { + const ref = useRef(null); + const isInView = useInView(ref, { + once: true, + margin: `${-margin}px`, // 画面に入ってからmargin分超えたら表示 + }); + + return ( + + {children} + + ); +} diff --git a/src/Components/Animation/TopIn.tsx b/src/Components/Animation/TopIn.tsx new file mode 100644 index 0000000..b1d7117 --- /dev/null +++ b/src/Components/Animation/TopIn.tsx @@ -0,0 +1,30 @@ +"use client"; +import { AnimationConfigs } from "@/types/types"; +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; + +// 上からフェードイン +export default function TopIn({ + children, + duration = 0.7, + delay = 0, + distance = 40, + margin = 50, +}: AnimationConfigs) { + const ref = useRef(null); + const isInView = useInView(ref, { + once: true, + margin: `${-margin}px`, // 画面に入ってからmargin分超えたら表示 + }); + + return ( + + {children} + + ); +} diff --git a/src/Components/GoalModal/CreateGoalModal.tsx b/src/Components/GoalModal/CreateGoalModal.tsx index 35380e8..84a9b3f 100644 --- a/src/Components/GoalModal/CreateGoalModal.tsx +++ b/src/Components/GoalModal/CreateGoalModal.tsx @@ -29,7 +29,7 @@ export default function CreateGoalModal({ defaultDeadline?: string; }) { const [text, setText] = useState(""); - const [dueDate, setDueDate] = useState(""); + const [deadline, setDeadline] = useState(""); const { user } = useUser(); @@ -43,7 +43,7 @@ export default function CreateGoalModal({ convertedDate.getTime() - convertedDate.getTimezoneOffset() * 60000 ); localDate.setDate(localDate.getDate() + 1); // 1日後にする - setDueDate(localDate.toISOString().slice(0, 16)); + setDeadline(localDate.toISOString().slice(0, 16)); } else { // 初期値の指定が無い場合は次の日の23時に設定 const nextDay = new Date(); @@ -52,31 +52,21 @@ export default function CreateGoalModal({ const localNextDay = new Date( nextDay.getTime() - nextDay.getTimezoneOffset() * 60000 ); - setDueDate(localNextDay.toISOString().slice(0, 16)); + setDeadline(localNextDay.toISOString().slice(0, 16)); } }, [defaultText, defaultDeadline]); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); - // 過去の時間が入力されている場合 - if (new Date(dueDate).getTime() < Date.now()) { - showSnackBar({ - message: "過去の時間を指定することはできません", - type: "warning", - }); - return; - } - const postData: Goal = { userId: user?.userId as string, text: text, - deadline: new Date(dueDate), + deadline: new Date(deadline), }; try { - const data = await createGoal(postData); - console.log("Success:", data); + await createGoal(postData); showSnackBar({ message: "目標を作成しました", @@ -85,7 +75,7 @@ export default function CreateGoalModal({ triggerDashBoardRerender(); setText(""); - setDueDate(""); + setDeadline(""); setOpen(false); } catch (error: unknown) { console.error("Error creating goal:", error); @@ -114,15 +104,15 @@ export default function CreateGoalModal({
setText(e.target.value)} required /> setDueDate(e.target.value)} + value={deadline} + onChange={(e) => setDeadline(e.target.value)} required /> diff --git a/src/Components/Header/Header.module.scss b/src/Components/Header/Header.module.scss deleted file mode 100644 index 8aeac69..0000000 --- a/src/Components/Header/Header.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.header { - width: 100%; -} diff --git a/src/Components/Header/Header.tsx b/src/Components/Header/Header.tsx deleted file mode 100644 index 8cae911..0000000 --- a/src/Components/Header/Header.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import styles from "./Header.module.scss"; - -export default function Header() { - return
Todo Real(仮)
; -} diff --git a/src/Components/Loader/Loader.tsx b/src/Components/Loader/Loader.tsx index ad44cca..a503579 100644 --- a/src/Components/Loader/Loader.tsx +++ b/src/Components/Loader/Loader.tsx @@ -4,7 +4,7 @@ import { useUser } from "@/utils/UserContext"; import CircularProgress from "@mui/joy/CircularProgress"; import { Button, Typography } from "@mui/material"; import { redirect } from "next/navigation"; -import { ReactNode, useState } from "react"; +import { ReactNode, useEffect, useState } from "react"; interface LoaderProps { children: ReactNode; @@ -14,9 +14,14 @@ export const Loader = ({ children }: LoaderProps) => { const [showErrorButton, setShowErrorButton] = useState(false); const { user } = useUser(); - if (user === null && window.location.pathname !== "/account/") { - redirect("/account"); - } + + // ログインしていない場合はログインページにリダイレクト + useEffect(() => { + const pathname = window.location.pathname; + if (user === null && pathname !== "/account/" && pathname !== "/") { + redirect("/account"); + } + }, [user]); setTimeout(() => { setShowErrorButton(true); diff --git a/src/Components/NavigationMenu/NavigationMenu.tsx b/src/Components/NavigationMenu/NavigationMenu.tsx index 9d89982..0039f3a 100644 --- a/src/Components/NavigationMenu/NavigationMenu.tsx +++ b/src/Components/NavigationMenu/NavigationMenu.tsx @@ -1,6 +1,7 @@ "use client"; import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; -import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; +import GroupIcon from "@mui/icons-material/Group"; +import HomeIcon from "@mui/icons-material/Home"; import Person from "@mui/icons-material/Person"; import Box from "@mui/joy/Box"; import ListItemDecorator from "@mui/joy/ListItemDecorator"; @@ -13,26 +14,31 @@ import { useEffect, useState } from "react"; export default function NavigationMenu() { const paths = [ ["/mycontent", "/mycontent/"], - ["/"], + ["/discover", "/discover/"], ["/account", "/account/"], + ["/"], ]; const pathname = window.location.pathname; const defaultIndex = paths[0].some((path) => path === pathname) ? 0 : paths[1].some((path) => path === pathname) ? 1 - : 2; + : paths[2].some((path) => path === pathname) + ? 2 + : 3; const [index, setIndex] = useState(defaultIndex); - const colors = ["success", "primary", "warning"] as const; useEffect(() => { if (document.readyState === "complete") { if (!paths[index].some((path) => path === pathname)) { + // パスが存在しない場合はリダイレクト redirect(paths[index][0]); } } }, [index]); + const colors = ["success", "primary", "warning", "danger"] as const; + return ( setIndex(value as number)} sx={(theme) => ({ p: 1, - borderRadius: 13, + borderRadius: 15, maxWidth: "93%", mx: "auto", boxShadow: theme.shadow.sm, - "--joy-shadowChannel": theme.vars.palette[colors[index]].darkChannel, [`& .${tabClasses.root}`]: { py: 1, flex: 1, transition: "0.3s", fontWeight: "md", - fontSize: "md", + // TODO: スマホの時に文字が改行されてる + fontSize: "sm", [`&:not(.${tabClasses.selected}):not(:hover)`]: { opacity: 0.7, }, @@ -76,41 +82,72 @@ export default function NavigationMenu() { variant="plain" size="sm" disableUnderline - sx={{ borderRadius: "lg", p: 0 }} + sx={{ borderRadius: "lg", p: 0, gap: "3px" }} > - My contents + My content - + - Home + Discover Profile + + + + + Top + diff --git a/src/Components/NotificationButton/NotificationButton.tsx b/src/Components/NotificationButton/NotificationButton.tsx index c697a6c..a363bcb 100644 --- a/src/Components/NotificationButton/NotificationButton.tsx +++ b/src/Components/NotificationButton/NotificationButton.tsx @@ -37,6 +37,10 @@ export default function NotificationButton() { }); return; } + showSnackBar({ + message: "通知を有効化中...", + type: "normal", + }); setNotificationTokenGenerating(true); requestPermission(user.userId) .then(() => { diff --git a/src/Components/PostModal/PostModal.tsx b/src/Components/PostModal/PostModal.tsx index eaae85b..b79ee8c 100644 --- a/src/Components/PostModal/PostModal.tsx +++ b/src/Components/PostModal/PostModal.tsx @@ -102,10 +102,9 @@ export default function PostModal({ }; try { - const data = await createPost(postData); + await createPost(postData); setProgress(100); - console.log("Post created:", data); showSnackBar({ message: "投稿しました", @@ -175,7 +174,7 @@ export default function PostModal({ open={open} onClose={() => setOpen(false)} keepMounted - disablePortal + disablePortal={false} > -
+ - - {userData?.name} - -
- - {userData?.streak}日連続 - - - 達成率{successRate}% - - - 累計{userData?.completed}回達成 +
+ + {userData?.name} +
+ + {userData?.streak}日連続 + + + 達成率{successRate}% + + + 累計{userData?.completed}回達成 + +
-
- - ({ - gap: "0px", - "--Stepper-verticalGap": "30px", - "--StepIndicator-size": "2.5rem", - "--Step-gap": "1rem", - "--Step-connectorInset": "0.5rem", - "--Step-connectorRadius": "1rem", - "--Step-connectorThickness": "4px", - "--joy-palette-success-solidBg": "var(--joy-palette-success-400)", - [`& .${stepClasses.completed}`]: { - "&::after": { bgcolor: "success.solidBg" }, - }, - // copletedとactive両方の場合 - [`& .${stepClasses.completed}.${stepClasses.active}`]: { - [`& .${stepIndicatorClasses.root}`]: { - border: "4px solid #fff", - boxShadow: `0 0 0 1px ${theme.vars.palette.primary[500]}`, + + ({ + gap: "0px", + "--Stepper-verticalGap": "30px", + "--StepIndicator-size": "2.5rem", + "--Step-gap": "1rem", + "--Step-connectorInset": "0.5rem", + "--Step-connectorRadius": "1rem", + "--Step-connectorThickness": "4px", + "--joy-palette-success-solidBg": "var(--joy-palette-success-400)", + [`& .${stepClasses.completed}`]: { + "&::after": { bgcolor: "success.solidBg" }, }, - "&::after": { - bgcolor: "success.solidBg", - marginTop: "-50px", + // copletedとactive両方の場合 + [`& .${stepClasses.completed}.${stepClasses.active}`]: { + [`& .${stepIndicatorClasses.root}`]: { + border: "4px solid #fff", + boxShadow: `0 0 0 1px ${theme.vars.palette.primary[500]}`, + }, + "&::after": { + bgcolor: "success.solidBg", + marginTop: "-50px", + }, }, - }, - [`& .${stepClasses.active}`]: { - [`& .${stepIndicatorClasses.root}`]: { - border: "4px solid #fff", - boxShadow: `0 0 0 1px ${theme.vars.palette.primary[500]}`, + [`& .${stepClasses.active}`]: { + [`& .${stepIndicatorClasses.root}`]: { + border: "4px solid #fff", + boxShadow: `0 0 0 1px ${theme.vars.palette.primary[500]}`, + }, }, - }, - [`& .${stepClasses.disabled} *`]: { - color: "neutral.softDisabledColor", - }, - [`& .${typographyClasses["title-sm"]}`]: { - textTransform: "uppercase", - letterSpacing: "1px", - fontSize: "10px", - }, - })} - > - {children} - -
+ [`& .${stepClasses.disabled} *`]: { + color: "neutral.softDisabledColor", + }, + [`& .${typographyClasses["title-sm"]}`]: { + textTransform: "uppercase", + letterSpacing: "1px", + fontSize: "10px", + }, + })} + > + {children} + + + ); }; diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx new file mode 100644 index 0000000..0ddbbad --- /dev/null +++ b/src/app/discover/page.tsx @@ -0,0 +1,12 @@ +"use client"; +import DashBoard from "@/Components/DashBoard/DashBoard"; +import GoalModalButton from "@/Components/GoalModal/GoalModalButton"; + +export default function Discover() { + return ( + <> + + + + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7b4b895..2f82703 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,3 @@ -import Header from "@/Components/Header/Header"; import { Loader } from "@/Components/Loader/Loader"; import NavigationMenu from "@/Components/NavigationMenu/NavigationMenu"; import SnackBar from "@/Components/SnackBar/SnackBar"; @@ -8,9 +7,11 @@ import { UserProvider } from "@/utils/UserContext"; import type { Metadata } from "next"; import "./firebase"; +const description = "TODO REALはTODOリストとBeRealを組み合わせたアプリです。"; + export const metadata: Metadata = { - title: "Todo Real(仮)", - description: "BeRealとTodoアプリを組み合わせたアプリ", + title: "Todo Real", + description, }; export const viewport = { @@ -23,11 +24,35 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const rootURL = "https://todo-real-c28fa.web.app/"; + return ( - + + + {/* Open Graph */} + + + + + + {/* Twitter Card */} + + + + + + {/* Google Fonts */} + + + -
{children} diff --git a/src/app/page.module.scss b/src/app/page.module.scss new file mode 100644 index 0000000..72761cf --- /dev/null +++ b/src/app/page.module.scss @@ -0,0 +1,53 @@ +.introductionBody { + --primary-color: #008edc; + --primary-font-family: Roboto, "Zen Kaku Gothic New", sans-serif; + + color: var(--primary-color); + font-family: var(--primary-font-family); + line-height: 1.7; + + --joy-palette-text-primary: var(--primary-color); + --joy-fontFamily-display: var(--primary-font-family); + + > div { + padding: 15px 5px; + } + + > div:nth-child(odd) { + background-color: #e8f7ff; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.2; + margin: 5px 0; + text-align: center; + } + + img { + border-radius: 13px; + display: block; + margin: 5px auto; + object-fit: contain; + width: 95%; + } + + .svg { + fill: var(--primary-color); + height: 35px; + margin: 0; + padding-bottom: 3px; + width: 35px; + } +} + +.highlight { + background-color: var(--primary-color); + color: white; + margin: 0 2px; + padding: 1px 3px 0; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 2460720..f84938c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,202 @@ "use client"; -import DashBoard from "@/Components/DashBoard/DashBoard"; -import GoalModalButton from "@/Components/GoalModal/GoalModalButton"; + +import CenterIn from "@/Components/Animation/CenterIn"; +import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import Button from "@mui/joy/Button"; +import Typography, { TypographyProps } from "@mui/joy/Typography"; +import { ReactNode } from "react"; +import styled from "styled-components"; +import styles from "./page.module.scss"; + +const StyledTypography = styled(Typography)` + color: var(--primary-color); + font-family: Roboto, "Zen Kaku Gothic New", sans-serif; + font-weight: 600; + padding: 0 3px; +`; export default function Top() { return ( - <> - - - +
+ + + 友達と共有・競争できるTODOリスト + +
+ + TODO REAL +
+ +
+ + + TODO REALとは? + + やりたいことがつい後回しになってしまったり、毎日続けたいと思っていた習慣が途切れてしまったり…そんな経験はありませんか? +
+ TODO REALはあなたの目標達成・継続を手助けします! + + TODO REALでは目標を完了したら写真を投稿することができ、 + + SNS感覚でTODOリストを使ったり友達と共有できます! + +
+ 面倒なイメージのTODOリストも、TODO REALを使えば + 思わず開きたくなるアプリに + 変えることができます! +
+
+ + + + 完了したTODOリストを友達と共有 + + + 完了したTODOリストは友達に公開されます。友達に共有することで、達成感を共有できます。 + + + + + + + 失敗したら友達に晒す + + + 失敗してもペナルティの無いTODOリストとは異なり、期限内に完了しなかったTODOリストも公開されます。なんて恥ずかしい。 + + + + + + + 期限が近づくと通知を送信 + + + 5分前になると通知を送信します。通知を受け取ることで、TODOリストの達成を手助けします。 + {/* TODO: 画像を差し替える */} + + + + + + + 毎日継続して友達と競おう + + + 連続記録で毎日目標を達成することを促進します! +
+ また、達成率や達成回数で友達と競うこともできます! +
+ +
+ + + + 目標を作成する面倒を無くす + + + 毎回目標を設定するのが面倒?ワンクリックで作成可能に! +
+ 完了投稿をすると投稿ボタンが変化して同じ目標をワンクリックで複製できるように! +
+ +
+ + + + 友達の投稿を見たいなら、あなたも投稿しましょう + + + 友達の投稿を表示するには、あなたの完了した目標をシェアしましょう。あなたが最後に投稿した時間より後の目標の画像はモザイクがかかっています。 + + {/* TODO: 正式に実装したら画像を新しくする */} + + + + + + 使い方 + + + 1. 目標を作成 + + + 画面右下の+ボタンから目標を作成できます。 + + + + + 2. 投稿 + + + 目標を完了したら写真をアップロードして共有しましょう! + + + + + アップロードしたら自動で友達に公開されます! + + + + + +

今すぐ始めよう!

+ + あなたの最初の目標はこのアプリを使い始めることです。 + +
+ +
+ + ゲストログインを使用するとアカウントを作成せずに閲覧できますが、投稿機能は使用できません。 + +
+ + + + 今後追加したい機能 + + + いいねやコメント等の友達とインタラクトできる機能を実装予定です。 + + + + + + + コード・実装方法はGitHubのドキュメントを参照 + + + +
); } + +const Highlight = ({ children }: { children: ReactNode }) => { + return {children}; +}; diff --git a/src/types/types.ts b/src/types/types.ts index c7fe0df..c1fed72 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,5 @@ +import { ReactNode } from "react"; + export interface User { userId: string; name: string; @@ -33,3 +35,20 @@ export interface Post { export interface PostWithGoalId extends Post { goalId: string; } + +export const animationTypes = [ + "left", + "right", + "center", + "bottom", + "top", +] as const; +export type AnimationType = (typeof animationTypes)[number]; + +export interface AnimationConfigs { + children: ReactNode; + duration?: number; + delay?: number; + distance?: number; // アニメーションの移動距離(ex. 右に100px移動しながらフェードイン) + margin?: number; // 画面に入ってから何px超えたら表示するか +} diff --git a/src/utils/API/Goal/createGoal.ts b/src/utils/API/Goal/createGoal.ts index 259f7a0..2817671 100644 --- a/src/utils/API/Goal/createGoal.ts +++ b/src/utils/API/Goal/createGoal.ts @@ -8,6 +8,16 @@ import { Goal } from "@/types/types"; * @return {*} */ export const createGoal = async (postData: Goal) => { + // 過去の時間が入力されている場合 + if (new Date(postData.deadline).getTime() < Date.now()) { + throw new Error("past deadline can't be set"); + } + + // 文字数制限を100文字までにする + if (postData.text.length > 100) { + throw new Error("too long comment"); + } + const response = await fetch(`${functionsEndpoint}/goal/`, { method: "POST", headers: { @@ -36,7 +46,12 @@ export const handleCreateGoalError = (error: unknown) => { let snackBarMessage = "目標の作成に失敗しました"; if (error instanceof Error) { - console.error("Fetch error:", error.message); + if (error.message.includes("past deadline can't be set")) { + snackBarMessage = "過去の時間を指定することはできません"; + } + if (error.message.includes("too long comment")) { + snackBarMessage = "目標の文字数は100文字以下にしてください"; + } if (error.message.includes("400")) { snackBarMessage = "入力内容に問題があります"; } diff --git a/src/utils/API/Post/createPost.ts b/src/utils/API/Post/createPost.ts index 158e9d1..2d0b168 100644 --- a/src/utils/API/Post/createPost.ts +++ b/src/utils/API/Post/createPost.ts @@ -8,6 +8,11 @@ import { PostWithGoalId } from "@/types/types"; * @return {*} */ export const createPost = async (postData: PostWithGoalId) => { + // 文字は100文字まで + if (postData.text.length > 100) { + throw new Error("too long comment"); + } + const response = await fetch(`${functionsEndpoint}/post/`, { method: "POST", headers: { @@ -36,7 +41,9 @@ export const handleCreatePostError = (error: unknown) => { let snackBarMessage = "投稿の作成に失敗しました"; if (error instanceof Error) { - console.error("Fetch error:", error.message); + if (error.message.includes("too long comment")) { + snackBarMessage = "投稿コメントは100文字以下にしてください"; + } if (error.message.includes("400")) { snackBarMessage = "入力内容に問題があります"; } diff --git a/src/utils/API/Result/fetchResult.ts b/src/utils/API/Result/fetchResult.ts index 247bc69..67483a9 100644 --- a/src/utils/API/Result/fetchResult.ts +++ b/src/utils/API/Result/fetchResult.ts @@ -56,7 +56,6 @@ export const handleFetchResultError = (error: unknown) => { let snackBarMessage = "データの取得に失敗しました"; if (error instanceof Error) { - console.error("Fetch error:", error.message); if (error.message.includes("404")) { snackBarMessage = "データが見つかりませんでした"; } diff --git a/src/utils/API/User/createUser.ts b/src/utils/API/User/createUser.ts index 08e434f..0f57cc5 100644 --- a/src/utils/API/User/createUser.ts +++ b/src/utils/API/User/createUser.ts @@ -22,7 +22,4 @@ export const createUser = async (name: string, userId: string) => { const data = await response.json(); throw new Error(`Error ${status}: ${data.message}`); } - - const data = await response.json(); - console.log("Success:", data); }; diff --git a/src/utils/API/User/fetchUser.ts b/src/utils/API/User/fetchUser.ts index d061cf2..577f38a 100644 --- a/src/utils/API/User/fetchUser.ts +++ b/src/utils/API/User/fetchUser.ts @@ -36,7 +36,6 @@ export const handleFetchUserError = (error: unknown) => { let snackBarMessage = "初回ログインかユーザーデータが見つかりません"; if (error instanceof Error) { - console.error("Fetch error:", error.message); if (error.message.includes("404")) { snackBarMessage = "ユーザー情報が登録されていません"; } diff --git a/src/utils/UserContext.tsx b/src/utils/UserContext.tsx index 25f628f..3311b60 100644 --- a/src/utils/UserContext.tsx +++ b/src/utils/UserContext.tsx @@ -68,7 +68,7 @@ export const UserProvider = ({ children }: Props) => { auth, async (firebaseUser: FirebaseUser | null) => { if (!firebaseUser) { - console.log("No user is logged in."); + console.log("ログインしていません"); setUser(null); return; }