Skip to content

Commit

Permalink
Merge pull request #89 from cs3216-a3-group-4/haoyang/top-events-period
Browse files Browse the repository at this point in the history
Implement changing the top event period per user
  • Loading branch information
chloeelim authored Sep 26, 2024
2 parents cdd297e + 2ca8f85 commit 704cc3c
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""add_top_events_period_to_user
Revision ID: 7f60f0211af1
Revises: feeb1c78c0a2
Create Date: 2024-09-26 21:57:33.021370
"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "7f60f0211af1"
down_revision: Union[str, None] = "feeb1c78c0a2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"user",
sa.Column(
"top_events_period", sa.Integer(), nullable=False, server_default="7"
),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("user", "top_events_period")
# ### end Alembic commands ###
3 changes: 2 additions & 1 deletion backend/src/auth/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from sqlalchemy import Column, ForeignKey, Table
from sqlalchemy import Column, ForeignKey, Integer, Table
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.common.base import Base
from src.events.models import Category
Expand Down Expand Up @@ -29,6 +29,7 @@ class User(Base):

categories: Mapped[list[Category]] = relationship(secondary=user_category_table)
notes: Mapped[list[Note]] = relationship("Note", backref="user")
top_events_period: Mapped[int] = mapped_column(Integer, default=7)


class PasswordReset(Base):
Expand Down
1 change: 1 addition & 0 deletions backend/src/auth/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class UserPublic(BaseModel):
email: EmailStr

categories: list[CategoryDTO]
top_events_period: int = 7


class Token(BaseModel):
Expand Down
20 changes: 12 additions & 8 deletions backend/src/profile/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ def update_profile(
user: Annotated[User, Depends(get_current_user)],
session=Depends(get_session),
) -> UserPublic:
categories = session.scalars(
select(Category).where(Category.id.in_(data.category_ids))
).all()

user = session.get(User, user.id)
user.categories = categories

session.add(user)
session.commit()
session.refresh(user)
if data.category_ids:
categories = session.scalars(
select(Category).where(Category.id.in_(data.category_ids))
).all()
user.categories = categories
if data.top_events_period:
user.top_events_period = data.top_events_period

if data.category_ids or data.top_events_period:
session.add(user)
session.commit()
session.refresh(user)

return user
3 changes: 2 additions & 1 deletion backend/src/profile/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@


class ProfileUpdate(BaseModel):
category_ids: list[int]
category_ids: list[int] = None
top_events_period: int = 7
113 changes: 98 additions & 15 deletions frontend/app/home.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,65 @@
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { ChevronDown } from "lucide-react";

import { getEventsEventsGet, MiniEventDTO } from "@/client";
import ScrollToTopButton from "@/components/navigation/scroll-to-top-button";
import ArticleLoading from "@/components/news/article-loading";
import NewsArticle from "@/components/news/news-article";
import { useUpdateTopEventsPeriod } from "@/queries/user";
import { useUserStore } from "@/store/user/user-store-provider";
import { parseDate } from "@/utils/date";

const enum Period {
Day = 1,
Week = 7,
Month = 30,
}

const getDisplayValueFor = (period: Period) => {
switch (period) {
case Period.Day:
return "past day";
case Period.Month:
return "month";
case Period.Week:
default:
return "week";
}
};

const NUM_TOP_EVENTS = 10;
const DAYS_PER_WEEK = 7;
const DEFAULT_EVENT_PERIOD = Period.Week;

/* This component should only be rendered to authenticated users */
const Home = () => {
const eventStartDate = useMemo(() => {
const date = new Date();
date.setDate(date.getDate() - DAYS_PER_WEEK);
return date;
}, []);

const [topEvents, setTopEvents] = useState<MiniEventDTO[]>([]);
const [isLoaded, setIsLoaded] = useState<boolean>(false);
const user = useUserStore((state) => state.user);
const [showDropdown, setShowDropdown] = useState<boolean>(false);
const [selectedPeriod, setSelectedPeriod] =
useState<Period>(DEFAULT_EVENT_PERIOD);

const user = useUserStore((state) => state.user);
const updateTopEventsMutation = useUpdateTopEventsPeriod();
const router = useRouter();
if (!user!.categories.length) {
router.push("/onboarding");
}

const eventPeriod = useMemo(
() =>
user?.top_events_period ? user.top_events_period : DEFAULT_EVENT_PERIOD,
[user?.top_events_period],
);

const eventStartDate = useMemo(() => {
const eventStartDate = new Date();
eventStartDate.setDate(eventStartDate.getDate() - eventPeriod);
return eventStartDate;
}, [eventPeriod]);

useEffect(() => setSelectedPeriod(eventPeriod), [eventPeriod]);

useEffect(() => {
const fetchTopEvents = async () => {
setIsLoaded(false);
const formattedEventStartDate = eventStartDate
.toISOString()
.split("T")[0];
Expand All @@ -51,8 +81,8 @@ const Home = () => {
},
};
}
const response = await getEventsEventsGet(eventQuery);

const response = await getEventsEventsGet(eventQuery);
if (response.error) {
console.log(response.error);
} else {
Expand All @@ -61,9 +91,25 @@ const Home = () => {
}
};

if (user?.top_events_period) setSelectedPeriod(user.top_events_period);
fetchTopEvents();
}, [user, eventStartDate]);

// Handle the option selection and close dropdown
const handleSelection = (period: Period) => {
if (period != selectedPeriod) {
// Update the text
setSelectedPeriod(period);
updateTopEventsMutation.mutate({ timePeriod: period });
}
// Close dropdown
setShowDropdown(false);
};

if (!user!.categories.length) {
router.push("/onboarding");
}

return (
<div className="relative w-full h-full">
<div
Expand All @@ -76,9 +122,46 @@ const Home = () => {
className="flex flex-col mb-4 gap-y-2 px-4 md:px-8 xl:px-12"
id="homePage"
>
<h1 className="text-4xl 2xl:text-4xl font-bold text-primary-800">
What happened this week
</h1>
<div className="flex">
<span className="text-4xl 2xl:text-4xl font-bold text-primary-800">
What happened this
</span>
<div className="relative">
<button
className="flex items-center space-x-2 text-3xl 2xl:text-4xl font-bold hover:underline"
onClick={() => setShowDropdown(!showDropdown)}
>
<span className="text-4xl 2xl:text-4xl font-bold text-primary-800">
&nbsp;{getDisplayValueFor(selectedPeriod)}
</span>
<ChevronDown className="w-4 h-4" />
</button>
{showDropdown && (
<div className="absolute mt-2 w-40 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
<div className="py-1">
<button
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => handleSelection(Period.Day)}
>
past day
</button>
<button
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => handleSelection(Period.Week)}
>
week
</button>
<button
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => handleSelection(Period.Month)}
>
month
</button>
</div>
</div>
)}
</div>
</div>
<span className="text-primary text-lg">
{parseDate(eventStartDate)} - {parseDate(new Date())}
</span>
Expand Down
11 changes: 10 additions & 1 deletion frontend/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,9 +576,13 @@ export const ProfileUpdateSchema = {
type: "array",
title: "Category Ids",
},
top_events_period: {
type: "integer",
title: "Top Events Period",
default: 7,
},
},
type: "object",
required: ["category_ids"],
title: "ProfileUpdate",
} as const;

Expand Down Expand Up @@ -655,6 +659,11 @@ export const UserPublicSchema = {
type: "array",
title: "Categories",
},
top_events_period: {
type: "integer",
title: "Top Events Period",
default: 7,
},
},
type: "object",
required: ["id", "email", "categories"],
Expand Down
4 changes: 3 additions & 1 deletion frontend/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ export type PointMiniDTO = {
};

export type ProfileUpdate = {
category_ids: Array<number>;
category_ids?: Array<number>;
top_events_period?: number;
};

export type ReadDTO = {
Expand All @@ -162,6 +163,7 @@ export type UserPublic = {
id: number;
email: string;
categories: Array<CategoryDTO>;
top_events_period?: number;
};

export type UserQuestionMiniDTO = {
Expand Down
16 changes: 16 additions & 0 deletions frontend/queries/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ export const useUpdateProfile = () => {
});
};

export const useUpdateTopEventsPeriod = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ timePeriod }: { timePeriod: number }) => {
return updateProfileProfilePut({
body: {
top_events_period: timePeriod,
},
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QueryKeys.UserProfile] });
},
});
};

export const useChangePassword = () => {
return useMutation({
mutationFn: ({
Expand Down

0 comments on commit 704cc3c

Please sign in to comment.