| @@ -10601,6 +10601,11 @@ | |||||
| "minimist": "^1.2.5" | "minimist": "^1.2.5" | ||||
| } | } | ||||
| }, | }, | ||||
| "moment": { | |||||
| "version": "2.29.1", | |||||
| "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", | |||||
| "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" | |||||
| }, | |||||
| "move-concurrently": { | "move-concurrently": { | ||||
| "version": "1.0.1", | "version": "1.0.1", | ||||
| "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", | "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", | ||||
| @@ -10,6 +10,7 @@ | |||||
| "@types/node": "^12.20.15", | "@types/node": "^12.20.15", | ||||
| "@types/react": "^17.0.13", | "@types/react": "^17.0.13", | ||||
| "@types/react-dom": "^17.0.8", | "@types/react-dom": "^17.0.8", | ||||
| "moment": "^2.29.1", | |||||
| "node-sass": "^6.0.1", | "node-sass": "^6.0.1", | ||||
| "query-string": "^7.0.1", | "query-string": "^7.0.1", | ||||
| "react": "^17.0.2", | "react": "^17.0.2", | ||||
| @@ -17,6 +17,7 @@ import { ReactComponent as TimeIcon } from './assets/icons/time.svg'; | |||||
| import { IProfile } from "./structure/profile"; | import { IProfile } from "./structure/profile"; | ||||
| import { ALL_WORDS } from "./data/all-words"; | import { ALL_WORDS } from "./data/all-words"; | ||||
| import { WordDetails } from "./components/word-details/WordDetails"; | import { WordDetails } from "./components/word-details/WordDetails"; | ||||
| import { Calendar } from "./components/calendar/Calendar"; | |||||
| export var userProfileData : IProfile = { | export var userProfileData : IProfile = { | ||||
| @@ -78,6 +79,7 @@ function App() { | |||||
| <Route path="/shelf-details/" component={ShelfDetails} /> | <Route path="/shelf-details/" component={ShelfDetails} /> | ||||
| <Route path="/word-details/" component={WordDetails} /> | <Route path="/word-details/" component={WordDetails} /> | ||||
| <Route path="/revise" component={Revise} /> | <Route path="/revise" component={Revise} /> | ||||
| <Route path="/calendar" component={Calendar} /> | |||||
| <Redirect from="/" to="/home" /> | <Redirect from="/" to="/home" /> | ||||
| </Switch> | </Switch> | ||||
| <Tabs /> | <Tabs /> | ||||
| @@ -0,0 +1,207 @@ | |||||
| .navHeader { | |||||
| background-color: transparent; | |||||
| text-align: center; | |||||
| position: relative; | |||||
| display: flex; | |||||
| justify-content: flex-start; | |||||
| align-items: center; | |||||
| padding: 1rem 1.5rem; | |||||
| button { | |||||
| width: 4rem; | |||||
| text-align: center; | |||||
| background-color: transparent; | |||||
| border: 0; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: flex-start; | |||||
| svg { | |||||
| width: 1rem; | |||||
| color: var(--black); | |||||
| } | |||||
| } | |||||
| } | |||||
| .todayHeader { | |||||
| padding: 0 2rem; | |||||
| margin-top: 2rem; | |||||
| h4 { | |||||
| font-size: 2rem; | |||||
| color: var(--black); | |||||
| } | |||||
| p { | |||||
| font-size: 1.2rem; | |||||
| color: var(--light-grey); | |||||
| letter-spacing: 0.5px; | |||||
| font-weight: 300; | |||||
| } | |||||
| h5 { | |||||
| margin-top: 3rem; | |||||
| font-size: 1.6rem; | |||||
| font-weight: 400; | |||||
| color: var(--black); | |||||
| padding-bottom: 0.5rem; | |||||
| } | |||||
| } | |||||
| .currentWeek { | |||||
| padding: 0.5rem; | |||||
| list-style: none; | |||||
| display: grid; | |||||
| grid-template-columns: repeat(7, 1fr); | |||||
| background-color: var(--creamy-white); | |||||
| z-index: 1; | |||||
| position: sticky; | |||||
| top: 0; | |||||
| box-shadow: 0px 5px 5px -8px var(--light-grey); | |||||
| li { | |||||
| text-align: center; | |||||
| padding: 0.5rem 0; | |||||
| &:last-child { | |||||
| label, span { | |||||
| color: var(--red); | |||||
| } | |||||
| } | |||||
| &.active { | |||||
| background-color: var(--teal); | |||||
| border-radius: 1rem; | |||||
| label, span { | |||||
| color: white; | |||||
| } | |||||
| } | |||||
| } | |||||
| label { | |||||
| display: block; | |||||
| font-size: 1.2rem; | |||||
| color: var(--light-grey); | |||||
| } | |||||
| span { | |||||
| display: block; | |||||
| font-size: 1.2rem; | |||||
| font-weight: 600; | |||||
| color: var(--black); | |||||
| } | |||||
| } | |||||
| .currentDayDetails { | |||||
| padding: 1rem; | |||||
| list-style: none; | |||||
| li { | |||||
| min-height: 4rem; | |||||
| position: relative; | |||||
| &:nth-child(4n - 2) .task::before { | |||||
| background-color: var(--orange); | |||||
| } | |||||
| &:nth-child(4n - 1) .task::before { | |||||
| background-color: var(--blue); | |||||
| } | |||||
| &:nth-child(4n - 3) .task::before { | |||||
| background-color: var(--red); | |||||
| } | |||||
| &:nth-child(4n - 4) .task::before { | |||||
| background-color: var(--teal); | |||||
| } | |||||
| &.cycle { | |||||
| label { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: flex-start; | |||||
| &::after { | |||||
| content: ''; | |||||
| flex-grow: 1; | |||||
| height: 2px; | |||||
| border-bottom: 2px dashed var(--light-grey); | |||||
| margin-left: 2rem; | |||||
| transform: scaleY(0.4); | |||||
| } | |||||
| } | |||||
| } | |||||
| label { | |||||
| font-size: 1.4rem; | |||||
| color: var(--light-grey); | |||||
| text-align: left; | |||||
| opacity: 0.8; | |||||
| } | |||||
| } | |||||
| .task { | |||||
| width: calc(100% - 6rem); | |||||
| display: block; | |||||
| margin-left: auto; | |||||
| padding: 1rem 2rem; | |||||
| border-radius: 2.5rem; | |||||
| position: relative; | |||||
| overflow: visible; | |||||
| line-height: 1.7; | |||||
| &.completed { | |||||
| opacity: 0.5; | |||||
| } | |||||
| &::before { | |||||
| content: ''; | |||||
| position: absolute; | |||||
| left: 0; | |||||
| top: 0; | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| background-color: var(--orange); | |||||
| opacity: 0.5; | |||||
| border-radius: inherit; | |||||
| } | |||||
| &>* { | |||||
| position: relative; | |||||
| } | |||||
| h5 { | |||||
| font-size: 1.6rem; | |||||
| color: var(--black); | |||||
| } | |||||
| p { | |||||
| font-size: 1.1rem; | |||||
| color: var(--grey); | |||||
| } | |||||
| .indicator { | |||||
| position: absolute; | |||||
| left: -2rem; | |||||
| top: calc(50% - 0.8rem); | |||||
| width: 1.6rem; | |||||
| height: 1.6rem; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| border-radius: 50%; | |||||
| color: white; | |||||
| &.overTime { | |||||
| background-color: var(--red); | |||||
| } | |||||
| &.completed { | |||||
| background-color: var(--teal); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,173 @@ | |||||
| import React, { useState } from 'react'; | |||||
| import styles from './Calendar.module.scss'; | |||||
| import { ReactComponent as ChevronLeft } from '../../assets/icons/chevron-left.svg'; | |||||
| import moment from 'moment'; | |||||
| import { ISchedule } from '../../structure/schedule'; | |||||
| import { NavLink } from 'react-router-dom'; | |||||
| const SCHEDULE: Array<ISchedule> = [{ | |||||
| calendar_date: { | |||||
| date: moment().format('DD'), | |||||
| month: moment().format('MM'), | |||||
| year: moment().format('YYYY'), | |||||
| time: { | |||||
| hours: 8, | |||||
| minutes: 0 | |||||
| } | |||||
| }, | |||||
| title: 'Vocabulary: All Words', | |||||
| description: 'Revision of "All Words" in Vocabulary shelf', | |||||
| revision_link: '/revise', | |||||
| external_link: '', | |||||
| duration_in_min: 10, | |||||
| isCompleted: true | |||||
| }, { | |||||
| calendar_date: { | |||||
| date: moment().format('DD'), | |||||
| month: moment().format('MM'), | |||||
| year: moment().format('YYYY'), | |||||
| time: { | |||||
| hours: 10, | |||||
| minutes: 0 | |||||
| } | |||||
| }, | |||||
| title: 'Revision Test: All Shelves', | |||||
| description: '', | |||||
| revision_link: '/revise', | |||||
| external_link: '', | |||||
| duration_in_min: 30, | |||||
| isCompleted: false | |||||
| }, { | |||||
| calendar_date: { | |||||
| date: moment().format('DD'), | |||||
| month: moment().format('MM'), | |||||
| year: moment().format('YYYY'), | |||||
| time: { | |||||
| hours: 17, | |||||
| minutes: 30 | |||||
| } | |||||
| }, | |||||
| title: 'Books: Sapiens', | |||||
| description: 'Revision of "Sapiens" in Books shelf', | |||||
| revision_link: '/revise', | |||||
| external_link: '', | |||||
| duration_in_min: 30, | |||||
| isCompleted: false | |||||
| }, ]; | |||||
| function getCurrentWeek() { | |||||
| let currentDate = moment(); | |||||
| let weekStart = currentDate.clone().startOf('isoWeek'); | |||||
| let days = []; | |||||
| for (let i = 0; i <= 6; i++) { | |||||
| days.push({ | |||||
| date: moment(weekStart).add(i, 'days').format("DD"), | |||||
| day: moment(weekStart).add(i, 'days').format("ddd"), | |||||
| month_name: moment(weekStart).add(i, 'days').format("MMMM"), | |||||
| month: moment(weekStart).add(i, 'days').format("MM"), | |||||
| year: moment().format('YYYY') | |||||
| }); | |||||
| } | |||||
| return days; | |||||
| } | |||||
| export const Calendar: React.FC = () => { | |||||
| const today = { | |||||
| date: moment().format('DD'), | |||||
| day: moment().format('ddd'), | |||||
| month_name: moment().format('MMMM'), | |||||
| month: moment().format('MM'), | |||||
| year: moment().format('YYYY') | |||||
| }; | |||||
| const currentTime = { | |||||
| hours: Number(moment().format('HH')), | |||||
| minutes: Number(moment().format('mm')) | |||||
| } | |||||
| let current_week = getCurrentWeek(); | |||||
| const [selectedDate, setSelectedDate] = useState<{ | |||||
| date: string, | |||||
| month: string, | |||||
| year: string | |||||
| }>({ | |||||
| date: today.date, | |||||
| month: today.month, | |||||
| year: today.year | |||||
| }); | |||||
| let hours = []; | |||||
| for (let i = 0; i < 24; i += 1) { | |||||
| hours.push(i); | |||||
| } | |||||
| return <section className="modalPage"> | |||||
| <header className={styles.navHeader}> | |||||
| <button onClick={() => window.history.back()}> | |||||
| <ChevronLeft /> | |||||
| </button> | |||||
| </header> | |||||
| <header className={styles.todayHeader}> | |||||
| <h4> Today </h4> | |||||
| <p> Productive day, Neymar </p> | |||||
| <h5> { today.month_name }, { today.year } </h5> | |||||
| </header> | |||||
| <ul className={styles.currentWeek}> | |||||
| { current_week.map((week, index) => { | |||||
| return <li key={index} className={week.date === selectedDate.date && week.month === selectedDate.month ? styles.active : ''} | |||||
| onClick={() => setSelectedDate({ | |||||
| date: week.date, | |||||
| month: week.month, | |||||
| year: week.year | |||||
| })}> | |||||
| <label> { week.day } </label> | |||||
| <span> { week.date } </span> | |||||
| </li> | |||||
| }) } | |||||
| </ul> | |||||
| <ul className={styles.currentDayDetails}> | |||||
| { hours.map((hours, index) => { | |||||
| return <li key={index} className={(hours % 4 === 0) ? styles.cycle : ''}> | |||||
| <label> { hours.toString().padStart(2, '0') }:00 </label> | |||||
| { SCHEDULE.map((schedule, schedule_index) => { | |||||
| if (schedule.calendar_date.date === selectedDate.date && | |||||
| schedule.calendar_date.month === selectedDate.month && | |||||
| schedule.calendar_date.year === selectedDate.year && | |||||
| schedule.calendar_date.time.hours === hours) { | |||||
| return <NavLink to={schedule.revision_link} key={schedule_index} | |||||
| className={styles.task + ' ' + (schedule.isCompleted ? styles.completed: '')} | |||||
| onClick={() => schedule.isCompleted = true}> | |||||
| { currentTime.hours >= schedule.calendar_date.time.hours && | |||||
| currentTime.minutes >= schedule.calendar_date.time.minutes && | |||||
| !schedule.isCompleted && <div className={styles.indicator + ' ' + styles.overTime}> | |||||
| ! | |||||
| </div> } | |||||
| { schedule.isCompleted && <div className={styles.indicator + ' ' + styles.completed}> | |||||
| ✓ | |||||
| </div> } | |||||
| <h5> { schedule.title } </h5> | |||||
| <p> { schedule.description } </p> | |||||
| <p> { schedule.duration_in_min } minutes practice </p> | |||||
| </NavLink> | |||||
| } | |||||
| }) } | |||||
| </li> | |||||
| }) } | |||||
| </ul> | |||||
| </section> | |||||
| } | |||||
| @@ -68,9 +68,9 @@ export const Home: React.FC = () => { | |||||
| <BookIcon /> | <BookIcon /> | ||||
| Revisions | Revisions | ||||
| </h4> | </h4> | ||||
| <button> | |||||
| <NavLink to="/calendar"> | |||||
| <CalendarIcon /> | <CalendarIcon /> | ||||
| </button> | |||||
| </NavLink> | |||||
| </header> | </header> | ||||
| <ul> | <ul> | ||||
| <li> | <li> | ||||
| @@ -134,9 +134,9 @@ export const Home: React.FC = () => { | |||||
| <span className={styles.text}> { shelf.words.length > 0 ? (shelf.revisedWords.length * 100/ shelf.words.length) : 0 }% </span> | <span className={styles.text}> { shelf.words.length > 0 ? (shelf.revisedWords.length * 100/ shelf.words.length) : 0 }% </span> | ||||
| </div> | </div> | ||||
| <h5> { category.name } </h5> | |||||
| <p> { shelf.name } </p> | |||||
| <h5> { shelf.name } </h5> | |||||
| <p> { shelf.words.length } words </p> | <p> { shelf.words.length } words </p> | ||||
| <p> { category.name } </p> | |||||
| </li> | </li> | ||||
| }) | }) | ||||
| }) } | }) } | ||||
| @@ -21,11 +21,9 @@ export const Revise: React.FC = () => { | |||||
| { progressState === 'END' && <div> | { progressState === 'END' && <div> | ||||
| <Summary /> | <Summary /> | ||||
| <NavLink to={'/home'}> | |||||
| <button className={styles.finishButton}> | |||||
| Done | |||||
| </button> | |||||
| </NavLink> | |||||
| <button className={styles.finishButton} onClick={() => window.history.back()}> | |||||
| Done | |||||
| </button> | |||||
| </div> } | </div> } | ||||
| </section> | </section> | ||||
| } | } | ||||
| @@ -0,0 +1,17 @@ | |||||
| export type ISchedule = { | |||||
| calendar_date: { | |||||
| date: string, | |||||
| month: string, | |||||
| year: string, | |||||
| time: { | |||||
| hours: number, | |||||
| minutes: number, | |||||
| } | |||||
| }, | |||||
| title: string, | |||||
| description: string, | |||||
| revision_link: string, | |||||
| external_link: string, | |||||
| duration_in_min: number, | |||||
| isCompleted: boolean, | |||||
| }; | |||||