diff --git a/public/index.html b/public/index.html index 5bcff27..dbc9c7b 100644 --- a/public/index.html +++ b/public/index.html @@ -24,9 +24,10 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Note Saver + @@ -41,5 +42,14 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> + diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..1d9903c 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "Note Keeper", + "name": "Note Keeper", "icons": [ { "src": "favicon.ico", diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 0000000..a489e61 --- /dev/null +++ b/public/offline.html @@ -0,0 +1,12 @@ + + + + + + + Oops! + + +

Please go online to see, save or delete your note.

+ + \ No newline at end of file diff --git a/public/worker.js b/public/worker.js new file mode 100644 index 0000000..c665122 --- /dev/null +++ b/public/worker.js @@ -0,0 +1,62 @@ +/* eslint-disable no-restricted-globals */ +const OFFLINE_VERSION = 1; +const CACHE_NAME = 'offline'; +// Customize this with a different URL if needed. +const OFFLINE_URL = 'offline.html'; + +self.addEventListener('install', (event) => { + event.waitUntil((async () => { + const cache = await caches.open(CACHE_NAME); + // Setting {cache: 'reload'} in the new request will ensure that the response + // isn't fulfilled from the HTTP cache; i.e., it will be from the network. + await cache.add(new Request(OFFLINE_URL, {cache: 'reload'})); + })()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + // Enable navigation preload if it's supported. + // See https://developers.google.com/web/updates/2017/02/navigation-preload + if ('navigationPreload' in self.registration) { + await self.registration.navigationPreload.enable(); + } + })()); + + // Tell the active service worker to take control of the page immediately. + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + // We only want to call event.respondWith() if this is a navigation request + // for an HTML page. + if (event.request.mode === 'navigate') { + event.respondWith((async () => { + try { + // First, try to use the navigation preload response if it's supported. + const preloadResponse = await event.preloadResponse; + if (preloadResponse) { + return preloadResponse; + } + + const networkResponse = await fetch(event.request); + return networkResponse; + } catch (error) { + // catch is only triggered if an exception is thrown, which is likely + // due to a network error. + // If fetch() returns a valid HTTP response with a response code in + // the 4xx or 5xx range, the catch() will NOT be called. + console.log('Fetch failed; returning offline page instead.', error); + + const cache = await caches.open(CACHE_NAME); + const cachedResponse = await cache.match(OFFLINE_URL); + return cachedResponse; + } + })()); + } + + // If our if() condition is false, then this fetch handler won't intercept the + // request. If there are any other fetch handlers registered, they will get a + // chance to call event.respondWith(). If no fetch handlers call + // event.respondWith(), the request will be handled by the browser as if there + // were no service worker involvement. +}); \ No newline at end of file diff --git a/src/assets/colors/index.js b/src/assets/colors/index.js new file mode 100644 index 0000000..9e15095 --- /dev/null +++ b/src/assets/colors/index.js @@ -0,0 +1,3 @@ +export const MAIN_COLOR = `#111c33`; +export const TEXT_COLOR = `#1ecbe1`; +export const MAIN_COLOR_LIGHT = `#223861`; diff --git a/src/assets/login-bg.jpg b/src/assets/login-bg.jpg new file mode 100644 index 0000000..6cefc23 Binary files /dev/null and b/src/assets/login-bg.jpg differ diff --git a/src/components/Camera.js b/src/components/Camera.js index c41154b..13243c0 100644 --- a/src/components/Camera.js +++ b/src/components/Camera.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, useContext } from "react"; +import React, { useRef, useEffect, useContext } from "react"; import PropTypes from "prop-types"; import styled from "styled-components"; import LoginContext from "../context"; diff --git a/src/components/CameraView.js b/src/components/CameraView.js index da9833f..a2674b1 100644 --- a/src/components/CameraView.js +++ b/src/components/CameraView.js @@ -1,5 +1,5 @@ import Camera from "../components/Camera"; -import Editor from "../components/Editor"; +import EditorView from "./EditorView"; import styled from "styled-components"; import { useContext, useEffect, useRef, useState } from "react"; import { v4 as uuiv4 } from "uuid"; @@ -103,7 +103,7 @@ const CameraView = ({ onCameraHide, stream }) => { /> )} {showEditor && ( - { +const EditorView = ({ image, onProceed, onCancel }) => { const imgRef = useRef(null); const [crop, setCrop] = useState({ x: 35, @@ -100,9 +100,9 @@ const Editor = ({ image, onProceed, onCancel }) => { ); }; -Editor.propTypes = { +EditorView.propTypes = { image: PropTypes.string, onProceed: PropTypes.func, onCancel: PropTypes.func }; -export default Editor; +export default EditorView; diff --git a/src/components/LoginView.js b/src/components/LoginView.js index e460f4e..843f65b 100644 --- a/src/components/LoginView.js +++ b/src/components/LoginView.js @@ -2,41 +2,57 @@ import React from "react"; import styled from "styled-components"; import firebase from "../firebase"; import "firebase/auth"; +import LoginBg from "../assets/login-bg.jpg"; const LoginViewWrapper = styled.div` height: 100vh; width: 100vw; - background: #eee; + background: url(${LoginBg}); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + user-select: none; +`; +const Backdrop = styled.div` + height: 100%; + width: 100%; display: flex; - flex-direction: column; justify-content: center; align-items: center; + flex-direction: column; + background: rgba(0, 0, 0, 0.65); `; -const LoginButton = styled.button` - height: 35px; - width: 200px; - background: #1ecbe1; +const Title = styled.div` + font-family: "Montserrat", sans-serif; color: #fff; - cursor: pointer; - border: none; - margin-bottom: 5px; - border-radius: 20px; - &:hover { - box-shadow: 0 5px 10px rgba(154, 160, 185, 0.05), - 0 15px 40px rgba(166, 173, 201, 0.2); + display: flex; + width: 100%; + justify-content: center; + align-items: center; + margin-bottom: 25px; +`; +const TitleName = styled.h3` + flex: 0 0 auto; + margin: 0 10px; + font-weight: 500; + font-size: 1.8rem; + @media screen and (max-width: 720px) { + font-size: 1rem; } `; -const Input = styled.input` - height: 35px; - width: 30vw; - min-width: 200px; +const IconHolder = styled.div` + justify-content: center; + align-items: center; + display: flex; +`; +const Icon = styled.div` + height: auto; + width: auto; +`; +const Line = styled.div` + height: 3px; + width: 20%; background: #fff; - border-radius: 20px; - border: none; - padding: 15px; - margin: 0 0 10px 0; - &:focus { - outline: none; - } + border-radius: 5px; `; const LoginView = () => { const handleLoginWithGoogle = async () => { @@ -47,20 +63,31 @@ const LoginView = () => { alert(error.message); } }; - const handleAnnonymousLogin = async () => { - await firebase.auth().signInAnonymously(); + const handleLoginWithGithub = async () => { + try { + const provider = new firebase.auth.GithubAuthProvider(); + await firebase.auth().signInWithPopup(provider); + } catch (error) { + alert(error.message); + } }; return ( - {/* - - Login */} - - Login With Google - - {/* - Login Annonymously - */} + + + <Line /> + <TitleName>Login with</TitleName> + <Line /> + + + + + + + + + + ); }; diff --git a/src/components/NotesView.js b/src/components/NotesView.js index acf9eb7..abf4287 100644 --- a/src/components/NotesView.js +++ b/src/components/NotesView.js @@ -3,12 +3,26 @@ import styled from "styled-components"; import { Lightbox } from "react-modal-image"; import firebase from "../firebase"; import LoginContext from "../context"; +import { Delete } from "@material-ui/icons"; const NotesContainer = styled.div` display: flex; flex-wrap: wrap; height: auto; user-select: none; - padding-top: 70px; + padding-top: 66px; +`; +const NoteControlContainer = styled.div` + height: auto; + width: 100%; + position: absolute; + top: 0; + opacity: 0; +`; +const IconWrapper = styled.div` + height: auto; + width: auto; + padding: 5px; + color: #1ecbe1; `; const NoteName = styled.p` color: #fff; @@ -33,6 +47,9 @@ const Note = styled.div` &:hover ${NoteName} { opacity: 1; } + &:hover ${NoteControlContainer} { + opacity: 1; + } `; const NoteImage = styled.img` height: 200px; @@ -86,12 +103,13 @@ const Info = styled.p` ////////////////// Firebase Refs ///////////////////// const db = firebase.database(); -const NotesView = () => { +const NotesView = ({ onCount }) => { const [showLightbox, setShowLighbox] = useState(false); const [lightboxData, setLightboxData] = useState(null); const [imageData, setImageData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [noImageFound, setNoImageFound] = useState(false); + const [noteCount, setNoteCount] = useState(0); const { uid } = useContext(LoginContext); const handleShowLightbox = (data) => { setShowLighbox(!showLightbox); @@ -100,6 +118,11 @@ const NotesView = () => { const hideLightBox = () => { setShowLighbox(!showLightbox); }; + const handleDelete = ({ uid: imgId }) => { + const imageRef = db.ref(`images/${uid}/${imgId}`); + setImageData(data => data.filter(v => v.uid !== imgId)); + imageRef.set(null); + }; useEffect(() => { const imagesRef = db.ref(`images/${uid}`); setImageData([]); @@ -107,6 +130,7 @@ const NotesView = () => { const val = snap.val(); console.log(val); setImageData((data) => [...data, val]); + setNoteCount((count) => count + 1); setIsLoading(false); }); }, [uid]); @@ -121,6 +145,10 @@ const NotesView = () => { } }); }, [uid, imageData]); + useEffect(() => { + onCount(noteCount); + }, [noteCount, onCount]); + return ( {noImageFound && No image found, please take one.} @@ -139,9 +167,19 @@ const NotesView = () => { {imageData && !isLoading && imageData.map((d) => ( - handleShowLightbox(d)}> - - {d && d.name} + + handleShowLightbox(d)} + /> + handleShowLightbox(d)}> + {d && d.name} + + + handleDelete(d)}> + + + ))} {showLightbox && lightboxData && ( diff --git a/src/components/PreviewView.js b/src/components/PreviewView.js index 74f3d13..db73ad7 100644 --- a/src/components/PreviewView.js +++ b/src/components/PreviewView.js @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import styled from "styled-components"; -import { Edit, NavigateBefore, NavigateNext } from "@material-ui/icons"; +import { Edit, NavigateBefore, Save } from "@material-ui/icons"; const PreviewImage = styled.img` height: auto; width: auto; @@ -31,7 +31,7 @@ const Icon = styled.div` opacity: 0.8; } `; -const Control = styled.div` +const Control = styled.form` position: absolute; z-index: 50; top: 0; @@ -41,6 +41,7 @@ const Control = styled.div` height: 100%; align-items: flex-end; padding: 50px; + background: rgba(0, 0, 0, 0.6); `; const InputsHolder = styled.div` position: absolute; @@ -48,7 +49,7 @@ const InputsHolder = styled.div` height: auto; width: 100%; display: flex; - justify-content: center; + justify-content: space-between; flex-direction: column; align-items: center; top: 50%; @@ -66,6 +67,8 @@ const NameInput = styled.input` font-size: 20px; font-weight: 400; font-family: "Montserrat", sans-serif; + position: relative; + display: block; &:focus { outline: none; } @@ -93,7 +96,13 @@ const LoadingIndicator = styled.div` font-family: "Montserrat", sans-serif; font-weight: 500; `; -const PreviewView = ({ image, onRetake, onShowEditor, onImageSave, isSaving }) => { +const PreviewView = ({ + image, + onRetake, + onShowEditor, + onImageSave, + isSaving, +}) => { const wrapper = useRef(null); const [tValue, setTvalue] = useState("0"); const [noteName, setNoteName] = useState(""); @@ -102,16 +111,24 @@ const PreviewView = ({ image, onRetake, onShowEditor, onImageSave, isSaving }) = ]); return ( + {/* Loading indicatior. */} {isSaving && Saving...} + {/* Image previewer */} - - setNoteName(e.currentTarget.value)} - placeholder="Enter your note name" - /> - - + {/* Control form, holds input, back, edit and save. */} + { + e.preventDefault(); + onImageSave(noteName); + }} + > + + setNoteName(e.currentTarget.value)} + placeholder="Enter your note name" + /> + {/* Back icon */} { @@ -129,7 +146,7 @@ const PreviewView = ({ image, onRetake, onShowEditor, onImageSave, isSaving }) = {/* Forward icon */} onImageSave(noteName)}> - + diff --git a/src/components/SearchView.js b/src/components/SearchView.js new file mode 100644 index 0000000..bc54153 --- /dev/null +++ b/src/components/SearchView.js @@ -0,0 +1,38 @@ +import React, { useEffect, useRef } from "react"; +import { TEXT_COLOR, MAIN_COLOR, MAIN_COLOR_LIGHT } from "../assets/colors"; +import TransitionView from "./TransitionView"; +import styled from "styled-components"; +const SearchviewContainer = styled.div` + height: 100%; + width: 100%; + background: ${MAIN_COLOR}; + position: absolute; + left: 0; + z-index: 300; + transition: transform 0.8s; +`; +const SearchInput = styled.input` + height: auto; + width: 100%; + background: ${MAIN_COLOR_LIGHT}; + color: ${TEXT_COLOR}; + padding: 25px; + font-size: 20px; + border: none; + &:focus { + outline: none; + } +`; +const SearchView = ({ onClose }) => { + return ( + + {(handleClose) => ( + + + + + )} + + ); +}; +export default SearchView; diff --git a/src/components/Showcase.js b/src/components/Showcase.js index a56bc9b..c1d6aff 100644 --- a/src/components/Showcase.js +++ b/src/components/Showcase.js @@ -1,11 +1,12 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, useState } from "react"; import styled from "styled-components"; import LoginContext from "../context"; import CameraView from "./CameraView"; import NotesView from "./NotesView"; +import SearchView from "./SearchView"; import PropTypes from "prop-types"; import "firebase/auth"; -import { CameraAlt, ExitToApp, Search } from "@material-ui/icons"; +import { CameraAlt, Search } from "@material-ui/icons"; const Wrapper = styled.div` height: 100%; width: 100%; @@ -27,7 +28,7 @@ const HeaderContainer = styled.div` width: 100%; padding: 15px; align-items: center; - background: #fff; + background: #111c33; box-shadow: 0 1px 1.5px rgba(0, 0, 0, 0.1); user-select: none; position: fixed; @@ -48,17 +49,6 @@ const IconWrapper = styled.div` cursor: pointer; margin-left: 10px; `; -const Button = styled.button` - border: none; - color: #fff; - background: #1ecbe1; - padding: 8px 15px; - border-radius: 5px; - cursor: pointer; - &:hover { - opacity: 0.7; - } -`; const Menu = styled.div` flex: 1 1 auto; display: flex; @@ -69,7 +59,7 @@ const Menu = styled.div` `; const MenuItem = styled.div` margin: 0 10px; - color: #6e6d7a; + color: gray; transition: 0.3s; cursor: pointer; &:hover { @@ -85,7 +75,7 @@ const TakePicture = styled.div` position: absolute; bottom: 20px; right: 20px; - background: #1ecbe1; + background: #111c33; color: #fff; display: flex; justify-content: center; @@ -95,28 +85,71 @@ const TakePicture = styled.div` cursor: pointer; `; const CircledImage = styled.img` - height: 35px; - width: 35px; + height: ${(props) => props.dim}px; + width: ${(props) => props.dim}px; border-radius: 50%; cursor: pointer; &:hover { opacity: 0.7; } `; -const SearchContainer = styled.div` +const ProfileWrapper = styled.div` + height: 300px; + width: 300px; + position: absolute; + z-index: 250; + top: 70px; + right: 50px; + background: #111c33; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + border-radius: 3.5px; display: flex; - padding: 10px; - margin-top: 60px; + justify-content: flex-start; + align-items: center; + flex-direction: column; + padding: 15px; + @media screen and (max-width: 720px) { + right: 5px; + } +`; +const Text = styled.p` + color: ${(props) => props.color}; + font-family: "Montserrat", sans-serif; + font-weight: 450; + margin-top: 8px; + font-size: ${(props) => props.size}px; +`; +const Button = styled.div` + height: auto; + width: auto; + background: #1ecbe1; + color: #fff; + border-radius: 25px; + display: flex; + justify-content: center; + align-items: center; + padding: 5px 20px; + margin-top: 35px; + font-size: 15px; + cursor: pointer; + border: 2px solid #fff; +`; +const SearchContainer = styled.div` + height: auto; + min-width: 300px; + position: absolute; + top: 75px; + right: 5px; + z-index: 300; `; const SearchInput = styled.input` - height: 35px; - background: #fff; - flex: 1; + height: 40px; + width: 100%; + background: #111c33; border: none; - padding: 10px; - margin-right: 10px; - border-radius: 5px; - box-shadow: 0 1px 1.5px rgba(0, 0, 0, 0.1); + color: #1ecbe1; + padding: 15px; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); &:focus { outline: none; } @@ -126,16 +159,25 @@ const Showcase = ({ onUserLogOut }) => { const user = useContext(LoginContext); const [showCamera, setShowCamera] = useState(false); const [animeDone, setAnimeDone] = useState(false); - const handleTakePicture = () => { - setShowCamera(!showCamera); - }; + const [showProfile, setShowProfile] = useState(false); + const [showSearchInput, setShowSearchInput] = useState(false); + const [noteCount, setNoteCount] = useState(0); const handleHideCamera = (refresh) => { setAnimeDone(false); setTimeout(() => { setShowCamera(!showCamera); }, 350); }; - + const handleShowProfile = () => { + setShowSearchInput(false); + setShowProfile(!showProfile); + }; + const handleNoteCount = (count) => setNoteCount(count); + const handleShowSearchInput = () => { + setShowSearchInput(!showSearchInput); + setShowProfile(false); + }; + const handleTakePicture = () => setShowCamera(!showCamera); return ( {showCamera && } @@ -153,20 +195,41 @@ const Showcase = ({ onUserLogOut }) => { I&Ct - - + + {/* Profile */} + {showProfile && ( + + + + {user && user.displayName} + + + {user && user.email} + + + Notes: {noteCount === 0 ? "..." : noteCount} + + + + )} + - - - - {/* - - - */} - + {showSearchInput && ( + // + // + // + + )} + {/* Notes */} + + {/* Take pictur button */} diff --git a/src/components/Test.js b/src/components/Test.js deleted file mode 100644 index 2ffe1f0..0000000 --- a/src/components/Test.js +++ /dev/null @@ -1,15 +0,0 @@ -import React, { useContext, useEffect } from "react"; -import LoginContext from "../context"; -const Test = () => { - const user = useContext(LoginContext); - useEffect(() => { - console.log(user); - }, [user]); - return ( -
-

{user && user.displayName}

- -
- ); -}; -export default Test; diff --git a/src/components/TransitionView.js b/src/components/TransitionView.js new file mode 100644 index 0000000..800f694 --- /dev/null +++ b/src/components/TransitionView.js @@ -0,0 +1,59 @@ +import React, { useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +const Container = styled.div` + height: 100vh; + width: 100vw; + background: transparent; + position: absolute; + z-index: 300; +`; +const TransitionView = ({ from, to, willLast, onClose, children }) => { + const containerRef = useRef(null); + const [tValue, setTvalue] = useState(`translate(0)`); + useEffect(() => { + containerRef.current.style.transform = tValue; + }, [tValue]); + const handleClose = () => { + switch (to) { + case "left": + setTvalue(`translateX(-100%)`); + break; + case "right": + setTvalue(`translateX(100%)`); + break; + case "top": + setTvalue(`translateY(-100%)`); + break; + case "bottom": + setTvalue(`translateY(100%)`); + break; + default: + setTvalue(`translate(0)`); + break; + } + setTimeout(() => { + onClose(); + }, parseInt(willLast)); + }; + return ( + + {children(handleClose)} + + ); +}; +export default TransitionView; diff --git a/src/index.css b/src/index.css index a6dda02..6bc4890 100644 --- a/src/index.css +++ b/src/index.css @@ -10,3 +10,11 @@ body { height: 100%; overflow: hidden; } + +ion-icon { + color: #fff; + font-size: 2rem; + cursor: pointer; + display: block; + margin: 0 20px; +} diff --git a/src/index.js b/src/index.js index 846ed2c..d4e8591 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,6 @@ import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; - ReactDOM.render(