Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement changing the top event period per user #89

Merged
merged 13 commits into from
Sep 26, 2024
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
Loading