diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore index a035a509..93c57fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,20 +8,30 @@ # testing /coverage +# next.js +/.next/ +/out/ + # production /build # misc .DS_Store -.env.local -.env -.env.development.local -.env.test.local -.env.production.local +*.pem +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -# Local Netlify folder -.netlify +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.env diff --git a/.sanity/runtime/app.js b/.sanity/runtime/app.js new file mode 100644 index 00000000..ecc33a2b --- /dev/null +++ b/.sanity/runtime/app.js @@ -0,0 +1,11 @@ + +// This file is auto-generated on 'sanity dev' +// Modifications to this file is automatically discarded +import {renderStudio} from "sanity" +import studioConfig from "../../sanity.config.js" + +renderStudio( + document.getElementById("sanity"), + studioConfig, + {reactStrictMode: false, basePath: "/"} +) diff --git a/.sanity/runtime/index.html b/.sanity/runtime/index.html new file mode 100644 index 00000000..65584bae --- /dev/null +++ b/.sanity/runtime/index.html @@ -0,0 +1,203 @@ + + +Sanity Studio
\ No newline at end of file diff --git a/README.md b/README.md index 8a1ad2ec..242cafcc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ ![Untitled](https://github.com/Librum-Reader/Librum-Website/assets/69865187/ecb5e40d-dd60-4cd5-baff-54904da39dfe) - Welcome to the official website of Librum, a powerful and intuitive application for managing your personal online library and reading your books. This repository contains the source code and assets for the Librum website, where you can learn more about the application, its features, and how to get started.
@@ -36,23 +35,27 @@ We appreciate contributions to the Librum website that improve its content, desi If you would like to run the Librum website locally on your machine, follow the instructions below: 1. Clone the repository: + ```bash git clone https://github.com/Librum-Reader/Librum-Website.git ``` 2. Navigate to the project directory: + ```bash cd Librum-Website ``` 3. Install the dependencies: + ```bash npm install ``` 4. Start the development server: + ```bash -npm start +npm run dev ``` 5. Open your browser and visit `http://localhost:3000` to view the Librum website. diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..ff4fbdb7 --- /dev/null +++ b/TODO.md @@ -0,0 +1,10 @@ +- [x] Add the privacy policy page +- [x] Add the disclaimer page +- [x] Reduce the font size of the other pages with legal documents, to look exactly like on the official version of the website +- [x] Switch the whole theme of the website from the current blue-ish color (#47478f) to purple used on the website (#946BDE) +- [x] Make the items in the navbar white instead of blue and increase the size of the items a bit (including the logo), to look similar to the official one +- [ ] Add the theme switch ( @slrbristol said that this is mostly done ) +- [x] Fix the News page to look like the official website's news page +- [x] Add a footer that looks like the ones of the official website +- [ ] Add popup to the Register "page" that tells the user to confirm their email address (similar to the one on the website, feel free to improve the UI of it if you have anything in mind) +- [ ] Add a popup that opens when the website is opened for the first time, which tells the user that the website is still in development and to accept the legal terms (You can look at https://github.com/Librum-Reader/Librum-Website/pull/33 to see what contents need to be present) diff --git a/app/Hooks/useFormSubmit.js b/app/Hooks/useFormSubmit.js new file mode 100644 index 00000000..1843631f --- /dev/null +++ b/app/Hooks/useFormSubmit.js @@ -0,0 +1,90 @@ +import { useState, useCallback, useRef } from "react"; +import { useGoogleReCaptcha } from "react-google-recaptcha-v3"; + +const useFormSubmit = () => { + const [res, setRes] = useState(""); + const [error, setError] = useState(""); + const [captchaError, setCaptchaError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const recaptchaRef = useRef(null); + const { executeRecaptcha } = useGoogleReCaptcha(); + + const verifyToken = async (token) => { + try { + await fetch( + `https://librum-dev.azurewebsites.net/api/recaptchaVerify?userToken=${token}`, + { + method: "POST", + } + ) + .then((res) => res.json()) + .then((data) => { + if (data?.success === false) { + throw new Error("Something went wrong!"); + } + recaptchaRef.current = data; + }); + } catch (err) { + setCaptchaError(err); + } + }; + + // const handleReCaptchaVerify = useCallback( + // async (e, values, captchaActionName) => { + // e?.preventDefault(); + // try { + // const token = await executeRecaptcha(captchaActionName); + // if (!token) { + // throw new Error("Something went wrong!"); + // } + // await submitForm(e, token, values); + // } catch (err) { + // console.log(err); + // setError(err); + // } + // }, + // [executeRecaptcha] + // ); + + const resetStates = () => { + setRes(""); + setError(""); + setCaptchaError(""); + }; + + const submitForm = async (e, token, values) => { + e.preventDefault(); + setIsLoading(true); + try { + await fetch("https://formsubmit.co/ajax/help@librumreader.com", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(values), + }) + .then((response) => response.json()) + .then(async (data) => { + if ((await data?.success) !== "true") { + throw new Error("Something went wrong!"); + } + setRes(data?.success); + }); + } catch (err) { + setError(err); + } + setIsLoading(false); + }; + + return { + res, + error, + captchaError, + isLoading, + // handleReCaptchaVerify, + resetStates, + }; +}; + +export default useFormSubmit; diff --git a/app/components/blog/SanityImage.jsx b/app/components/blog/SanityImage.jsx new file mode 100644 index 00000000..7430f7fa --- /dev/null +++ b/app/components/blog/SanityImage.jsx @@ -0,0 +1,26 @@ +import { useNextSanityImage } from "next-sanity-image"; +import { createClient } from "next-sanity"; +import Image from "next/image"; + +const client = createClient({ + projectId: "46vwrypj", + dataset: "production", + apiVersion: "2023-08-27", + useCdn: false, +}); + +const SanityImage = ({ asset }) => { + const imageProps = useNextSanityImage(client, asset); + + if (!imageProps) return null; + + return ( + + ); +}; + +export default SanityImage; diff --git a/app/components/profile/AccountSettings.jsx b/app/components/profile/AccountSettings.jsx new file mode 100644 index 00000000..b200dcf6 --- /dev/null +++ b/app/components/profile/AccountSettings.jsx @@ -0,0 +1,77 @@ +import React from "react"; +import { Flex, Text, Button } from "@chakra-ui/react"; +import { useCookies } from "react-cookie"; +import { useSelector, useDispatch } from "react-redux"; +import { useRouter } from "next/navigation"; +import { + updateUser, + updateLoggedIn, + resetUser, +} from "@/app/features/user/userSlice"; + +const AccountSettings = () => { + const router = useRouter(); + // Logout function + const [cookies, setCookie, removeCookie] = useCookies(["user"]); + const dispatch = useDispatch(); + const user = useSelector((state) => state.user.value); + + const logOut = () => { + localStorage.removeItem("token"); + removeCookie("token"); + // dispatch(resetUser({})); + dispatch(updateLoggedIn(false)); + router.push("/"); + }; + + return ( + + + + ACCOUNT SETTINGS + + + + + + + + ); +}; + +export default AccountSettings; diff --git a/app/components/profile/AvatarAndUserName.jsx b/app/components/profile/AvatarAndUserName.jsx new file mode 100644 index 00000000..efc3dd4a --- /dev/null +++ b/app/components/profile/AvatarAndUserName.jsx @@ -0,0 +1,191 @@ +import React from "react"; +import { + Flex, + Avatar, + Button, + Box, + Text, + Spinner, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Input, + useDisclosure, + useToast, +} from "@chakra-ui/react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { FaRegEdit, FaRegSave } from "react-icons/fa"; +import { useEffect, useState } from "react"; +import { + fetchUserInfo, + fetchAvatar, + updatePictureInfo, + uploadAvatar, +} from "@/app/utils/apiFunctions"; + +const AvatarAndUserName = () => { + const { + isOpen: isAvatarOpen, + onOpen: onAvatarOpen, + onClose: onAvatarClose, + } = useDisclosure(); + + let token; + if (typeof window !== "undefined") { + token = localStorage.getItem("token"); + } + + const { isLoading, error, data } = useQuery({ + queryKey: ["user"], + queryFn: () => { + return fetchUserInfo(token); + }, + }); + + const { + isLoading: isAvatarLoading, + error: avatarError, + data: avatarData, + } = useQuery({ + queryKey: ["avatar"], + queryFn: () => { + return fetchAvatar(token); + }, + }); + + const updatePicture = useMutation({ + mutationFn: updatePictureInfo, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + onEditUserNameClose(); + }, + }); + + // File upload mutation + const queryClient = useQueryClient(); + const avatarUpload = useMutation({ + mutationFn: uploadAvatar, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["avatar"] }); + onAvatarClose(); + }, + }); + + // File upload function + const uploadFile = async (e, avatar) => { + e.preventDefault(); + const currentDateTime = new Date(); + const formattedDateTime = currentDateTime.toISOString().slice(0, 19); + const token = localStorage.getItem("token"); + const formData = new FormData(); + formData.append("file", avatar); + avatarUpload.mutate({ file: formData, token: token }); + updatePicture.mutate({ + hasProfilePicture: true, + lastUpdated: formattedDateTime, + token: token, + }); + }; + + const [avatar, setAvatar] = useState(); + + const handleFileSelect = (e) => { + setAvatar(e.target.files[0]); + }; + + const cancelUpload = () => { + setAvatar(null); + onAvatarClose(); + }; + + return ( + <> + + } size="2xl" /> + + {/* {!isAvatarLoading && Fetched} */} + + {data?.firstName} {data?.lastName} + + + + + + + {/* Upload/change avatar modal */} + + + + Upload avatar + + +
{ + uploadFile(e, avatar); + }} + > + + + + + + + +
+
+ + +
+
+ + ); +}; + +export default AvatarAndUserName; diff --git a/app/components/profile/TierInformation.jsx b/app/components/profile/TierInformation.jsx new file mode 100644 index 00000000..a77c8947 --- /dev/null +++ b/app/components/profile/TierInformation.jsx @@ -0,0 +1,82 @@ +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Flex, Text, Button } from "@chakra-ui/react"; + +const TierInformation = () => { + let token; + if (typeof window !== "undefined") { + token = localStorage.getItem("token"); + } + + const { isLoading, error, data } = useQuery({ + queryKey: ["user"], + queryFn: () => { + return fetchUserInfo(token); + }, + }); + + let storageLimit; + let usedStorage; + let storageProgress; + + if (!isLoading) { + console.log(data); + storageLimit = data?.bookStorageLimit; + storageLimit = storageLimit / 1024; + storageLimit = storageLimit / 1024; + storageLimit = storageLimit / 1024; + + // usedStorage = data?.usedBookStorage; + usedStorage = data?.usedBookStorage; + usedStorage = usedStorage / 1024; + usedStorage = usedStorage / 1024; + usedStorage = usedStorage / 1024; + + storageProgress = usedStorage / storageLimit; + storageProgress = storageProgress * 100; + } + + return ( + + + YOUR TIER + + + + {data?.role.toUpperCase()} + + + {storageLimit?.toFixed(2)} GB + + + + + + + + ); +}; + +export default TierInformation; diff --git a/app/components/profile/UsedStorage.jsx b/app/components/profile/UsedStorage.jsx new file mode 100644 index 00000000..b195ee8f --- /dev/null +++ b/app/components/profile/UsedStorage.jsx @@ -0,0 +1,93 @@ +import React from "react"; +import { Flex, Text, Progress } from "@chakra-ui/react"; +import { useQuery } from "@tanstack/react-query"; + +const UsedStorage = () => { + let token; + if (typeof window !== "undefined") { + token = localStorage.getItem("token"); + } + + const { isLoading, error, data } = useQuery({ + queryKey: ["user"], + queryFn: () => { + return fetchUserInfo(token); + }, + }); + + let storageLimit; + let usedStorage; + let storageProgress; + + if (!isLoading) { + console.log(data); + storageLimit = data?.bookStorageLimit; + storageLimit = storageLimit / 1024; + storageLimit = storageLimit / 1024; + storageLimit = storageLimit / 1024; + + // usedStorage = data?.usedBookStorage; + usedStorage = data?.usedBookStorage; + usedStorage = usedStorage / 1024; + usedStorage = usedStorage / 1024; + usedStorage = usedStorage / 1024; + + storageProgress = usedStorage / storageLimit; + storageProgress = storageProgress * 100; + } + + return ( + + + USED STORAGE + + + + + {usedStorage?.toFixed(2)} GB + + + Used Storage + + + + + {storageLimit?.toFixed(2)} GB + + + Free Storage + + + + + + ); +}; + +export default UsedStorage; diff --git a/app/components/profile/UsernameAndEmail.jsx b/app/components/profile/UsernameAndEmail.jsx new file mode 100644 index 00000000..045aaf6b --- /dev/null +++ b/app/components/profile/UsernameAndEmail.jsx @@ -0,0 +1,437 @@ +import React from "react"; + +import { + Flex, + Text, + Button, + Spinner, + useDisclosure, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Input, + InputGroup, + InputRightElement, + Alert, + AlertIcon, + AlertTitle, + AlertDescription, + CloseButton, + useToast, +} from "@chakra-ui/react"; +import { BeatLoader } from "react-spinners"; + +import { FaRegEdit } from "react-icons/fa"; +import { AiOutlineEyeInvisible, AiOutlineEye } from "react-icons/ai"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + fetchUserInfo, + editUser, + changePassword, +} from "@/app/utils/apiFunctions"; +import { useState } from "react"; + +const UsernameAndEmail = () => { + let token; + const queryClient = useQueryClient(); + + const { + isOpen: isAlertOpen, + onClose: onAlertClose, + onOpen: onAlertOpen, + } = useDisclosure(); + + // State handlers for changing password + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [passwordIsValid, setPasswordIsValid] = useState(null); + const [showPassword, setShowPassword] = useState(false); + const resetPassword = () => { + setNewPassword(""); + setConfirmPassword(""); + }; + + // State handlers for changing username + const [newFirstName, setNewFirstName] = useState(""); + const [newLastName, setNewLastName] = useState(""); + + const handleNewFirstName = (e) => setNewFirstName(e.target.value); + const handleNewLastName = (e) => setNewLastName(e.target.value); + const resetUsernames = () => { + setNewFirstName(""); + setNewLastName(""); + }; + + const handleUpdateUser = (newFirstName, newLastName) => { + const token = localStorage.getItem("token"); + + updateUser.mutate({ + firstName: newFirstName, + lastName: newLastName, + token: token, + }); + }; + + const handleEditPassword = (password) => { + const token = localStorage.getItem("token"); + + editPassword.mutate({ + password: password, + token: token, + }); + }; + + const handleShowPassword = () => { + setShowPassword(!showPassword); + }; + + const handleNewPassword = (e) => { + setNewPassword(e.target.value); + }; + + const validatePassword = (e) => { + setConfirmPassword(e.target.value); + if (e.target.value === newPassword) { + setPasswordIsValid(true); + } else { + setPasswordIsValid(false); + } + }; + + if (typeof window !== "undefined") { + token = localStorage.getItem("token"); + } + + const { isLoading, error, data } = useQuery({ + queryKey: ["user"], + queryFn: () => { + return fetchUserInfo(token); + }, + }); + + // Mutations for changing username, image, password, and other info + const updateUser = useMutation({ + mutationFn: editUser, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + onEditUserNameClose(); + toast({ + title: "Success!", + description: "Your information was updated.", + status: "success", + duration: 9000, + isClosable: true, + }); + }, + }); + + const { + isOpen: isEditUserNameOpen, + onOpen: onEditUserNameOpen, + onClose: onEditUserNameClose, + } = useDisclosure(); + + const [errorMsg, setErrorMsg] = useState(""); + + const editPassword = useMutation({ + mutationFn: changePassword, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + + if (data.code === 0) { + setErrorMsg("The password entered was too short."); + onAlertOpen(); + } else { + onChangePasswordClose(); + resetPassword(); + toast({ + title: "Success!", + description: "Your password has been changed.", + status: "success", + duration: 9000, + isClosable: true, + }); + } + }, + }); + + const { + isOpen: isChangePasswordOpen, + onOpen: onChangePasswordOpen, + onClose: onChangePasswordClose, + } = useDisclosure(); + + const { + isOpen: isEditEmailOpen, + onOpen: onEditEmailOpen, + onClose: onEditEmailClose, + } = useDisclosure(); + + return ( + + + YOUR INFORMATION + + + + Username + + + + + {data?.firstName} {data?.lastName} + + + + + + + + Email + + + + {data?.email} + + + + Security + + + + + {/* Edit username modal */} + + + + Edit your username + + + + First name + + + + Last name + + + + + + + + + + + {/* Change password modal */} + + + + Change your password + + + + New password + + + + + + + + + Confirm new password + + + + + + + + {confirmPassword ? ( + passwordIsValid ? null : ( + Passwords must match before submitting. + ) + ) : null} + + + + + {isAlertOpen ? ( + + + + {errorMsg} + + + + ) : null} + + + + + + + + + + ); +}; + +export default UsernameAndEmail; diff --git a/app/components/sections/Alternate.jsx b/app/components/sections/Alternate.jsx new file mode 100644 index 00000000..aa04c4fd --- /dev/null +++ b/app/components/sections/Alternate.jsx @@ -0,0 +1,167 @@ +"use client"; +import { + Flex, + Image, + Text, + Heading, + VStack, + Card, + CardBody, + Box, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + useDisclosure, + useColorMode, + useColorModeValue, +} from "@chakra-ui/react"; + +import { motion, useInView, useAnimation } from "framer-motion"; +import { useRef, useEffect, useState } from "react"; +import FeaturesAnimate from "../ui/FeaturesAnimate"; +import FeaturesAnimateMobile from "../ui/FeaturesAnimateMobile"; +import MobileFeatureCard from "../ui/MobileFeatureCard"; + +const Alternate = () => { + const { colorMode } = useColorMode(); + + const evenAlternateBackground = useColorModeValue("gray.50", "#3c4047"); + const oddAlternateBackground = useColorModeValue("gray.200", "#282c34"); + + const { isOpen, onOpen, onClose } = useDisclosure(); + const [modalData, setModalData] = useState(""); + + const data = [ + { + title: "Simple", + text: "Focus on what actually matters, using a simple and straight forward interface.", + text2: + "Your time is too valuable to be wasted on badly designed applications.", + text_mobile: + "Focus on what actually matters, using a simple and straight forward interface. Your time is too valuable to be wasted on badly designed applications.", + image: "/screenshots/reading_dark.png", + image_light: "/screenshots/reading_light.png", + }, + { + title: "Your Own Library", + text: "Create your own personalized online library that you can access from any device, anytime, anywhere.", + text2: "Librum automatically saves everything you need to the cloud.", + text_mobile: + "Create your own personalized online library that you can access from any device, anytime, anywhere. Librum automatically saves everything you need to the cloud.", + image: "/screenshots/library_dark.png", + image_light: "/screenshots/library_light.png", + }, + { + title: "Fully Customizable", + text: "Customize Librum to make it look and feel the way you want it to. No app is perfect for everyone right away, but we make it possible for you to make it perfect.", + text_mobile: + "Customize Librum to make it look and feel the way you want it to. No app is perfect for everyone right away, but we make it possible for you to make it perfect.", + image: "/screenshots/settings_dark.png", + image_light: "/screenshots/settings_light.png", + }, + ]; + + return ( + <> + + + + {/* + */} + {/* */} + + + + {/* */} + + + {data.map((block, index) => { + return ( + // + + // + ); + })} + {data.map((block, index) => { + return ( + // + + + + Illustration { + setModalData(block.image); + onOpen(); + }} + /> + + + + + + {block.title} + + + {block.text} + + {block.text2 ? ( + + {block.text2} + + ) : null} + + + // + ); + })} + + ); +}; + +export default Alternate; diff --git a/app/components/sections/ComingSoon.jsx b/app/components/sections/ComingSoon.jsx new file mode 100644 index 00000000..2fb0977d --- /dev/null +++ b/app/components/sections/ComingSoon.jsx @@ -0,0 +1,35 @@ +import { HStack, Heading, VStack } from "@chakra-ui/react"; +import FeatureCard from "./../ui/FeatureCard"; + +const ComingSoon = (props) => { + return ( + + Coming Soon + + + + + + + ); +}; + +export default ComingSoon; diff --git a/app/components/sections/Contribute.jsx b/app/components/sections/Contribute.jsx new file mode 100644 index 00000000..7135c217 --- /dev/null +++ b/app/components/sections/Contribute.jsx @@ -0,0 +1,28 @@ +import { VStack, Button, Text, Link } from "@chakra-ui/react"; +import NextLink from "next/link"; + +const Contribute = (props) => { + return ( + + Text inviting to contribute and more and more and more text + + + + + ); +}; + +export default Contribute; diff --git a/app/components/sections/Features.jsx b/app/components/sections/Features.jsx new file mode 100644 index 00000000..9e44173a --- /dev/null +++ b/app/components/sections/Features.jsx @@ -0,0 +1,38 @@ +"use client"; + +import { + Flex, + Image, + Box, + Text, + Heading, + useColorMode, +} from "@chakra-ui/react"; +import React from "react"; +import Alternate from "./Alternate"; + +const Features = () => { + const { colorMode, toggleColorMode } = useColorMode(); + + return ( + + + + + + + + + ); +}; + +export default Features; diff --git a/app/components/sections/Footer.jsx b/app/components/sections/Footer.jsx new file mode 100644 index 00000000..fddf6bd4 --- /dev/null +++ b/app/components/sections/Footer.jsx @@ -0,0 +1,138 @@ +import React from "react"; +import { + Flex, + Input, + Textarea, + FormLabel, + VStack, + Heading, + Button, + Image, + Text, + Grid, + GridItem, + Box, + Link, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + useDisclosure, + UnorderedList, + ListItem, +} from "@chakra-ui/react"; + +const Footer = () => { + const { isOpen, onOpen, onClose } = useDisclosure(); + + return ( + <> + + + + + Librum-Reader Legal Information + + + + + The following pages provide the various legal information + pertaining to Librum-Reader: + + + + Legal Disclaimer + + + Privacy Policy + + + Cookies Policy + + + Terms of Service + + + + + + + + + + + Have any questions or concerns? + + +