Skip to content

Commit

Permalink
Rework sections: add ordering, support hierarchy (#5)
Browse files Browse the repository at this point in the history
- Make sections hierarchical
- Change auth token lifetime
- Implement moving subsections
- Introduce the root section concept for development convenience
- Improve paddings for section hierarchy in frontend
- Add skeleton for sections hierarchy in frontend
- Replace the AI-generated logo with one created by a professional
- Set up favicon
- Add 12 new tests
  • Loading branch information
m-danya authored Nov 2, 2024
1 parent fd96555 commit 8e88cc0
Show file tree
Hide file tree
Showing 45 changed files with 723 additions and 189 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Planty

<p align="center">
<img src="./imgs/planty.png" height="200px">
<img src="./imgs/logo_vert.png" height="200px">
</p>

> "I have <i>plenty</i> things to do!"
Expand Down
21 changes: 21 additions & 0 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import Head from "next/head";
import "./globals.css";

export const metadata: Metadata = {
Expand All @@ -13,6 +14,26 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
</Head>
<body>{children}</body>
</html>
);
Expand Down
50 changes: 0 additions & 50 deletions frontend/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,56 +60,6 @@ const data = {
icon: () => <SimpleIcon icon={siGithub} />,
},
],
favorites: [
{
name: "Inbox",
url: "#",
emoji: "📩",
},
{
name: "Current tasks",
url: "#",
emoji: "📝",
},
{
name: "Projects",
url: "#",
emoji: "🎯",
},
{
name: "Sometime later",
url: "#",
emoji: "📝",
children: [
{
name: "Duties",
url: "#",
emoji: "💼",
},
{
name: "Programming",
url: "#",
emoji: "💻",
},
{
name: "Music",
url: "#",
emoji: "🎸",
},
{
name: "Would be great to do",
url: "#",
emoji: "🦄",
},
],
},
{
name: "Waiting for others",
url: "#",
emoji: "⌛",
},
],
workspaces: [],
};

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Image from "next/image";
export function Logo() {
return (
<div className="flex justify-center w-full">
<div className="w-32">
<div className="w-40">
<Image
src="/logo.png"
src="/logo_hor.png"
className="w-full h-auto "
width={200}
height={200}
Expand Down
158 changes: 133 additions & 25 deletions frontend/components/nav-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,58 +15,67 @@ import {
SidebarMenuItem,
SidebarMenuSub,
} from "@/components/ui/sidebar";
import { Skeleton } from "@/components/ui/skeleton";
import React from "react";

import { useSections } from "@/hooks/use-sections";

export function NavSections() {
const { sections, isLoading, isError } = useSections();
if (isLoading) return <p>Loading sections...</p>;
const { sections, rootSectionId, isLoading, isError } = useSections();

if (isError) return <p>Failed to load sections.</p>;
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroup className="group-data-[collapsible=icon]:hidden text-nowrap">
<SidebarGroupLabel>Sections</SidebarGroupLabel>
<SidebarMenu>
{sections.map((item) => (
<Tree key={item.title} item={item} />
))}
{isLoading ? (
<SectionsSkeleton />
) : (
sections.map((item) => <Tree key={item.title} item={item} />)
)}
</SidebarMenu>
</SidebarGroup>
);
}

function Tree({ item }) {
const hasChildren = item.children && item.children.length > 0;

function Tree({ item, noIndent = false }) {
const hasChildren = item.subsections && item.subsections.length > 0;
if (!hasChildren) {
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<TreeElement item={item} />
<SidebarMenuButton>
<TreeElement
item={item}
withChevron={false}
clickable={true}
withIdentIfNoChevron={!noIndent}
/>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
const anyChildHasChildren = item.subsections.some(
(child) => child.subsections.length > 0
);

return (
<SidebarMenuItem key={item.title}>
<Collapsible
className="group/collapsible [&[data-state=open]>div>button>svg:first-child]:rotate-90"
defaultOpen={true}
>
<Collapsible className="group" defaultOpen={true}>
<div className="flex items-center">
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<ChevronRight className="transition-transform" />
<span>{item.emoji}</span>
<span>{item.title}</span>
<TreeElement item={item} withChevron={true} clickable={false} />
</SidebarMenuButton>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<SidebarMenuSub className="w-fit">
{item.children.map((child) => (
<Tree key={child.title} item={child} />
{item.subsections.map((child) => (
<Tree
key={child.title}
item={child}
noIndent={!anyChildHasChildren}
/>
))}
</SidebarMenuSub>
</CollapsibleContent>
Expand All @@ -75,8 +84,107 @@ function Tree({ item }) {
);
}

const TreeElement = React.forwardRef(({ item, ...props }, ref) => (
<a ref={ref} href={`/section/${item.id}`} title={item.title} {...props}>
<span>{item.title}</span>
</a>
));
const TreeElement = React.forwardRef<
HTMLAnchorElement,
{
item: { id: string; title: string };
withChevron?: boolean;
clickable?: boolean;
withIdentIfNoChevron?: boolean;
} & React.HTMLAttributes<HTMLAnchorElement>
>(
(
{
item,
clickable = true,
withChevron = false,
withIdentIfNoChevron = true,
...props
},
ref
) => {
const displayFullIdent = !withChevron && withIdentIfNoChevron;
const displayMiniIdent = !withChevron && !withIdentIfNoChevron;
const mainContent = (
<div className="flex items-center">
{displayFullIdent && <div className="w-5 flex justify-left" />}
{displayMiniIdent && <div className="w-2.5 flex justify-left" />}
{withChevron && (
<div className="w-5 flex justify-left">
<ChevronRight className="transition-transform w-4 group-data-[state=open]:rotate-90" />
</div>
)}
<div className="flex-grow ">
<span>{item.title}</span>
</div>
</div>
);
if (!clickable) {
return mainContent;
} else {
return (
<a
ref={ref}
href={`/section/${item.id}`}
title={item.title}
{...props}
className=""
>
{mainContent}
</a>
);
}
}
);

function SectionsSkeleton() {
return (
<div className="space-y-4 space-x-4 px-5">
<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[120px]" />
</div>

<div className="space-y-2">
<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[120px]" />
</div>
<div className="ml-8 space-y-2">
<Skeleton className="h-4 w-[80px]" />
<Skeleton className="h-4 w-[80px]" />
</div>
</div>

<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[120px]" />
</div>

<div className="space-y-2">
<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[120px]" />
</div>
<div className="ml-8 space-y-2">
<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[80px]" />
</div>
<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[80px]" />
</div>
<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[80px]" />
</div>
<div className="flex items-center space-x-4">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-[150px]" />
</div>
</div>
</div>
</div>
);
}
5 changes: 3 additions & 2 deletions frontend/hooks/use-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { fetcher } from "@/hooks/fetcher";

export const useSections = () => {
const { data, error, isLoading } = useSWR("/api/sections", fetcher);

let rootSectionId = data?.[0]?.id;
return {
sections: data,
sections: data?.[0]?.subsections,
rootSectionId: data?.[0].id,
isLoading,
isError: error,
};
Expand Down
Binary file added frontend/public/android-chrome-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/android-chrome-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/favicon.ico
Binary file not shown.
Binary file removed frontend/public/logo.png
Binary file not shown.
Binary file added frontend/public/logo_hor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
Binary file added imgs/logo_hor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_hor_black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_hor_black_stroke.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_hor_stroke.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_hor_white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_vert.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_vert_black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_vert_black_stroke.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_vert_stroke.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/logo_vert_white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed imgs/planty.png
Binary file not shown.
Binary file removed imgs/planty_small.png
Binary file not shown.
Binary file added imgs/sign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/sign_black_stroke.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/sign_stroke.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/sign_white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 11 additions & 2 deletions planty/application/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
from planty.infrastructure.database import get_async_session
from planty.infrastructure.models import AccessTokenModel, UserModel

# Cookie/token lifetime is 10 years. This is intentional to avoid relogin
# torture when the token expires. Token can be invalidated in case of
# passord/cookie leakage, cause it's stored in the db.
#
# (TODO: consider using refresh tokens when they will be supported by
# fastapi-users: https://github.com/fastapi-users/fastapi-users/discussions/350
# and set lifetime to a shorter period without requiring user to relogin)
TOKEN_LIFETIME = 60 * 60 * 24 * 365 * 10


async def get_user_db(
session: AsyncSession = Depends(get_async_session),
Expand All @@ -40,10 +49,10 @@ def get_database_strategy(
get_access_token_db
),
) -> DatabaseStrategy[Any, Any, Any]:
return DatabaseStrategy(access_token_db, lifetime_seconds=3600)
return DatabaseStrategy(access_token_db, lifetime_seconds=TOKEN_LIFETIME)


cookie_transport = CookieTransport(cookie_max_age=3600)
cookie_transport = CookieTransport(cookie_max_age=TOKEN_LIFETIME)


cookie_auth_backend = AuthenticationBackend(
Expand Down
11 changes: 11 additions & 0 deletions planty/application/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
AttachmentUploadInfo,
SectionCreateRequest,
SectionCreateResponse,
SectionMoveRequest,
SectionResponse,
ShuffleSectionRequest,
TaskCreateRequest,
Expand Down Expand Up @@ -191,6 +192,16 @@ async def get_section(
return section


@router.post("/section/move")
async def move_section(
request: SectionMoveRequest, user: User = Depends(current_user)
) -> None:
async with SqlAlchemyUnitOfWork() as uow:
section_service = SectionService(uow=uow)
await section_service.move_section(user.id, request)
await uow.commit()


@router.get("/sections")
async def get_sections(user: User = Depends(current_user)) -> SectionsListResponse:
async with SqlAlchemyUnitOfWork() as uow:
Expand Down
Loading

0 comments on commit 8e88cc0

Please sign in to comment.