@@ -2,9 +2,17 @@ import { ChevronRightIcon } from "@radix-ui/react-icons";
2
2
import { cookies } from "next/headers" ;
3
3
import Link from "next/link" ;
4
4
import { useTranslations } from "next-intl" ;
5
+ import { getTranslations } from "next-intl/server" ;
5
6
6
7
import { EnduranceIcon , SpeedlineIcon } from "@/assets" ;
7
8
import { Button } from "@/components/ui/button" ;
9
+ import {
10
+ Card ,
11
+ CardContent ,
12
+ CardDescription ,
13
+ CardHeader ,
14
+ CardTitle ,
15
+ } from "@/components/ui/card" ;
8
16
import {
9
17
Popover ,
10
18
PopoverContent ,
@@ -15,103 +23,140 @@ import type { Database } from "@/utils/supabase/database.types";
15
23
import { useSupabaseServer } from "@/utils/supabase/server" ;
16
24
17
25
import FormattedDate from "./FormattedDate" ;
26
+ import { Heatmap , YearSwitcher } from "./WalkActivityCalendar" ;
18
27
19
28
export const dynamic = "force-dynamic" ;
29
+ type Entry =
30
+ | Database [ "public" ] [ "Tables" ] [ "entry" ] [ "Row" ] & {
31
+ highline : Database [ "public" ] [ "Tables" ] [ "highline" ] [ "Row" ] | null ;
32
+ } ;
20
33
21
34
interface Props {
22
35
username : string ;
36
+ year ?: string ;
23
37
}
24
38
25
- export default async function LastWalks ( { username } : Props ) {
39
+ export default async function LastWalks ( { username, year } : Props ) {
26
40
const cookieStore = cookies ( ) ;
27
41
const supabase = useSupabaseServer ( cookieStore ) ;
42
+ const t = await getTranslations ( "profile.lastWalks" ) ;
28
43
29
44
const { data : entries } = await supabase
30
45
. from ( "entry" )
31
46
. select (
32
47
`
33
- *,
34
- highline (*)
35
- `
48
+ *,
49
+ highline (*)
50
+ `
36
51
)
37
52
. eq ( "instagram" , `@${ username } ` )
38
- . limit ( 5 )
39
53
. order ( "created_at" , { ascending : false } ) ;
40
54
41
- return < LastWalksContent entries = { entries || [ ] } /> ;
55
+ if ( ! entries ) return null ;
56
+
57
+ function groupByDay ( entries : Entry [ ] ) {
58
+ const groupedByDay : Record < string , number > = { } ;
59
+ const years : Array < string > = [ ] ;
60
+
61
+ entries . forEach ( ( entry ) => {
62
+ const day = entry . created_at . split ( "T" ) [ 0 ] ;
63
+ const year = day . split ( "-" ) [ 0 ] ;
64
+
65
+ groupedByDay [ day ] = groupedByDay [ day ] + 1 || 1 ;
66
+ if ( years [ years . length - 1 ] !== year ) {
67
+ years . push ( year ) ;
68
+ }
69
+ } ) ;
70
+
71
+ return {
72
+ years : years ,
73
+ data : groupedByDay ,
74
+ } ;
75
+ }
76
+
77
+ const { data, years } = groupByDay ( entries ) ;
78
+ const selectedYear = year && years . includes ( year ) ? year : years [ 0 ] ;
79
+
80
+ return (
81
+ < Card >
82
+ < CardContent >
83
+ < CardHeader className = "flex-row justify-between space-y-0 p-0 pt-6" >
84
+ < div className = "flex-1" >
85
+ < CardTitle > { t ( "title" ) } </ CardTitle >
86
+ < CardDescription > { t ( "description" ) } </ CardDescription >
87
+ </ div >
88
+ < YearSwitcher years = { years } selectedYear = { selectedYear } />
89
+ </ CardHeader >
90
+
91
+ < Heatmap year = { parseInt ( selectedYear ) } data = { data } />
92
+ < LastWalksContent entries = { entries . slice ( 0 , 5 ) } />
93
+ </ CardContent >
94
+ </ Card >
95
+ ) ;
42
96
}
43
97
44
98
interface ContentProps {
45
- entries :
46
- | ( Database [ "public" ] [ "Tables" ] [ "entry" ] [ "Row" ] & {
47
- highline : Database [ "public" ] [ "Tables" ] [ "highline" ] [ "Row" ] | null ;
48
- } ) [ ] ;
99
+ entries : Entry [ ] ;
49
100
}
50
101
function LastWalksContent ( { entries } : ContentProps ) {
51
102
const t = useTranslations ( "profile.lastWalks" ) ;
52
103
53
104
return (
54
- < div className = "max-w-screen-md rounded-xl border border-gray-200 bg-white px-2 py-4 shadow dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800" >
55
- < h3 className = "text-xl font-semibold dark:text-white" > { t ( "title" ) } </ h3 >
56
- < ul className = "mt-4 divide-y divide-gray-200 dark:divide-gray-700" >
57
- { entries . length > 0 ? (
58
- entries . map ( ( entry ) => (
59
- < li
60
- key = { entry . id }
61
- className = "overflow-hidden py-4 first:pt-0 last:pb-0"
62
- >
63
- < Popover >
64
- < PopoverTrigger className = "mb-0.5 truncate text-base font-semibold leading-none text-blue-500 dark:text-blue-400" >
65
- { entry . highline ?. name }
66
- </ PopoverTrigger >
67
- < PopoverContent
68
- side = "top"
69
- className = "max-h-96 overflow-auto p-0"
70
- >
71
- < div className = "space-y-2 p-4" >
72
- < p > { entry . highline ?. description } </ p >
73
- < div >
74
- < p >
75
- { t ( "popover.height" ) } : { entry . highline ?. height } m
76
- </ p >
77
- < p >
78
- { t ( "popover.length" ) } : { entry . highline ?. lenght } m
79
- </ p >
80
- </ div >
81
- < Button className = "w-full" variant = { "outline" } asChild >
82
- < Link href = { `/${ entry . highline ?. id } ` } >
83
- { t ( "popover.buttonLabel" ) } { " " }
84
- < ChevronRightIcon
85
- className = "ml-1.5 h-3 w-3"
86
- strokeWidth = { 2 }
87
- stroke = "currentColor"
88
- />
89
- </ Link >
90
- </ Button >
91
- </ div >
92
- </ PopoverContent >
93
- </ Popover >
94
- < FormattedDate date = { new Date ( entry . created_at ) } />
95
- < div className = "text-white-700 mb-1 flex gap-4 truncate text-sm font-normal dark:text-white" >
96
- { entry . crossing_time && (
97
- < div className = "flex items-end gap-1" >
98
- < SpeedlineIcon className = "text-gray-900 dark:text-white" />
99
- < p > { transformSecondsToTimeString ( entry . crossing_time ) } </ p >
105
+ < ul className = "mt-4 divide-y divide-gray-200 dark:divide-gray-700" >
106
+ { entries . length > 0 ? (
107
+ entries . map ( ( entry ) => (
108
+ < li
109
+ key = { entry . id }
110
+ className = "overflow-hidden py-4 first:pt-0 last:pb-0"
111
+ >
112
+ < Popover >
113
+ < PopoverTrigger className = "mb-0.5 truncate text-base font-semibold leading-none text-blue-500 dark:text-blue-400" >
114
+ { entry . highline ?. name }
115
+ </ PopoverTrigger >
116
+ < PopoverContent side = "top" className = "max-h-96 overflow-auto p-0" >
117
+ < div className = "space-y-2 p-4" >
118
+ < p > { entry . highline ?. description } </ p >
119
+ < div >
120
+ < p >
121
+ { t ( "popover.height" ) } : { entry . highline ?. height } m
122
+ </ p >
123
+ < p >
124
+ { t ( "popover.length" ) } : { entry . highline ?. lenght } m
125
+ </ p >
100
126
</ div >
101
- ) }
127
+ < Button className = "w-full" variant = { "outline" } asChild >
128
+ < Link href = { `/${ entry . highline ?. id } ` } >
129
+ { t ( "popover.buttonLabel" ) } { " " }
130
+ < ChevronRightIcon
131
+ className = "ml-1.5 h-3 w-3"
132
+ strokeWidth = { 2 }
133
+ stroke = "currentColor"
134
+ />
135
+ </ Link >
136
+ </ Button >
137
+ </ div >
138
+ </ PopoverContent >
139
+ </ Popover >
140
+ < FormattedDate date = { new Date ( entry . created_at ) } />
141
+ < div className = "text-white-700 mb-1 flex gap-4 truncate text-sm font-normal dark:text-white" >
142
+ { entry . crossing_time && (
102
143
< div className = "flex items-end gap-1" >
103
- < EnduranceIcon className = "text-gray-900 dark:text-white" />
104
- < p > { entry . distance_walked } m </ p >
144
+ < SpeedlineIcon className = "text-gray-900 dark:text-white" />
145
+ < p > { transformSecondsToTimeString ( entry . crossing_time ) } </ p >
105
146
</ div >
147
+ ) }
148
+ < div className = "flex items-end gap-1" >
149
+ < EnduranceIcon className = "text-gray-900 dark:text-white" />
150
+ < p > { entry . distance_walked } m</ p >
106
151
</ div >
107
- < p > { entry . comment } </ p >
108
- </ li >
109
- ) )
110
- ) : (
111
- < p > { t ( "empty" ) } </ p >
112
- ) }
113
- </ ul >
114
- </ div >
152
+ </ div >
153
+ < p > { entry . comment } </ p >
154
+ </ li >
155
+ ) )
156
+ ) : (
157
+ < p > { t ( "empty" ) } </ p >
158
+ ) }
159
+ </ ul >
115
160
) ;
116
161
}
117
162
0 commit comments