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

Climbing log #50

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f2d4004
add climbing log routes and pages
demshy Sep 19, 2024
148b8d0
wip: pagination
demshy Oct 1, 2024
6751156
add ascent list table and cards, ascentscontex for determining select…
demshy Oct 9, 2024
710759c
add column selection
demshy Oct 20, 2024
52390ec
add sort to ascent log
demshy Oct 20, 2024
86c9929
some climbing log layout changes, add filter
demshy Nov 2, 2024
e52065f
use headless listbox anchor for select
demshy Nov 2, 2024
38d9e3e
Merge branch 'main' into climbing-log
demshy Nov 2, 2024
c24dd85
fix buttons going over dialog
demshy Nov 2, 2024
f1d48eb
add z-index to date-picker
demshy Nov 2, 2024
14f5712
Merge branch 'headless-ui-patch' into climbing-log
demshy Nov 4, 2024
6b140b4
add route type option to filter
demshy Nov 4, 2024
5ac19b7
Merge branch 'headless-ui-patch' into combobox
demshy Nov 4, 2024
21f14b7
Merge branch 'datepicker-popover' into climbing-log
demshy Nov 16, 2024
f610fd4
log: update breadcrumbs
demshy Nov 16, 2024
b00aced
Merge branch 'main' into combobox
demshy Nov 16, 2024
fca3af7
combobox with sandbox for crag / route finder
demshy Nov 19, 2024
ac3df68
Merge branch 'combobox' into climbing-log
demshy Nov 19, 2024
7a764ca
Merge branch 'datepicker-popover' into climbing-log
demshy Nov 20, 2024
b37ee79
Merge branch 'main' into climbing-log
demshy Nov 24, 2024
75d5ade
climbing log filters - crag, date, add searchParamsHandler hook
demshy Nov 25, 2024
ea3b54f
Merge branch 'datepicker-popover' into climbing-log
demshy Dec 9, 2024
2b38d8c
handle checkbox filters, add filter count to trigger
demshy Dec 26, 2024
bf3e1b8
add filter by route
demshy Dec 26, 2024
cde7e7d
add calendar view (mostly done)
demshy Dec 30, 2024
c17cbac
highlight today
demshy Jan 2, 2025
c4b1d49
add calendar day + actions row
demshy Jan 3, 2025
adc6a33
calendar day view - list (crag) activities
demshy Jan 5, 2025
b209435
fix calendar borders, make days next Links, add today link
demshy Jan 5, 2025
f54c15b
add dot icon for activity types, add activity popup placeholder
demshy Jan 16, 2025
48f116e
Merge branch 'main' into climbing-log
demshy Jan 19, 2025
81f1d3a
move ascents context to lib
demshy Jan 19, 2025
356cac8
finished activity form
demshy Jan 19, 2025
f9f4028
create activity form
demshy Jan 23, 2025
28a0c18
Merge branch 'main' into climbing-log
demshy Jan 27, 2025
af166a9
update generated.ts
demshy Jan 27, 2025
5736521
Merge branch 'main' into climbing-log
demshy Jan 27, 2025
494a72f
lint code
demshy Jan 27, 2025
bede516
handle urls and breadcrumbs
demshy Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import AscentType from "@/components/ascent-type";
import Grade from "@/components/grade";
import Button from "@/components/ui/button";
import { IconSize } from "@/components/ui/icons/icon-size";
import IconMore from "@/components/ui/icons/more";
import { ActivityRoute } from "@/graphql/generated";
import displayDate from "@/lib/display-date";
import { useAscentsContext } from "../../lib/ascents-context";

type TAscentCardProps = {
ascent: ActivityRoute;
};

function AscentCard({ ascent }: TAscentCardProps) {
const { columns } = useAscentsContext();

const showRoute = columns.selectedState.includes("route");
const showCrag = columns.selectedState.includes("crag");
const showDifficulty = columns.selectedState.includes("difficulty");
const showAscentType = columns.selectedState.includes("ascentType");
const showNotes = columns.selectedState.includes("notes");
const showVisibility = columns.selectedState.includes("visibility");

return (
<div className="px-4 py-2 border-b border-b-neutral-200">
<div className="flex flex-row items-center">
<div className="flex-1 text-neutral-500">
{displayDate(ascent.date)}
</div>
<span className="-m-1">
<Button variant="quaternary">
<IconMore size={IconSize.small} />
</Button>
</span>
</div>
{(showRoute || showDifficulty || showAscentType) && (
<div className="flex flex-row items-center gap-4">
{showRoute && (
<a href="#" className="flex-1 font-medium">
{ascent.route.name}
</a>
)}
{showDifficulty && ascent.route.difficulty && (
<Grade difficulty={ascent.route.difficulty} />
)}
{showAscentType && (
<AscentType
type={ascent.ascentType}
compact={true}
iconSize={IconSize.small}
/>
)}
</div>
)}
{showCrag && <div>{ascent.route.crag.name}</div>}
{showVisibility && (
<div className={`${ascent.publish != "public" && "text-neutral-400"}`}>
vidnost:{" "}
{ascent.publish == "public"
? "javno"
: ascent.publish == "club"
? "klub in prijatelji"
: "samo zame"}
</div>
)}
{showNotes && ascent.notes && (
<div className="flex flex-row gap-1">
<div>opombe:</div>
<div>{ascent.notes}</div>
</div>
)}
</div>
);
}

export default AscentCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ActivityRoute } from "@/graphql/generated";
import AscentCard from "./ascent-card";

type TAscentListCardsProps = {
ascents: ActivityRoute[];
};

function AscentListCards({ ascents }: TAscentListCardsProps) {
return (
<div>
{ascents.map((ascent) => (
<AscentCard ascent={ascent} key={ascent.id} />
))}
</div>
);
}

export default AscentListCards;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ActivityRoute } from "@/graphql/generated";
import AscentRow from "./ascent-row";
import { useAscentsContext } from "../../lib/ascents-context";

// Define the props type
type TAscentListTableProps = {
ascents: ActivityRoute[];
};

function AscentListTable({ ascents }: TAscentListTableProps) {
const { columns } = useAscentsContext();

return (
<div className="px-8">
<table className="w-full">
<thead>
<tr className="border-b border-neutral-200 text-left text-neutral-500">
{columns.shown.map((column, index) => {
return (
<th
key={index}
className={`p-4 font-normal first:pl-0 last:pr-0 ${
columns.shown.length > 1 ? "last:text-right" : ""
}`}
style={{
minWidth: `${
(column.width -
(index == 0 ? 16 : 0) -
(index == columns.shown.length - 1 ? 16 : 0)) /
16
}rem`,
}}
>
{column.label}
</th>
);
})}
</tr>
</thead>

<tbody>
{ascents.map((ascent) => (
<AscentRow ascent={ascent} key={ascent.id} />
))}
</tbody>
</table>
</div>
);
}

export default AscentListTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import Pagination from "@/components/ui/pagination";
import { ActivityRoute, PaginationMeta } from "@/graphql/generated";
import { useCallback, useState } from "react";
import useResizeObserver from "@/hooks/useResizeObserver";
import { useAscentsContext } from "../../lib/ascents-context";
import AscentListCards from "./ascent-list-cards";
import AscentListTable from "./ascent-list-table";
import useSearchParamsHandler from "@/hooks/useSearchParamsHandler";

type TAscentListProps = {
ascents: ActivityRoute[];
paginationMeta: PaginationMeta;
};

function AscentList({ ascents, paginationMeta }: TAscentListProps) {
const { columns } = useAscentsContext();
const [compact, setCompact] = useState<boolean | null>(null);

const neededWidth = columns.all
.filter((column) => columns.selectedState.includes(column.name))
.reduce((sum, { width }) => sum + width, -32);

const onResize = useCallback(
(_target: HTMLDivElement, entry: ResizeObserverEntry) => {
const availableWidth = entry.contentRect.width;
setCompact(availableWidth < neededWidth);
},
[neededWidth]
);
const containerRef = useResizeObserver(onResize);

const { updateSearchParams } = useSearchParamsHandler();

function handlePageChange(pageNumber: number) {
updateSearchParams({ page: `${pageNumber}` });
}

const fromIndex =
(paginationMeta.pageNumber - 1) * paginationMeta.pageSize + 1;
const toIndex = Math.min(
paginationMeta.pageNumber * paginationMeta.pageSize,
paginationMeta.itemCount
);

return (
<div ref={containerRef} className="mx-auto 2xl:container">
{compact ? (
<AscentListCards ascents={ascents} />
) : (
<AscentListTable ascents={ascents} />
)}
{paginationMeta.itemCount > 0 && (
<div
className={`${compact ? "p-4 flex flex-col gap-4 items-center" : "flex items-center flex-row-reverse justify-between px-8 py-4"}`}
>
<Pagination
currentPage={paginationMeta.pageNumber}
totalPages={paginationMeta.pageCount}
onPageChange={handlePageChange}
/>
<div>
{fromIndex}-{toIndex} od {paginationMeta.itemCount} vzponov
</div>
</div>
)}
</div>
);
}

export default AscentList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import AscentType from "@/components/ascent-type";
import Grade from "@/components/grade";
import Button from "@/components/ui/button";
import { IconSize } from "@/components/ui/icons/icon-size";
import IconMore from "@/components/ui/icons/more";
import { ActivityRoute } from "@/graphql/generated";
import displayDate from "@/lib/display-date";
import { useAscentsContext } from "../../lib/ascents-context";
import CragLink from "@/components/crag-link";
import RouteLink from "@/components/route-link";

type TAscentRowProps = {
ascent: ActivityRoute;
};

function AscentRow({ ascent }: TAscentRowProps) {
const { columns } = useAscentsContext();

return (
<tr className="border-b border-neutral-200">
{columns.shown.map((column, index) => {
let cellContent;
switch (column.name) {
case "date":
cellContent = (
<span className="whitespace-nowrap">
{displayDate(ascent.date)}
</span>
);
break;

case "crag":
cellContent = <CragLink crag={ascent.route.crag} />;
break;

case "route":
cellContent = <RouteLink route={ascent.route} />;
break;

case "difficulty":
cellContent = ascent.route.difficulty && (
<Grade difficulty={ascent.route.difficulty} />
);
break;

case "ascentType":
cellContent = (
<AscentType
type={ascent.ascentType}
compact={false}
iconSize={IconSize.regular}
/>
);
break;

case "notes":
cellContent = ascent.notes;
break;

case "visibility":
cellContent =
ascent.publish == "public"
? "javno"
: ascent.publish == "club"
? "klub in prijatelji"
: "samo zame";
break;

case "more":
cellContent = (
<Button variant="quaternary">
<span className="-m-1">
<IconMore size={IconSize.regular} />
</span>
</Button>
);
break;
}

return (
<td
key={index}
className={`hyphens-auto break-words p-4 [word-break:break-word] first:pl-0 last:pr-0 align-top ${
columns.shown.length > 1 ? "last:text-right" : ""
}`}
>
{cellContent}
</td>
);
})}
</tr>
);
}

export default AscentRow;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";
import { useState } from "react";
import Filter, { TAscentListFilter } from "./filter";
import SelectColumns from "./select-columns";
import Sort from "./sort";

type TActionsRowProps = {
filterValues: TAscentListFilter;
};

function ActionsRow({ filterValues }: TActionsRowProps) {
const [isFiltersDialogOpen, setIsFiltersDialogOpen] = useState(false);

return (
<>
{/* Actions row */}
{/*
for <sm: all icons, including search are displayed centered.
for >=sm: search icon becomes search text field and sticks right, all other icons stick left
for <md: filter pane is triggered by filter icon
for >=md: filter pane is always visible, filter icon dissapears
*/}

<div className="x-auto mx-auto rotate-0 items-center border-b border-b-neutral-200 px-4 2xl:container xs:px-8 sm:justify-between md:border-b-0 flex justify-center">
<div className="flex items-center justify-center py-4 sm:py-5">
<div>
<Filter
filterValues={filterValues}
isOpen={isFiltersDialogOpen}
setIsOpen={setIsFiltersDialogOpen}
/>
</div>

<div className="ml-3 h-6 border-l border-neutral-300 pr-3"></div>

<SelectColumns />

<div className="ml-3 h-6 border-l border-neutral-300 pr-3"></div>

<Sort />
</div>
</div>
</>
);
}

export default ActionsRow;
Loading
Loading