Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
HFT-informatics-weekend
SI - Time Series Map Viewer
Commits
048d0ef1
Commit
048d0ef1
authored
Oct 12, 2025
by
Heisenberg5124
Browse files
Initial commit
parent
36353266
Changes
9
Expand all
Show whitespace changes
Inline
Side-by-side
frontend/package-lock.json
View file @
048d0ef1
This diff is collapsed.
Click to expand it.
frontend/package.json
View file @
048d0ef1
...
@@ -12,7 +12,9 @@
...
@@ -12,7 +12,9 @@
},
},
"dependencies"
:
{
"dependencies"
:
{
"@napi-rs/canvas"
:
"^0.1.80"
,
"@napi-rs/canvas"
:
"^0.1.80"
,
"@radix-ui/react-dialog"
:
"^1.1.15"
,
"@radix-ui/react-label"
:
"^2.1.7"
,
"@radix-ui/react-label"
:
"^2.1.7"
,
"@radix-ui/react-progress"
:
"^1.1.7"
,
"@radix-ui/react-radio-group"
:
"^1.3.8"
,
"@radix-ui/react-radio-group"
:
"^1.3.8"
,
"@radix-ui/react-select"
:
"^2.2.6"
,
"@radix-ui/react-select"
:
"^2.2.6"
,
"@radix-ui/react-slider"
:
"^1.3.6"
,
"@radix-ui/react-slider"
:
"^1.3.6"
,
...
@@ -31,6 +33,7 @@
...
@@ -31,6 +33,7 @@
"react"
:
"^19.1.1"
,
"react"
:
"^19.1.1"
,
"react-dom"
:
"^19.1.1"
,
"react-dom"
:
"^19.1.1"
,
"react-map-gl"
:
"^8.1.0"
,
"react-map-gl"
:
"^8.1.0"
,
"recharts"
:
"^3.2.1"
,
"tailwind-merge"
:
"^3.3.1"
,
"tailwind-merge"
:
"^3.3.1"
,
"tailwindcss"
:
"^4.1.14"
"tailwindcss"
:
"^4.1.14"
},
},
...
...
frontend/src/App.tsx
View file @
048d0ef1
...
@@ -4,9 +4,31 @@ import {useState} from "react";
...
@@ -4,9 +4,31 @@ import {useState} from "react";
import
{
SingleMap
}
from
"
@/components/map/single-map.tsx
"
;
import
{
SingleMap
}
from
"
@/components/map/single-map.tsx
"
;
import
{
SplitMap
}
from
"
@/components/map/split-map.tsx
"
;
import
{
SplitMap
}
from
"
@/components/map/split-map.tsx
"
;
import
{
SplitMapCard
}
from
"
@/components/map/split-map-card.tsx
"
;
import
{
SplitMapCard
}
from
"
@/components/map/split-map-card.tsx
"
;
import
KpiDialog
from
"
@/components/map/kpi-dialog.tsx
"
;
type
Mode
=
'
single
'
|
'
split
'
;
type
Mode
=
'
single
'
|
'
split
'
;
const
kpiMock
=
{
left
:
{
year
:
2020
,
median
:
0.22810404002666473
,
p95
:
0.6100596785545349
,
frac_gt_0_4
:
0.18748562226823096
,
frac_gt_0_6
:
0.05244996549344375
,
},
right
:
{
year
:
2025
,
median
:
0.15449757874011993
,
p95
:
0.4455701410770416
,
frac_gt_0_4
:
0.07292385553255118
,
frac_gt_0_6
:
0.0006901311249137336
,
},
change
:
{
fracLoss
:
0.3400046008741661
,
// Δ ≤ -0.1
fracGain
:
0.006211180124223602
,
// Δ ≥ +0.1
},
}
as
const
;
function
App
()
{
function
App
()
{
const
MIN_YEAR
=
2020
;
const
MIN_YEAR
=
2020
;
const
MAX_YEAR
=
2025
;
const
MAX_YEAR
=
2025
;
...
@@ -19,6 +41,7 @@ function App() {
...
@@ -19,6 +41,7 @@ function App() {
<
div
className
=
"absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3"
>
<
div
className
=
"absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3"
>
<
MapLayerCard
/>
<
MapLayerCard
/>
<
SplitMapCard
mode
=
{
mode
}
setMode
=
{
setMode
}
/>
<
SplitMapCard
mode
=
{
mode
}
setMode
=
{
setMode
}
/>
{
mode
===
'
split
'
&&
(<
KpiDialog
{
...
kpiMock
}
/>)
}
</
div
>
</
div
>
<
div
className
=
"h-full w-full"
>
<
div
className
=
"h-full w-full"
>
...
...
frontend/src/api/__generated__/index.ts
View file @
048d0ef1
...
@@ -7,7 +7,7 @@ An API that returns a satellite image file for a given year and computes NDVI &
...
@@ -7,7 +7,7 @@ An API that returns a satellite image file for a given year and computes NDVI &
- Swagger UI: `/docs`
- Swagger UI: `/docs`
- ReDoc: `/redoc`
- ReDoc: `/redoc`
* OpenAPI spec version: 1.
3
.0
* OpenAPI spec version: 1.
4
.0
*/
*/
import
{
import
{
useMutation
,
useMutation
,
...
@@ -192,6 +192,31 @@ clip_to_aoi?: boolean;
...
@@ -192,6 +192,31 @@ clip_to_aoi?: boolean;
compare_res_m
?:
number
;
compare_res_m
?:
number
;
};
};
export
type
NdviChangeValueNdviChangeValueGetParams
=
{
/**
* @minimum 2013
*/
start_year
:
number
;
/**
* @minimum 2013
*/
end_year
:
number
;
/**
* NDVI summary to compare: mean | median | p05 | p95
*/
metric
?:
string
;
/**
* @minimum 0
* @maximum 100
*/
max_cloud
?:
number
;
/**
* Optional AOI buffer (m)
* @minimum 0
*/
buffer_meters
?:
number
;
};
/**
/**
* Returns a GeoTIFF image file for the requested `year`.
* Returns a GeoTIFF image file for the requested `year`.
By default returns the best (least cloudy) June–August image intersecting your AOI.
By default returns the best (least cloudy) June–August image intersecting your AOI.
...
@@ -534,3 +559,96 @@ export function useNdviChangeNdviChangeGet<TData = Awaited<ReturnType<typeof ndv
...
@@ -534,3 +559,96 @@ export function useNdviChangeNdviChangeGet<TData = Awaited<ReturnType<typeof ndv
return
query
;
return
query
;
}
}
/**
* @summary Fast NDVI change value (in-memory, no file writes)
*/
export
const
ndviChangeValueNdviChangeValueGet
=
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
signal
?:
AbortSignal
)
=>
{
return
apiFetch
<
unknown
>
(
{
url
:
`/ndvi-change/value`
,
method
:
'
GET
'
,
params
,
signal
},
);
}
export
const
getNdviChangeValueNdviChangeValueGetQueryKey
=
(
params
?:
NdviChangeValueNdviChangeValueGetParams
,)
=>
{
return
[
`/ndvi-change/value`
,
...(
params
?
[
params
]:
[])
]
as
const
;
}
export
const
getNdviChangeValueNdviChangeValueGetQueryOptions
=
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
,
}
)
=>
{
const
{
query
:
queryOptions
}
=
options
??
{};
const
queryKey
=
queryOptions
?.
queryKey
??
getNdviChangeValueNdviChangeValueGetQueryKey
(
params
);
const
queryFn
:
QueryFunction
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>>
=
({
signal
})
=>
ndviChangeValueNdviChangeValueGet
(
params
,
signal
);
return
{
queryKey
,
queryFn
,
...
queryOptions
}
as
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
}
export
type
NdviChangeValueNdviChangeValueGetQueryResult
=
NonNullable
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>>
export
type
NdviChangeValueNdviChangeValueGetQueryError
=
HTTPValidationError
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
:
{
query
:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
&
Pick
<
DefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
>
,
'
initialData
'
>
,
}
,
queryClient
?:
QueryClient
):
DefinedUseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
&
Pick
<
UndefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
>
,
'
initialData
'
>
,
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
,
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
/**
* @summary Fast NDVI change value (in-memory, no file writes)
*/
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
,
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
{
const
queryOptions
=
getNdviChangeValueNdviChangeValueGetQueryOptions
(
params
,
options
)
const
query
=
useQuery
(
queryOptions
,
queryClient
)
as
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
};
query
.
queryKey
=
queryOptions
.
queryKey
;
return
query
;
}
frontend/src/components/map/kpi-dialog.tsx
0 → 100644
View file @
048d0ef1
import
{
Dialog
,
DialogContent
,
DialogHeader
,
DialogTitle
,
DialogTrigger
,
}
from
"
@/components/ui/dialog
"
;
import
{
Button
}
from
"
@/components/ui/button
"
;
import
{
Badge
}
from
"
@/components/ui/badge
"
;
import
{
Progress
}
from
"
@/components/ui/progress
"
;
import
{
ResponsiveContainer
,
ComposedChart
,
XAxis
,
YAxis
,
Line
,
Scatter
,
}
from
"
recharts
"
;
import
{
BarChart3
}
from
"
lucide-react
"
;
import
{
cn
}
from
"
@/lib/utils
"
;
type
YearStats
=
{
year
:
number
;
median
:
number
;
p95
:
number
;
frac_gt_0_4
?:
number
;
// 0..1
frac_gt_0_6
?:
number
;
// 0..1
};
type
ChangeStats
=
{
fracLoss
:
number
;
// ≤ -0.1 (0..1)
fracGain
:
number
;
// ≥ +0.1 (0..1)
};
type
Props
=
{
left
:
YearStats
;
right
:
YearStats
;
change
?:
ChangeStats
;
title
?:
string
;
className
?:
string
;
// for the trigger button container
open
?:
boolean
;
onOpenChange
?:
(
open
:
boolean
)
=>
void
;
triggerLabel
?:
string
;
};
const
fmtNDVI
=
(
v
:
number
)
=>
(
Number
.
isFinite
(
v
)
?
v
.
toFixed
(
3
)
:
"
—
"
);
const
fmtPct
=
(
f
?:
number
)
=>
f
==
null
||
!
Number
.
isFinite
(
f
)
?
"
—
"
:
`
${(
f
*
100
).
toFixed
(
1
)}
%`
;
// tiny slopegraph
function
SlopeMini
({
a
,
b
,
domain
,
width
=
180
,
height
=
26
,
}:
{
a
:
number
;
b
:
number
;
domain
?:
[
number
,
number
];
width
?:
number
;
height
?:
number
;
})
{
const
both
=
[
{
x
:
"
A
"
,
v
:
a
},
{
x
:
"
B
"
,
v
:
b
},
];
const
up
=
b
>
a
;
const
lineColor
=
up
?
"
#22c55e
"
/* emerald-500 */
:
"
#ef4444
"
/* rose-500 */
;
return
(
<
ResponsiveContainer
width
=
{
width
}
height
=
{
height
}
>
<
ComposedChart
data
=
{
both
}
margin
=
{
{
top
:
6
,
bottom
:
6
,
left
:
6
,
right
:
6
}
}
>
<
XAxis
dataKey
=
"x"
type
=
"category"
hide
/>
<
YAxis
type
=
"number"
domain
=
{
domain
??
[
Math
.
min
(
a
,
b
),
Math
.
max
(
a
,
b
)
+
1
e
-
6
]
}
hide
/>
<
Line
data
=
{
both
}
dataKey
=
"v"
type
=
"linear"
stroke
=
{
lineColor
}
strokeWidth
=
{
2
}
dot
=
{
false
}
/>
<
Scatter
data
=
{
[
both
[
0
]]
}
dataKey
=
"v"
fill
=
"#9ca3af"
/>
{
/* left year */
}
<
Scatter
data
=
{
[
both
[
1
]]
}
dataKey
=
"v"
fill
=
"#3b82f6"
/>
{
/* right year */
}
</
ComposedChart
>
</
ResponsiveContainer
>
);
}
function
Row
({
label
,
a
,
b
,
fmt
,
domain
,
}:
{
label
:
string
;
a
:
number
;
b
:
number
;
fmt
:
(
v
:
number
)
=>
string
;
domain
?:
[
number
,
number
];
})
{
return
(
<
div
className
=
"grid grid-cols-[1fr_auto] items-center gap-2"
>
<
div
className
=
"text-sm text-muted-foreground"
>
{
label
}
</
div
>
<
div
className
=
"flex items-center gap-2"
>
<
span
className
=
"text-xs tabular-nums text-muted-foreground"
>
{
fmt
(
a
)
}
</
span
>
<
SlopeMini
a
=
{
a
}
b
=
{
b
}
domain
=
{
domain
}
/>
<
span
className
=
{
cn
(
"
text-xs font-medium tabular-nums
"
,
b
-
a
>=
0
?
"
text-emerald-600
"
:
"
text-rose-600
"
)
}
title
=
"Delta (right - left)"
>
{
(
b
-
a
>=
0
?
"
+
"
:
""
)
+
fmt
(
b
-
a
)
}
</
span
>
</
div
>
</
div
>
);
}
export
default
function
KpiDialog
({
left
,
right
,
change
,
title
=
"
NDVI comparison
"
,
className
,
open
,
onOpenChange
,
triggerLabel
=
"
KPIs
"
,
}:
Props
)
{
const
ndviDomain
:
[
number
,
number
]
=
[
-
0.2
,
0.8
];
// adjust if needed
return
(
<
Dialog
open
=
{
open
}
onOpenChange
=
{
onOpenChange
}
>
<
div
className
=
{
cn
(
"
flex justify-end
"
,
className
)
}
>
<
DialogTrigger
asChild
>
<
Button
size
=
"icon"
variant
=
"secondary"
className
=
"rounded-full shadow"
>
<
BarChart3
className
=
"h-4 w-4"
/>
<
span
className
=
"sr-only"
>
{
triggerLabel
}
</
span
>
</
Button
>
</
DialogTrigger
>
</
div
>
<
DialogContent
className
=
"sm:max-w-[420px] w-[min(92vw,420px)]"
>
<
DialogHeader
>
<
DialogTitle
className
=
"flex items-center justify-between"
>
<
span
>
{
title
}
</
span
>
<
span
className
=
"flex items-center gap-3"
>
<
span
className
=
"flex items-center gap-1 text-xs text-muted-foreground"
>
<
span
className
=
"inline-block h-2.5 w-2.5 rounded-full bg-gray-400"
/>
{
left
.
year
}
</
span
>
<
span
className
=
"flex items-center gap-1 text-xs"
>
<
span
className
=
"inline-block h-2.5 w-2.5 rounded-full bg-blue-500"
/>
{
right
.
year
}
</
span
>
</
span
>
</
DialogTitle
>
</
DialogHeader
>
<
div
className
=
"space-y-3 pt-1"
>
<
Row
label
=
"Median NDVI"
a
=
{
left
.
median
}
b
=
{
right
.
median
}
fmt
=
{
fmtNDVI
}
domain
=
{
ndviDomain
}
/>
<
Row
label
=
"p95 NDVI"
a
=
{
left
.
p95
}
b
=
{
right
.
p95
}
fmt
=
{
fmtNDVI
}
domain
=
{
ndviDomain
}
/>
<
Row
label
=
"% area > 0.4"
a
=
{
left
.
frac_gt_0_4
??
NaN
}
b
=
{
right
.
frac_gt_0_4
??
NaN
}
fmt
=
{
(
v
)
=>
fmtPct
(
v
)
}
domain
=
{
[
0
,
1
]
}
/>
<
Row
label
=
"% area > 0.6"
a
=
{
left
.
frac_gt_0_6
??
NaN
}
b
=
{
right
.
frac_gt_0_6
??
NaN
}
fmt
=
{
(
v
)
=>
fmtPct
(
v
)
}
domain
=
{
[
0
,
1
]
}
/>
<
div
className
=
"pt-2 flex items-center justify-between"
>
<
div
className
=
"text-sm text-muted-foreground"
>
Δ strong change
</
div
>
<
div
className
=
"flex gap-2"
>
<
Badge
className
=
"bg-rose-50 text-rose-600 border-rose-200"
variant
=
"secondary"
>
Loss
{
fmtPct
(
change
?.
fracLoss
)
}
</
Badge
>
<
Badge
className
=
"bg-emerald-50 text-emerald-600 border-emerald-200"
variant
=
"secondary"
>
Gain
{
fmtPct
(
change
?.
fracGain
)
}
</
Badge
>
</
div
>
</
div
>
<
div
className
=
"space-y-2"
>
<
div
className
=
"text-xs text-muted-foreground"
>
Loss ≤ −0.1
</
div
>
<
Progress
value
=
{
Math
.
max
(
0
,
Math
.
min
(
100
,
(
change
?.
fracLoss
??
0
)
*
100
))
}
className
=
"h-2 [&>div]:bg-rose-500"
/>
<
div
className
=
"text-xs text-muted-foreground pt-2"
>
Gain ≥ +0.1
</
div
>
<
Progress
value
=
{
Math
.
max
(
0
,
Math
.
min
(
100
,
(
change
?.
fracGain
??
0
)
*
100
))
}
className
=
"h-2 [&>div]:bg-emerald-500"
/>
</
div
>
</
div
>
</
DialogContent
>
</
Dialog
>
);
}
frontend/src/components/map/split-map.tsx
View file @
048d0ef1
...
@@ -70,13 +70,10 @@ function SplitMap({
...
@@ -70,13 +70,10 @@ function SplitMap({
{
/* Bottom-centered selector (robust centering, like slider) */
}
{
/* Bottom-centered selector (robust centering, like slider) */
}
<
div
<
div
className
=
"absolute inset-x-0 bottom-5 z-50 flex
justify-center
pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
className
=
"absolute inset-x-0
opacity-80
bottom-5 z-50 flex pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[
9
0%] max-w-sm"
>
<
div
className
=
"pointer-events-auto w-[
2
0%] max-w-sm"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
CardContent
>
<
CardContent
>
<
Label
htmlFor
=
"leftYear"
className
=
"text-xs text-muted-foreground mb-1 block"
>
Left year
</
Label
>
<
Select
value
=
{
String
(
leftYear
)
}
onValueChange
=
{
(
v
)
=>
setLeftYear
(
Number
(
v
))
}
>
<
Select
value
=
{
String
(
leftYear
)
}
onValueChange
=
{
(
v
)
=>
setLeftYear
(
Number
(
v
))
}
>
<
SelectTrigger
id
=
"leftYear"
className
=
"h-9"
>
<
SelectTrigger
id
=
"leftYear"
className
=
"h-9"
>
<
SelectValue
placeholder
=
"Select year"
/>
<
SelectValue
placeholder
=
"Select year"
/>
...
@@ -108,13 +105,10 @@ function SplitMap({
...
@@ -108,13 +105,10 @@ function SplitMap({
{
/* Bottom-centered selector */
}
{
/* Bottom-centered selector */
}
<
div
<
div
className
=
"absolute inset-x-0 bottom-5 z-50 flex
justify-center
pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
className
=
"absolute inset-x-0
opacity-80
bottom-5 z-50 flex pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[
9
0%] max-w-sm"
>
<
div
className
=
"pointer-events-auto w-[
2
0%] max-w-sm"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
CardContent
>
<
CardContent
>
<
Label
htmlFor
=
"rightYear"
className
=
"text-xs text-muted-foreground mb-1 block"
>
Right year
</
Label
>
<
Select
value
=
{
String
(
rightYear
)
}
onValueChange
=
{
(
v
)
=>
setRightYear
(
Number
(
v
))
}
>
<
Select
value
=
{
String
(
rightYear
)
}
onValueChange
=
{
(
v
)
=>
setRightYear
(
Number
(
v
))
}
>
<
SelectTrigger
id
=
"rightYear"
className
=
"h-9"
>
<
SelectTrigger
id
=
"rightYear"
className
=
"h-9"
>
<
SelectValue
placeholder
=
"Select year"
/>
<
SelectValue
placeholder
=
"Select year"
/>
...
...
frontend/src/components/ui/badge.tsx
0 → 100644
View file @
048d0ef1
import
*
as
React
from
"
react
"
import
{
Slot
}
from
"
@radix-ui/react-slot
"
import
{
cva
,
type
VariantProps
}
from
"
class-variance-authority
"
import
{
cn
}
from
"
@/lib/utils
"
const
badgeVariants
=
cva
(
"
inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden
"
,
{
variants
:
{
variant
:
{
default
:
"
border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90
"
,
secondary
:
"
border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90
"
,
destructive
:
"
border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60
"
,
outline
:
"
text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground
"
,
},
},
defaultVariants
:
{
variant
:
"
default
"
,
},
}
)
function
Badge
({
className
,
variant
,
asChild
=
false
,
...
props
}:
React
.
ComponentProps
<
"
span
"
>
&
VariantProps
<
typeof
badgeVariants
>
&
{
asChild
?:
boolean
})
{
const
Comp
=
asChild
?
Slot
:
"
span
"
return
(
<
Comp
data-slot
=
"badge"
className
=
{
cn
(
badgeVariants
({
variant
}),
className
)
}
{
...
props
}
/>
)
}
export
{
Badge
,
badgeVariants
}
frontend/src/components/ui/dialog.tsx
0 → 100644
View file @
048d0ef1
import
*
as
React
from
"
react
"
import
*
as
DialogPrimitive
from
"
@radix-ui/react-dialog
"
import
{
XIcon
}
from
"
lucide-react
"
import
{
cn
}
from
"
@/lib/utils
"
function
Dialog
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Root
>
)
{
return
<
DialogPrimitive
.
Root
data-slot
=
"dialog"
{
...
props
}
/>
}
function
DialogTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Trigger
>
)
{
return
<
DialogPrimitive
.
Trigger
data-slot
=
"dialog-trigger"
{
...
props
}
/>
}
function
DialogPortal
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Portal
>
)
{
return
<
DialogPrimitive
.
Portal
data-slot
=
"dialog-portal"
{
...
props
}
/>
}
function
DialogClose
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Close
>
)
{
return
<
DialogPrimitive
.
Close
data-slot
=
"dialog-close"
{
...
props
}
/>
}
function
DialogOverlay
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Overlay
>
)
{
return
(
<
DialogPrimitive
.
Overlay
data-slot
=
"dialog-overlay"
className
=
{
cn
(
"
data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogContent
({
className
,
children
,
showCloseButton
=
true
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Content
>
&
{
showCloseButton
?:
boolean
})
{
return
(
<
DialogPortal
data-slot
=
"dialog-portal"
>
<
DialogOverlay
/>
<
DialogPrimitive
.
Content
data-slot
=
"dialog-content"
className
=
{
cn
(
"
bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg
"
,
className
)
}
{
...
props
}
>
{
children
}
{
showCloseButton
&&
(
<
DialogPrimitive
.
Close
data-slot
=
"dialog-close"
className
=
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<
XIcon
/>
<
span
className
=
"sr-only"
>
Close
</
span
>
</
DialogPrimitive
.
Close
>
)
}
</
DialogPrimitive
.
Content
>
</
DialogPortal
>
)
}
function
DialogHeader
({
className
,
...
props
}:
React
.
ComponentProps
<
"
div
"
>
)
{
return
(
<
div
data-slot
=
"dialog-header"
className
=
{
cn
(
"
flex flex-col gap-2 text-center sm:text-left
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogFooter
({
className
,
...
props
}:
React
.
ComponentProps
<
"
div
"
>
)
{
return
(
<
div
data-slot
=
"dialog-footer"
className
=
{
cn
(
"
flex flex-col-reverse gap-2 sm:flex-row sm:justify-end
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogTitle
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Title
>
)
{
return
(
<
DialogPrimitive
.
Title
data-slot
=
"dialog-title"
className
=
{
cn
(
"
text-lg leading-none font-semibold
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogDescription
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Description
>
)
{
return
(
<
DialogPrimitive
.
Description
data-slot
=
"dialog-description"
className
=
{
cn
(
"
text-muted-foreground text-sm
"
,
className
)
}
{
...
props
}
/>
)
}
export
{
Dialog
,
DialogClose
,
DialogContent
,
DialogDescription
,
DialogFooter
,
DialogHeader
,
DialogOverlay
,
DialogPortal
,
DialogTitle
,
DialogTrigger
,
}
frontend/src/components/ui/progress.tsx
0 → 100644
View file @
048d0ef1
import
*
as
React
from
"
react
"
import
*
as
ProgressPrimitive
from
"
@radix-ui/react-progress
"
import
{
cn
}
from
"
@/lib/utils
"
function
Progress
({
className
,
value
,
...
props
}:
React
.
ComponentProps
<
typeof
ProgressPrimitive
.
Root
>
)
{
return
(
<
ProgressPrimitive
.
Root
data-slot
=
"progress"
className
=
{
cn
(
"
bg-primary/20 relative h-2 w-full overflow-hidden rounded-full
"
,
className
)
}
{
...
props
}
>
<
ProgressPrimitive
.
Indicator
data-slot
=
"progress-indicator"
className
=
"bg-primary h-full w-full flex-1 transition-all"
style
=
{
{
transform
:
`translateX(-
${
100
-
(
value
||
0
)}
%)`
}
}
/>
</
ProgressPrimitive
.
Root
>
)
}
export
{
Progress
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment