diff --git a/api/tests/routes/api/v1/preference_test.py b/api/tests/routes/api/v1/preference_test.py index 6fffef50..420892b4 100644 --- a/api/tests/routes/api/v1/preference_test.py +++ b/api/tests/routes/api/v1/preference_test.py @@ -1,5 +1,12 @@ import json +from datetime import datetime +import pytest +from yelp_beans.models import MeetingSubscription +from yelp_beans.models import Rule +from yelp_beans.models import SubscriptionDateTime +from yelp_beans.models import User +from yelp_beans.models import UserSubscriptionPreferences from yelp_beans.routes.api.v1 import preferences from yelp_beans.routes.api.v1.preferences import preferences_api from yelp_beans.routes.api.v1.preferences import preferences_api_post @@ -51,3 +58,213 @@ def test_preference_api_post(monkeypatch, app, database, fake_user): assert response == "OK" assert fake_user.subscription_preferences == [] + + +def test_subscribe_api_post_no_user(client, session): + sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0)) + subscription = MeetingSubscription(datetime=[sub_time]) + session.add(subscription) + session.commit() + resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={}) + assert resp.status_code == 400 + + +def test_subscribe_api_post_user_cant_subscribe(client, session): + user = User(first_name="tester", last_name="user", email="darwin@yelp.com", meta_data={"email": "darwin@yelp.com"}) + session.add(user) + + sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0)) + rule = Rule(name="email", value="not-darwin@yelp.com") + subscription = MeetingSubscription( + datetime=[sub_time], + rule_logic="all", + user_rules=[rule], + ) + session.add(subscription) + session.commit() + resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email}) + assert resp.status_code == 403 + + +def test_subscribe_api_post_user_has_existing(client, session): + sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0)) + subscription = MeetingSubscription( + timezone="America/Los_Angeles", + datetime=[sub_time], + title="Test", + size=2, + office="tester", + location="test place", + user_rules=[], + default_auto_opt_in=False, + ) + session.add(subscription) + + # Setting auto_opt_in to true because subscription has default of false + preference = UserSubscriptionPreferences(subscription=subscription, preference=sub_time, auto_opt_in=True) + user = User( + first_name="tester", + last_name="user", + email="darwin@yelp.com", + meta_data={"email": "darwin@yelp.com"}, + subscription_preferences=[preference], + ) + session.add(user) + session.commit() + + resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email}) + assert resp.status_code == 200 + assert resp.json == { + "subscription": { + "id": subscription.id, + "default_auto_opt_in": False, + "location": "test place", + "name": "Test", + "office": "tester", + "rule_logic": None, + "rules": [], + "size": 2, + # Keeping the first of the preferences + "time_slots": [{"day": "thursday", "hour": 6, "minute": 0}], + "timezone": "America/Los_Angeles", + }, + "time_slot": { + "day": "thursday", + "hour": 6, + "minute": 0, + }, + "new_preference": False, + } + + new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one() + assert new_preference == preference + + +@pytest.mark.parametrize("default_auto_opt_in", (True, False)) +def test_subscribe_api_post_user_new_subscription(client, session, default_auto_opt_in): + sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0)) + subscription = MeetingSubscription( + timezone="America/Los_Angeles", + datetime=[sub_time], + title="Test", + size=2, + office="tester", + location="test place", + user_rules=[], + default_auto_opt_in=default_auto_opt_in, + ) + session.add(subscription) + + user = User( + first_name="tester", + last_name="user", + email="darwin@yelp.com", + meta_data={"email": "darwin@yelp.com"}, + subscription_preferences=[], + ) + session.add(user) + session.commit() + resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email}) + assert resp.status_code == 200 + assert resp.json == { + "subscription": { + "id": subscription.id, + "default_auto_opt_in": default_auto_opt_in, + "location": "test place", + "name": "Test", + "office": "tester", + "rule_logic": None, + "rules": [], + "size": 2, + # Keeping the first of the preferences + "time_slots": [{"day": "thursday", "hour": 6, "minute": 0}], + "timezone": "America/Los_Angeles", + }, + "time_slot": { + "day": "thursday", + "hour": 6, + "minute": 0, + }, + "new_preference": True, + } + + new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one() + assert new_preference.auto_opt_in == default_auto_opt_in + assert new_preference.subscription == subscription + assert new_preference.preference == sub_time + + +def test_subscribe_api_post_user_no_popular(client, session): + sub_time_1 = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0)) + sub_time_2 = SubscriptionDateTime(datetime=datetime(2017, 8, 20, 13, 0)) + subscription = MeetingSubscription( + timezone="America/Los_Angeles", + datetime=[sub_time_1, sub_time_2], + title="Test", + size=2, + office="tester", + location="test place", + user_rules=[], + default_auto_opt_in=True, + ) + session.add(subscription) + + user = User( + first_name="tester", + last_name="user", + email="darwin@yelp.com", + meta_data={"email": "darwin@yelp.com"}, + subscription_preferences=[], + ) + session.add(user) + session.commit() + + resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email}) + assert resp.status_code == 200 + assert resp.json["time_slot"] == { + "day": "thursday", + "hour": 6, + "minute": 0, + } + + new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one() + assert new_preference.preference == sub_time_1 + + +def test_subscribe_api_post_user_pick_popular(client, session): + sub_time_1 = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0)) + sub_time_2 = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 18, 0)) + subscription = MeetingSubscription( + timezone="America/Los_Angeles", + datetime=[sub_time_1, sub_time_2], + title="Test", + size=2, + office="tester", + location="test place", + user_rules=[], + default_auto_opt_in=True, + ) + session.add(subscription) + # Make a fake preference, so second time slot is more popular + preference = UserSubscriptionPreferences(subscription=subscription, preference=sub_time_2, auto_opt_in=True, user_id=200) + session.add(preference) + + user = User( + first_name="tester", + last_name="user", + email="darwin@yelp.com", + meta_data={"email": "darwin@yelp.com"}, + subscription_preferences=[], + ) + session.add(user) + session.commit() + resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email}) + assert resp.status_code == 200 + assert resp.json["time_slot"] == { + "day": "thursday", + "hour": 11, + "minute": 0, + } + + new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one() + assert new_preference.preference == sub_time_2 diff --git a/api/yelp_beans/logic/subscription.py b/api/yelp_beans/logic/subscription.py index 5d6fd454..3938e494 100644 --- a/api/yelp_beans/logic/subscription.py +++ b/api/yelp_beans/logic/subscription.py @@ -1,12 +1,17 @@ from datetime import datetime from datetime import timedelta +from typing import Any from database import db from pytz import timezone from pytz import utc +from sqlalchemy import func from yelp_beans.models import MeetingSpec from yelp_beans.models import MeetingSubscription +from yelp_beans.models import Rule +from yelp_beans.models import User +from yelp_beans.models import UserSubscriptionPreferences def filter_subscriptions_by_user_data(subscriptions, user): @@ -20,7 +25,9 @@ def filter_subscriptions_by_user_data(subscriptions, user): return approved_subscriptions -def apply_rules(user, subscription, subscription_rules): +def apply_rules( + user: User, subscription: MeetingSubscription | dict[str, Any], subscription_rules: list[Rule] +) -> MeetingSubscription | dict[str, Any] | None: """ Apply logic to rules set for each subscription. In a way this authorizes who can see the subscription. Rules can be applied in two ways: All rules must apply and @@ -133,3 +140,17 @@ def store_specs_from_subscription(subscription, week_start, specs): db.session.add_all(specs) db.session.commit() return specs + + +def get_subscriber_counts(subscription_id: int) -> dict[int, int]: + counts_query = ( + db.select(UserSubscriptionPreferences.preference_id, func.count("*")) + .filter(UserSubscriptionPreferences.subscription_id == subscription_id) + .group_by(UserSubscriptionPreferences.preference_id) + ) + counts = db.session.execute(counts_query).all() + return dict(counts) + + +def get_subscription(subscription_id: int) -> MeetingSubscription: + return MeetingSubscription.query.filter(MeetingSubscription.id == subscription_id).one() diff --git a/api/yelp_beans/logic/user.py b/api/yelp_beans/logic/user.py index 4db10261..bc709a65 100644 --- a/api/yelp_beans/logic/user.py +++ b/api/yelp_beans/logic/user.py @@ -1,4 +1,6 @@ import logging +from typing import NotRequired +from typing import TypedDict from database import db @@ -182,7 +184,12 @@ def remove_preferences(user, updated_preferences, subscription_id): return removed -def add_preferences(user, updated_preferences, subscription_id): +class PreferenceOptions(TypedDict): + active: NotRequired[bool] + auto_opt_in: NotRequired[bool] + + +def add_preferences(user: User, updated_preferences: dict[int, PreferenceOptions], subscription_id: int) -> set[int]: """ Parameters ---------- diff --git a/api/yelp_beans/routes/api/v1/preferences.py b/api/yelp_beans/routes/api/v1/preferences.py index 491989db..b62dd70b 100644 --- a/api/yelp_beans/routes/api/v1/preferences.py +++ b/api/yelp_beans/routes/api/v1/preferences.py @@ -1,14 +1,20 @@ import logging +from typing import Any from flask import Blueprint from flask import jsonify from flask import request +from yelp_beans.logic.subscription import apply_rules from yelp_beans.logic.subscription import filter_subscriptions_by_user_data +from yelp_beans.logic.subscription import get_subscriber_counts +from yelp_beans.logic.subscription import get_subscription from yelp_beans.logic.subscription import merge_subscriptions_with_preferences from yelp_beans.logic.user import add_preferences from yelp_beans.logic.user import get_user from yelp_beans.logic.user import remove_preferences +from yelp_beans.routes.api.v1.types import Subscription +from yelp_beans.routes.api.v1.types import TimeSlot preferences_blueprint = Blueprint("preferences", __name__) @@ -51,3 +57,43 @@ def preferences_api_post(subscription_id: int) -> str: logging.info(added) return "OK" + + +@preferences_blueprint.route("/subscribe/", methods=["POST"]) +def subscribe_api_post(subscription_id: int) -> dict[str, Any]: + data = request.json + user = get_user(data.get("email")) + if not user: + resp = jsonify({"msg": f"A user doesn't exist with the email of \"{data.get('email')}\""}) + resp.status_code = 400 + return resp + + subscription = get_subscription(subscription_id) + approved = apply_rules(user, subscription, subscription.user_rules) + if not approved: + resp = jsonify({"msg": "You are not eligible for this subscription"}) + resp.status_code = 403 + return resp + + datetime_to_subscriber_counts = get_subscriber_counts(subscription_id) + if datetime_to_subscriber_counts: + best_datetime_id, _ = max(datetime_to_subscriber_counts.items(), key=lambda row: row[1]) + else: + # No most popular time slot, so just pick the first one + best_datetime_id = subscription.datetime[0].id + + existing_matching_prefs = [pref for pref in user.subscription_preferences if pref.subscription_id == subscription_id] + + if existing_matching_prefs: + new_preference = False + else: + add_preferences(user, {best_datetime_id: {"active": True}}, subscription_id) + new_preference = True + + # get_subscriber_counts return this datetime id, so it should always exist in subscription.datetime + datetime = next(rule for rule in subscription.datetime if rule.id == best_datetime_id) + return { + "subscription": Subscription.from_sqlalchemy(subscription).model_dump(mode="json"), + "time_slot": TimeSlot.from_sqlalchemy(datetime, subscription.timezone).model_dump(mode="json"), + "new_preference": new_preference, + } diff --git a/api/yelp_beans/routes/api/v1/subscriptions.py b/api/yelp_beans/routes/api/v1/subscriptions.py index 680d9a01..035a7ece 100644 --- a/api/yelp_beans/routes/api/v1/subscriptions.py +++ b/api/yelp_beans/routes/api/v1/subscriptions.py @@ -1,126 +1,27 @@ from __future__ import annotations -import enum import json from datetime import datetime -from typing import Literal import arrow from database import db from flask import Blueprint from flask import jsonify from flask import request -from pydantic import BaseModel -from pydantic import ConfigDict -from pydantic import Field from pydantic import ValidationError -from pydantic import field_validator -from pytz import all_timezones -from pytz import utc +from yelp_beans.logic.subscription import get_subscription as get_subscription_model from yelp_beans.models import MeetingSubscription from yelp_beans.models import Rule from yelp_beans.models import SubscriptionDateTime +from yelp_beans.routes.api.v1.types import NewSubscription +from yelp_beans.routes.api.v1.types import RuleModel +from yelp_beans.routes.api.v1.types import Subscription +from yelp_beans.routes.api.v1.types import TimeSlot subscriptions_blueprint = Blueprint("subscriptions", __name__) -@enum.unique -class Weekday(enum.Enum): - MONDAY = "monday" - TUESDAY = "tuesday" - WEDNESDAY = "wednesday" - THURSDAY = "thursday" - FRIDAY = "friday" - SATURDAY = "saturday" - SUNDAY = "sunday" - - def to_day_number(self) -> int: - day_to_number = { - Weekday.MONDAY: 0, - Weekday.TUESDAY: 1, - Weekday.WEDNESDAY: 2, - Weekday.THURSDAY: 3, - Weekday.FRIDAY: 4, - Weekday.SATURDAY: 5, - Weekday.SUNDAY: 6, - } - return day_to_number[self] - - @staticmethod - def from_day_number(day_number: int) -> Weekday: - for day in Weekday: - if day_number == day.to_day_number(): - return day - raise ValueError(f"No day for day number of {day_number}") - - -class TimeSlot(BaseModel): - day: Weekday - hour: int = Field(ge=0, le=23) - minute: int = Field(0, ge=0, le=59) - model_config = ConfigDict(frozen=True) - - @classmethod - def from_sqlalchemy(cls, model: SubscriptionDateTime, timezone: str) -> RuleModel: - tz_time = arrow.get(model.datetime.replace(tzinfo=utc)).to(timezone) - return cls( - day=Weekday.from_day_number(model.datetime.weekday()), - hour=tz_time.hour, - minute=tz_time.minute, - ) - - -class RuleModel(BaseModel): - field: str - value: str - model_config = ConfigDict(frozen=True) - - @classmethod - def from_sqlalchemy(cls, model: Rule) -> RuleModel: - return cls(field=model.name, value=model.value) - - -class NewSubscription(BaseModel): - location: str = "Online" - name: str = Field(min_length=1) - office: str = "Remote" - rule_logic: Literal["any", "all", None] = None - rules: list[RuleModel] = Field(default_factory=list) - size: int = Field(2, ge=2) - time_slots: list[TimeSlot] = Field(min_length=1) - timezone: str = "America/Los_Angeles" - default_auto_opt_in: bool = False - - @field_validator("timezone") - @classmethod - def is_valid_timezone(cls, value: str) -> str: - if value not in all_timezones: - raise ValueError(f"{value} is not a valid timezone") - return value - - -class Subscription(NewSubscription): - id: int - - @classmethod - def from_sqlalchemy(cls, model: MeetingSubscription) -> Subscription: - rules = [RuleModel.from_sqlalchemy(rule) for rule in model.user_rules] - time_slots = [TimeSlot.from_sqlalchemy(time_slot, model.timezone) for time_slot in model.datetime] - return cls( - id=model.id, - location=model.location, - name=model.title, - office=model.office, - rule_logic=model.rule_logic, - rules=rules, - size=model.size, - time_slots=time_slots, - timezone=model.timezone, - default_auto_opt_in=model.default_auto_opt_in, - ) - - def calculate_meeting_datetime(time_slot: TimeSlot, timezone_str: str) -> datetime: cur_time = arrow.now(timezone_str) result_time = cur_time.replace(hour=time_slot.hour, minute=time_slot.minute, second=0, microsecond=0) @@ -178,7 +79,7 @@ def get_subscriptions(): @subscriptions_blueprint.route("/", methods=["GET"]) def get_subscription(sub_id: int): - sub_model = MeetingSubscription.query.filter(MeetingSubscription.id == sub_id).one() + sub_model = get_subscription_model(sub_id) sub = Subscription.from_sqlalchemy(sub_model) # There is probably a better way to do this, but not sure what it is yet resp = jsonify(json.loads(sub.json())) diff --git a/api/yelp_beans/routes/api/v1/types.py b/api/yelp_beans/routes/api/v1/types.py new file mode 100644 index 00000000..4d7c57f6 --- /dev/null +++ b/api/yelp_beans/routes/api/v1/types.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import enum +from typing import Literal + +import arrow +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import Field +from pydantic import field_validator +from pytz import all_timezones +from pytz import utc + +from yelp_beans.models import MeetingSubscription +from yelp_beans.models import Rule +from yelp_beans.models import SubscriptionDateTime + + +@enum.unique +class Weekday(enum.Enum): + MONDAY = "monday" + TUESDAY = "tuesday" + WEDNESDAY = "wednesday" + THURSDAY = "thursday" + FRIDAY = "friday" + SATURDAY = "saturday" + SUNDAY = "sunday" + + def to_day_number(self) -> int: + day_to_number = { + Weekday.MONDAY: 0, + Weekday.TUESDAY: 1, + Weekday.WEDNESDAY: 2, + Weekday.THURSDAY: 3, + Weekday.FRIDAY: 4, + Weekday.SATURDAY: 5, + Weekday.SUNDAY: 6, + } + return day_to_number[self] + + @staticmethod + def from_day_number(day_number: int) -> Weekday: + for day in Weekday: + if day_number == day.to_day_number(): + return day + raise ValueError(f"No day for day number of {day_number}") + + +class TimeSlot(BaseModel): + day: Weekday + hour: int = Field(ge=0, le=23) + minute: int = Field(0, ge=0, le=59) + model_config = ConfigDict(frozen=True) + + @classmethod + def from_sqlalchemy(cls, model: SubscriptionDateTime, timezone: str) -> RuleModel: + tz_time = arrow.get(model.datetime.replace(tzinfo=utc)).to(timezone) + return cls( + day=Weekday.from_day_number(model.datetime.weekday()), + hour=tz_time.hour, + minute=tz_time.minute, + ) + + +class RuleModel(BaseModel): + field: str + value: str + model_config = ConfigDict(frozen=True) + + @classmethod + def from_sqlalchemy(cls, model: Rule) -> RuleModel: + return cls(field=model.name, value=model.value) + + +class NewSubscription(BaseModel): + location: str = "Online" + name: str = Field(min_length=1) + office: str = "Remote" + rule_logic: Literal["any", "all", None] = None + rules: list[RuleModel] = Field(default_factory=list) + size: int = Field(2, ge=2) + time_slots: list[TimeSlot] = Field(min_length=1) + timezone: str = "America/Los_Angeles" + default_auto_opt_in: bool = False + + @field_validator("timezone") + @classmethod + def is_valid_timezone(cls, value: str) -> str: + if value not in all_timezones: + raise ValueError(f"{value} is not a valid timezone") + return value + + +class Subscription(NewSubscription): + id: int + + @classmethod + def from_sqlalchemy(cls, model: MeetingSubscription) -> Subscription: + rules = [RuleModel.from_sqlalchemy(rule) for rule in model.user_rules] + time_slots = [TimeSlot.from_sqlalchemy(time_slot, model.timezone) for time_slot in model.datetime] + return cls( + id=model.id, + location=model.location, + name=model.title, + office=model.office, + rule_logic=model.rule_logic, + rules=rules, + size=model.size, + time_slots=time_slots, + timezone=model.timezone, + default_auto_opt_in=model.default_auto_opt_in, + ) diff --git a/frontend/containers/Subscribe.js b/frontend/containers/Subscribe.js new file mode 100644 index 00000000..f17b9da5 --- /dev/null +++ b/frontend/containers/Subscribe.js @@ -0,0 +1,104 @@ +import axios from "axios"; +import React from "react"; +import PropTypes from "prop-types"; + +import { formatTimeSlot } from "../lib/datetime"; + +const getSubscriptionId = () => { + const path = window.location.pathname.split("/"); + return path[path.length - 1]; +}; + +function SubscribedMessage({ subscription, timeSlot, newPreference }) { + let msg = "have been"; + if (!newPreference) { + msg = "were already"; + } + return ( +
+

+ You {msg} subscribed to {subscription.name} for{" "} + {formatTimeSlot(timeSlot)} +

+

You will be redirected to the home page in 5 seconds...

+
+ ); +} + +SubscribedMessage.propTypes = { + subscription: PropTypes.shape({ + name: PropTypes.string.isRequired, + timezone: PropTypes.string.isRequired, + }).isRequired, + timeSlot: PropTypes.shape({ + day: PropTypes.string.isRequired, + hour: PropTypes.number.isRequired, + minute: PropTypes.number.isRequired, + }).isRequired, + newPreference: PropTypes.bool.isRequired, +}; + +function ErrorMessage({ error }) { + return
Error subscribing you. {error.msg}
; +} + +ErrorMessage.propTypes = { + error: PropTypes.shape({ + msg: PropTypes.string.isRequired, + }).isRequired, +}; + +function Subscribe() { + const [subscribedSubscription, setSubscribedSubscription] = React.useState(); + const [error, setError] = React.useState(); + + const errorHandler = (err) => { + if (err.response) { + setError(err.response.data); + } else { + setError({ msg: "Unknown error on the backend" }); + } + }; + + React.useEffect(() => { + axios + .get("/email") + .then((resEmail) => { + axios + .post(`/v1/user/preferences/subscribe/${getSubscriptionId()}`, { + email: resEmail.data.email, + }) + .then((resSubscribe) => { + setSubscribedSubscription(resSubscribe.data); + // In 5 seconds redirect to home page + setTimeout(() => window.location.assign("/"), 5000); + }) + .catch(errorHandler); + }) + .catch(errorHandler); + }, []); + + if (!error && !subscribedSubscription) { + return ( +
+
+

Subscribing...

+
+ ); + } + + return ( +
+ {error && } + {subscribedSubscription && ( + + )} +
+ ); +} + +export default Subscribe; diff --git a/frontend/containers/SubscriptionsList.js b/frontend/containers/SubscriptionsList.js index 76e876f8..8d5e9ed8 100644 --- a/frontend/containers/SubscriptionsList.js +++ b/frontend/containers/SubscriptionsList.js @@ -1,26 +1,10 @@ import axios from "axios"; import React from "react"; -const formatWeekday = (weekday) => - weekday.charAt(0).toUpperCase() + weekday.substr(1); - -const formatTime = (hour, minute) => { - const date = new Date(); - date.setHours(hour, minute); - return new Intl.DateTimeFormat(navigator.language, { - hour: "numeric", - minute: "numeric", - }).format(date); -}; +import { formatTimeSlot } from "../lib/datetime"; const formatTimeSlots = (timeSlots) => { - const slotStrings = timeSlots.map( - (timeSlot) => - `${formatWeekday(timeSlot.day)} ${formatTime( - timeSlot.hour, - timeSlot.minute, - )}`, - ); + const slotStrings = timeSlots.map(formatTimeSlot); return slotStrings.join(", "); }; diff --git a/frontend/index.js b/frontend/index.js index 2a59fe10..c29212ea 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -10,6 +10,7 @@ import MeetingRequest from "./containers/MeetingRequest"; import User from "./containers/User"; import SubscriptionsList from "./containers/SubscriptionsList"; import Subscription from "./containers/Subscription"; +import Subscribe from "./containers/Subscribe"; const router = createBrowserRouter([ { @@ -28,6 +29,10 @@ const router = createBrowserRouter([ path: "/meeting_request/:id", element: , }, + { + path: "/subscribe/:id", + element: , + }, { path: "/admin/subscriptions/:id", element: , diff --git a/frontend/lib/datetime.js b/frontend/lib/datetime.js new file mode 100644 index 00000000..fbd9c50b --- /dev/null +++ b/frontend/lib/datetime.js @@ -0,0 +1,17 @@ +export const formatWeekday = (weekday) => + weekday.charAt(0).toUpperCase() + weekday.substr(1); + +export const formatTime = (hour, minute) => { + const date = new Date(); + date.setHours(hour, minute); + return new Intl.DateTimeFormat(navigator.language, { + hour: "numeric", + minute: "numeric", + }).format(date); +}; + +export const formatTimeSlot = (timeSlot) => + `${formatWeekday(timeSlot.day)} ${formatTime( + timeSlot.hour, + timeSlot.minute, + )}`; diff --git a/frontend/webapp.js b/frontend/webapp.js index 2fb12f69..a5bd6cc8 100644 --- a/frontend/webapp.js +++ b/frontend/webapp.js @@ -61,6 +61,10 @@ app.get("/meeting_request/:id", (req, res) => { res.sendFile(`${__dirname}/index.html`); }); +app.get("/subscribe/:id", (req, res) => { + res.sendFile(`${__dirname}/index.html`); +}); + app.get("/admin/subscriptions", (req, res) => { res.sendFile(`${__dirname}/index.html`); });