diff --git a/.gitignore b/.gitignore index eba46e14..47d0085a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ __pycache__/ # Distribution / packaging .Python build/ +Scripts/ +include/ develop-eggs/ dist/ downloads/ @@ -30,6 +32,8 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +pyvenv.cfg + # PyInstaller # Usually these files are written by a python script from a template @@ -116,6 +120,7 @@ venv/ ENV/ env.bak/ venv.bak/ +.vscode/ Scripts/* pyvenv.cfg diff --git a/app/main.py b/app/main.py index fa778725..12345234 100644 --- a/app/main.py +++ b/app/main.py @@ -8,8 +8,8 @@ from app.dependencies import (logger, MEDIA_PATH, STATIC_PATH, templates) from app.internal.quotes import daily_quotes, load_quotes from app.routers import ( - agenda, categories, dayview, email, event, invitation, profile, search, - telegram, whatsapp + agenda, calendar, categories, dayview, email, + event, invitation, profile, search, telegram, whatsapp ) from app.telegram.bot import telegram_bot @@ -36,6 +36,7 @@ def create_tables(engine, psql_environment): routers_to_include = [ agenda.router, + calendar.router, categories.router, dayview.router, email.router, diff --git a/app/routers/calendar.py b/app/routers/calendar.py new file mode 100644 index 00000000..4762e16f --- /dev/null +++ b/app/routers/calendar.py @@ -0,0 +1,39 @@ +from http import HTTPStatus + +from app.dependencies import templates +from app.routers import calendar_grid as cg +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from starlette.responses import Response + +router = APIRouter( + prefix="/calendar/month", + tags=["calendar"], + responses={404: {"description": "Not found"}}, +) + +ADD_DAYS_ON_SCROLL: int = 42 + + +@router.get("/") +async def calendar(request: Request) -> Response: + user_local_time = cg.Day.get_user_local_time() + day = cg.create_day(user_local_time) + return templates.TemplateResponse( + "calendar/calendar.html", + { + "request": request, + "day": day, + "week_days": cg.Week.DAYS_OF_THE_WEEK, + "weeks_block": cg.get_month_block(day) + } + ) + + +@router.get("/{date}") +async def update_calendar(request: Request, date: str) -> HTMLResponse: + last_day = cg.Day.convert_str_to_date(date) + next_weeks = cg.create_weeks(cg.get_n_days(last_day, ADD_DAYS_ON_SCROLL)) + template = templates.get_template('calendar/add_week.html') + content = template.render(weeks_block=next_weeks) + return HTMLResponse(content=content, status_code=HTTPStatus.OK) diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py new file mode 100644 index 00000000..d8ceba23 --- /dev/null +++ b/app/routers/calendar_grid.py @@ -0,0 +1,212 @@ +import calendar +import itertools +import locale +from datetime import date, datetime, timedelta +from typing import Dict, Iterator, List, Tuple + +import pytz + +MONTH_BLOCK: int = 6 + +locale.setlocale(locale.LC_TIME, ("en", "UTF-8")) + + +class Day: + """A Day class. + + Args: + date (datetime): A single datetime date. + Arguments: + date (datetime): A single datetime date. + sday (str): The day name. + dailyevents (List): List of tuples represent daily event information. + events (List): List of tuples represent time event name. + EX: [("09AP", "Meeting with yam")] + css (Dict): All css classes represent day. + """ + + def __init__(self, date: datetime): + self.date: datetime = date + self.sday: str = self.date.strftime("%A") + self.dailyevents: List[Tuple] = [] + self.events: List[Tuple] = [] + self.css: Dict[str, str] = { + 'day_container': 'day', + 'date': 'day-number', + 'daily_event': 'month-event', + 'daily_event_front': ' '.join([ + 'daily', + 'front', + 'background-warmyellow' + ]), + 'daily_event_back': ' '.join([ + 'daily', + 'back', + 'text-darkblue', + 'background-lightgray' + ]), + 'event': 'event', + } + + def __str__(self) -> str: + return self.date.strftime("%d") + + def display(self) -> str: + """Returns day date inf the format of 00 MONTH 00""" + return self.date.strftime("%d %B %y").upper() + + def set_id(self) -> str: + """Returns day date inf the format of 00-mon-0000""" + return self.date.strftime("%d-%b-%Y") + + @classmethod + def get_user_local_time(cls) -> datetime: + greenwich = pytz.timezone('GB') + return greenwich.localize(datetime.now()) + + @classmethod + def convert_str_to_date(cls, date_string: str) -> datetime: + return datetime.strptime(date_string, '%d-%b-%Y') + + @classmethod + def is_weekend(cls, date: date) -> bool: + """Returns true if this day is represent a weekend.""" + return date.strftime("%A") in Week.DAYS_OF_THE_WEEK[-2:] + + +class DayWeekend(Day): + def __init__(self, date: datetime): + super().__init__(date) + self.css = { + 'day_container': 'day ', + 'date': ' '.join(['day-number', 'text-gray']), + 'daily_event': 'month-event', + 'daily_event_front': ' '.join([ + 'daily', + 'front', + 'background-warmyellow' + ]), + 'daily_event_back': ' '.join([ + 'daily', + 'back', + 'text-darkblue', + 'background-lightgray' + ]), + 'event': 'event', + } + + +class Today(Day): + def __init__(self, date: datetime): + super().__init__(date) + self.css = { + 'day_container': ' '.join([ + 'day', + 'text-darkblue', + 'background-yellow' + ]), + 'date': 'day-number', + 'daily_event': 'month-event', + 'daily_event_front': ' '.join([ + 'daily', + 'front', + 'text-lightgray', + 'background-darkblue' + ]), + 'daily_event_back': ' '.join([ + 'daily', + 'back', + 'text-darkblue', + 'background-lightgray' + ]), + 'event': 'event', + } + + +class FirstDayMonth(Day): + def __init__(self, date: datetime): + super().__init__(date) + self.css = { + 'day_container': ' '.join([ + 'day', + 'text-darkblue', + 'background-lightgray' + ]), + 'date': 'day-number', + 'daily_event': 'month-event', + 'daily_event_front': ' '.join([ + 'daily front', + 'text-lightgray', + 'background-red' + ]), + 'daily_event_back': ' '.join([ + 'daily', + 'back', + 'text-darkblue', + 'background-lightgray' + ]), + 'event': 'event', + } + + def __str__(self) -> str: + return self.date.strftime("%d %b %y").upper() + + +class Week: + WEEK_DAYS: int = 7 + DAYS_OF_THE_WEEK: List[str] = calendar.day_name + + def __init__(self, days: List[Day]): + self.days: List[Day] = days + + +def create_day(day: datetime) -> Day: + """Return the currect day object according to given date.""" + if day == date.today(): + return Today(day) + if int(day.day) == 1: + return FirstDayMonth(day) + if Day.is_weekend(day): + return DayWeekend(day) + return Day(day) + + +def get_next_date(date: datetime) -> Iterator[Day]: + """Generate date objects from a starting given date.""" + yield from ( + create_day(date + timedelta(days=i)) + for i in itertools.count(start=1) + ) + + +def get_date_before_n_days(date: datetime, n: int) -> datetime: + """Returns the date before n days.""" + return date - timedelta(days=n) + + +def get_first_day_month_block(date: datetime) -> datetime: + """Returns the first date in a month block of given date.""" + return list(calendar.Calendar().itermonthdates(date.year, date.month))[0] + + +def get_n_days(date: datetime, n: int) -> Iterator[Day]: + """Generate n dates from a starting given date.""" + next_date_gen = get_next_date(date) + yield from itertools.islice(next_date_gen, n) + + +def create_weeks( + days: Iterator[Day], + length: int = Week.WEEK_DAYS +) -> List[Week]: + """Return lists of Weeks objects.""" + ndays: List[Day] = list(days) + num_days: int = len(ndays) + return [Week(ndays[i:i + length]) for i in range(0, num_days, length)] + + +def get_month_block(day: Day, n: int = MONTH_BLOCK) -> List[Week]: + """Returns a 2D list represent a n days calendar from current month.""" + current = get_first_day_month_block(day.date) - timedelta(days=1) + num_of_days = Week.WEEK_DAYS * n + return create_weeks(get_n_days(current, num_of_days)) diff --git a/app/static/grid_style.css b/app/static/grid_style.css new file mode 100644 index 00000000..073708a4 --- /dev/null +++ b/app/static/grid_style.css @@ -0,0 +1,287 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-family: "Assistant", "Ariel", sans-serif; + font-weight: 400; + font-size: 62.5%; /*16px / 10px = 62.5% -> 1rem = 10px*/ + line-height: 1.7; + text-rendering: optimizeLegibility; + scroll-behavior: smooth; + background-color: #F7F7F7; + color: #222831; +} + +a { + text-decoration: none; + color: inherit; +} + +/* Grids */ +/* -> Website Structure */ +.container { + display: grid; + grid-template-columns: 4.8rem minmax(0, auto) 1fr; +} + +nav { + z-index: 5; + grid-row: 1/3; +} + +.menu { + position: sticky; + display: flex; + flex-direction: column; + top: 1rem; +} + +.fixed-features, +.user-features, +#open-features { + text-align: center; +} + +.user-features {margin-top: 3rem;} + +.fixed-features div { + font-size: 3rem; + height: 4.4rem; +} + +.fixed-features div:hover { + color: #FFDE4D; +} + +.user-features div { + height: 2.8rem; + width: 2.8rem; + font-size: 2rem; + margin: 1.2rem auto auto auto; + color: #F7F7F7; + background-color: #0CA789; + border-radius: 0.2rem; +} + +.user-features div:hover { + color: #222831; + background-color: #FFDE4D; +} + +.user-features div:visited:active { + color: #222831; + background-color: #FFDE4D; +} + +#open-features { + font-size: 2rem; + margin-top: 1rem; +} + +#feature-settings { + visibility: hidden; + width: 0.1rem; + grid-row: 1/3; +} + +.settings-open { + width: 30rem; +} + +img {fill: #F7F7F7;} + +header { + z-index: 5; + position: sticky; + top: 0; + display: flex; + grid-flow: row wrap; + margin: 0 1rem 0 1rem; + background-color: #F7F7F7; +} + +header div { + flex: 1; +} + +#logo-div { + text-align: end; + padding-right: 2rem; +} + +/* Main Element Grid */ +main { + display: flex; + flex-flow: row wrap; +} + +.calendar {flex: 1;} + +.day-view-visible { + flex: 0 1 30%; + opacity: 1; + transition: all 1s; +} + +.calender-days-titles { + z-index: 4; + position: sticky; + top: 6rem; + align-self: flex-start; + grid-column: 1/3; + display: grid; + grid-template-columns: repeat(7, 1fr); + margin: 1rem 1rem 0 1rem; + background-color: #F7F7F7; +} + +/* The Calendar Grid */ +#calender-grid { + flex: 1; + display: flex; + flex-direction: column; + margin: 0 1rem 0 1rem; +} + +.week { + flex: 1; + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-auto-rows: 12rem; + font-weight: 900; + font-size: 2rem; +} + +.week:hover { + box-shadow: -5px 0px 0px 0px #FFDE4D; +} + +.day { + z-index: 0; + display: flex; + flex-direction: column; + overflow: hidden; + + font-size: 1.2rem; + padding: 0 1rem 0 1rem; + border: 1px solid #e9ecef; + + overflow-y: auto; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.day::-webkit-scrollbar {display: none;} + +.event {font-weight: 400;} + +.day:hover {border: 0.2rem solid #222831;} + +.day:hover .day-number{color: #F24726;} + +.day:hover .add-small{display: block;} + +.day-number { + font-weight: 900; + font-size: 2rem; + text-align: left; +} + +.day_current { + background-color: #FFDE4D; + color: white; +} + +.month-day-header { + display: flex; + flex-flow: row wrap; +} + +.month-day-header div {flex-grow: 2;} + +.month-day-header:last-child{text-align: right;} + +.add-small { + display: none; + font-weight: 900; + font-size: 2rem; + padding-right: 0.5rem; +} + +/* Dates Navigation */ +.dates-navigation {position: fixed;} + +/* Events - Rotation*/ +.month-event { + position: relative; + perspective: 150rem; + -moz-perspective: 150rem; + height: 2.4rem; +} + +.month-event div{ + height: 2.4rem; + width: 100%; + transition: all 0.3s ease; + position: absolute; + top: 0; + left: 0; + backface-visibility: hidden; +} + +.back {transform: rotateX(180deg);} + +.month-event:hover .front{transform: rotateX(-180deg);} + +.month-event:hover .back{transform: rotateX(0);} + +.daily { + font-weight: 700; + border-radius: 0.4rem; + padding: 0 0.5rem 0 0.5rem; +} + +/* Titles */ +.day-title { + font-weight: 600; + font-size: 1.6rem; +} + +.title { + font-weight: 900; + font-size: 2.4rem; + margin-bottom: -1rem; +} + +.sec-title { + font-weight: 500; + font-size: 1.6rem; +} + +/* Text Colors */ +.text-yellow {color: #FFDE4D;} + +.text-gray {color: #adb5bd;} + +.text-lightgray {color: #F7F7F7;} + +.text-darkblue {color: #222831;} + +/* Borders */ +.border-dash-darkblue {border: 2px dashed #222831;} + +.border-darkblue {border: 2px solid #222831;} + +.underline-yellow {border-bottom: 4px solid #FFDE4D;} + +/* Background Color */ +.background-darkblue {background-color: #222831;} + +.background-red {background-color: #F24726;} + +.background-lightgray {background-color: #e9ecef;} + +.background-yellow {background-color: #FFDE4D;} \ No newline at end of file diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js new file mode 100644 index 00000000..61280130 --- /dev/null +++ b/app/static/js/grid_scripts.js @@ -0,0 +1,45 @@ +function setToggle(elementClass, targetElement, classToAdd, lastIndex) { + const allDays = document.getElementsByClassName(elementClass); + const target = document.getElementById(targetElement); + for (let i = lastIndex; i < allDays.length; ++i) { + allDays[i].addEventListener("click", function () { + target.classList.toggle(classToAdd); + }) + } +} + +document.addEventListener( + 'DOMContentLoaded', function () { + setToggle("day", "day-view", "day-view-visible", 0); + } +) + +function loadWeek(lastDay, index) { + if (lastDay.dataset.last === "false") { + return false; + } + const path = '/calendar/month/' + lastDay.id; + const newDays = document.createElement('html'); + fetch(path).then(function (response) { + lastDay.dataset.last = false; + return response.text(); + }).then(function (html) { + const newDiv = document.createElement("div"); + newDays.innerHTML = html; + newDiv.appendChild(newDays); + document.getElementById("calender-grid").append(newDays); + setToggle("day", "day-view", "day-view-visible", index); + }); +} + +window.addEventListener( + 'scroll', function () { + const tolerance = 1; + if (window.scrollY + window.innerHeight + tolerance < document.documentElement.scrollHeight) { + return false; + } + const allDays = document.getElementsByClassName('day'); + const lastDay = allDays[allDays.length - 1]; + loadWeek(lastDay, allDays.length); + } +) \ No newline at end of file diff --git a/app/static/style.css b/app/static/style.css index 5b82884f..ce0e7d42 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -1,27 +1,27 @@ body { - background: #A1FFCE; - background: -webkit-linear-gradient(to right, #FAFFD1, #A1FFCE); - background: linear-gradient(to right, #FAFFD1, #A1FFCE); + background: #A1FFCE; + background: -webkit-linear-gradient(to right, #FAFFD1, #A1FFCE); + background: linear-gradient(to right, #FAFFD1, #A1FFCE); } .profile-image { - width: 7em; + width: 7em; } .card-profile { - border-radius: 10px; + border-radius: 10px; } .card-event { - border-radius: 10px; + border-radius: 10px; } .card-event:hover { - transform: scale(1.02); + transform: scale(1.02); } .event-posted-time { - font-size: 0.7rem; + font-size: 0.7rem; } .no-border { @@ -31,27 +31,27 @@ body { .profile-modal-fadeIn { -webkit-animation-name: profile-modal-fadeIn; animation-name: profile-modal-fadeIn; - + -webkit-animation-duration: 1s; animation-duration: 1s; -webkit-animation-fill-mode: both; animation-fill-mode: both; } - + @keyframes profile-modal-fadeIn { from { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } - + to { opacity: 1; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } - + .profile-modal-dialog { margin: 0; } @@ -59,5 +59,4 @@ body { .profile-modal-header { border: none; background-color:whitesmoke; -} - \ No newline at end of file +} \ No newline at end of file diff --git a/app/templates/calendar/add_week.html b/app/templates/calendar/add_week.html new file mode 100644 index 00000000..702ee572 --- /dev/null +++ b/app/templates/calendar/add_week.html @@ -0,0 +1,21 @@ +{% for week in weeks_block %} +