Skip to content

Commit 9b0e745

Browse files
committed
dev(process): UI for login to EcoTaxa via backend
1 parent c9b9231 commit 9b0e745

File tree

8 files changed

+2009
-233
lines changed

8 files changed

+2009
-233
lines changed

package-lock.json

Lines changed: 1702 additions & 199 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@heroui/link": "2.2.20",
2626
"@heroui/navbar": "^2.2.20",
2727
"@heroui/react": "^2.8.1",
28+
"@heroui/autocomplete": "^2.2.20",
2829
"@heroui/spinner": "^2.2.20",
2930
"@heroui/switch": "^2.2.20",
3031
"@heroui/theme": "^2.4.19",

src/api/interfaces.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,21 @@ export interface VignetteResponse {
109109
folder: string;
110110
}
111111

112+
export interface EcoTaxaLoginResponse {
113+
token: string;
114+
}
115+
112116
export interface Ecotaxa {
113117
id: string;
114118
}
115119

120+
export interface EcotaxaProject {
121+
projid: number;
122+
title: string;
123+
}
124+
125+
export type EcotaxaProjects = Array<EcotaxaProject>;
126+
116127
export interface ProjectUpdate extends MinProject {
117128
updatedAt: string;
118129
}

src/api/zooprocess-api.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,22 @@ export async function getVignettes(
125125
return response.data; //.data;
126126
}
127127

128+
export async function loginToEcoTaxa(token: string, email: string, password: string) {
129+
const api = await axiosInstance({ token: token });
130+
const url = '/ecotaxa/login';
131+
const req = { username: email, password: password };
132+
const response = await api.post<I.EcoTaxaLoginResponse>(url, req);
133+
134+
return response.data.token;
135+
}
136+
137+
export async function listEcoTaxaProjects(token: string, ecotaxaToken: string) {
138+
const api = await axiosInstance({ token: token });
139+
const url = '/ecotaxa/projects?token=' + ecotaxaToken;
140+
const response = await api.get<I.EcotaxaProjects>(url);
141+
142+
return response.data;
143+
}
128144
/////////////////////////////////
129145
// UNUSED BELOW
130146
/////////////////////////////////

src/app/components/form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ const FormMessage = React.forwardRef<
150150
<p
151151
ref={ref}
152152
id={formMessageId}
153-
className={twMergeClsx('text-[0.8rem] font-medium text-destructive', className)}
153+
className={twMergeClsx('text-[0.8rem] font-medium text-destructive bg-red-500/15 p-2 rounded-md', className)}
154154
{...props}
155155
>
156156
{body}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import * as z from 'zod';
2+
import React, { useState, useTransition } from 'react';
3+
4+
import { Input } from '@heroui/input';
5+
import {
6+
Form,
7+
FormControl,
8+
FormField,
9+
FormItem,
10+
FormLabel,
11+
FormMessage,
12+
} from 'app/components/form.tsx';
13+
import { FormError } from 'app/components/form-error.tsx';
14+
import { FormSuccess } from 'app/components/form-success.tsx';
15+
import { Button } from '@heroui/button';
16+
17+
import { EyeFilledIcon } from '../login/components/EyeFilledIcon.jsx';
18+
import { EyeSlashFilledIcon } from '../login/components/EyeSlashFilledIcon.jsx';
19+
20+
import { useTranslation } from 'react-i18next';
21+
import { zodResolver } from '@hookform/resolvers/zod';
22+
import { useForm } from 'react-hook-form';
23+
24+
// Schema for EcoTaxa login validation
25+
const EcoTaxaLoginSchema = z.object({
26+
username: z.string().min(1, { message: 'Username is required' }),
27+
password: z.string().min(1, { message: 'Password is required' }),
28+
});
29+
30+
export type EcoTaxaCredentials = z.infer<typeof EcoTaxaLoginSchema>;
31+
32+
interface EcoTaxaLoginFormProps {
33+
onSubmit: (credentials: EcoTaxaCredentials) => void;
34+
}
35+
36+
export const EcoTaxaLoginForm: React.FC<EcoTaxaLoginFormProps> = ({ onSubmit }) => {
37+
const [error, setError] = useState<string | undefined>('');
38+
const [success, setSuccess] = useState<string | undefined>('');
39+
const [isPending, startTransition] = useTransition();
40+
41+
const form = useForm<z.infer<typeof EcoTaxaLoginSchema>>({
42+
resolver: zodResolver(EcoTaxaLoginSchema),
43+
defaultValues: {
44+
username: '',
45+
password: '',
46+
},
47+
});
48+
49+
const handleSubmit = (values: z.infer<typeof EcoTaxaLoginSchema>) => {
50+
setError('');
51+
setSuccess('');
52+
53+
startTransition(() => {
54+
onSubmit(values);
55+
});
56+
};
57+
58+
const [isVisible, setIsVisible] = React.useState(false);
59+
const toggleVisibility = () => setIsVisible(!isVisible);
60+
61+
return (
62+
<div className="w-full max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
63+
<h2 className="text-2xl font-bold mb-6 text-center">Connect to EcoTaxa</h2>
64+
<p className="text-gray-600 mb-6 text-center">
65+
Please enter your EcoTaxa credentials to upload data
66+
</p>
67+
68+
<Form {...form}>
69+
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
70+
<div className="space-y-4">
71+
<FormField
72+
control={form.control}
73+
name="username"
74+
render={({ field }) => (
75+
<FormItem>
76+
<FormLabel>Email</FormLabel>
77+
<FormControl>
78+
<Input
79+
{...field}
80+
disabled={isPending}
81+
placeholder="Email address"
82+
type="text"
83+
/>
84+
</FormControl>
85+
<FormMessage />
86+
</FormItem>
87+
)}
88+
/>
89+
<FormField
90+
control={form.control}
91+
name="password"
92+
render={({ field }) => (
93+
<FormItem>
94+
<FormLabel>Password</FormLabel>
95+
<FormControl>
96+
<Input
97+
{...field}
98+
disabled={isPending}
99+
placeholder="Your EcoTaxa password"
100+
endContent={
101+
<button
102+
className="focus:outline-none"
103+
type="button"
104+
onClick={toggleVisibility}
105+
>
106+
{isVisible ? (
107+
<EyeSlashFilledIcon className="text-2xl text-default-400 pointer-events-none" />
108+
) : (
109+
<EyeFilledIcon className="text-2xl text-default-400 pointer-events-none" />
110+
)}
111+
</button>
112+
}
113+
type={isVisible ? 'text' : 'password'}
114+
/>
115+
</FormControl>
116+
<FormMessage />
117+
</FormItem>
118+
)}
119+
/>
120+
</div>
121+
<FormError message={error} />
122+
<FormSuccess message={success} />
123+
<Button
124+
disabled={isPending}
125+
type="submit"
126+
className="w-full bg-blue-500 hover:bg-blue-600 text-white"
127+
>
128+
Connect to EcoTaxa
129+
</Button>
130+
</form>
131+
</Form>
132+
</div>
133+
);
134+
};

src/app/features/subsample/process/VignetteList.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ async function loadMatrixFromGz(url: string): Promise<number[][]> {
2121
return readMatrixFromCompressedBinary(buffer);
2222
}
2323

24-
// Fonction utilitaire pour sauvegarder le matrix .gz côté front
25-
function saveMaskGzClient(matrix: number[][], filename: string) {
26-
saveMatrixAsCompressedBinary(matrix, filename);
27-
}
2824
interface Props {
2925
initialVignettes: VignetteData[];
3026
folder: string;
@@ -66,7 +62,7 @@ export default function VignetteList({
6662
useEffect(() => {
6763
const selectedVignette = vignettes[selectedIndex];
6864
if (selectedVignette?.mask) {
69-
if (folder.startsWith('/vignette')) {
65+
if (folder.startsWith('/api/vignette')) {
7066
setZoomMaskSrc(`${folder}/${selectedVignette.mask}`);
7167
} else {
7268
setZoomMaskSrc(
@@ -171,7 +167,7 @@ export default function VignetteList({
171167
setEditMatrix(undefined); // Reset pour forcer rechargement
172168
const gzFile = vignettes[index].matrix;
173169
let matrixUrl;
174-
if (folder.startsWith('/api/backend')) {
170+
if (folder.startsWith('/api/vignette')) {
175171
matrixUrl = `${folder}/${gzFile}`;
176172
} else {
177173
matrixUrl = `/${folder}/${gzFile}`.replace(/\\/g, '/').replace(/\/\/+/, '/');
@@ -182,7 +178,7 @@ export default function VignetteList({
182178
} catch (e) {
183179
// fallback matrice zéro si erreur
184180
let imgPath;
185-
if (folder.startsWith('/api/backend')) {
181+
if (folder.startsWith('/api/vignette')) {
186182
imgPath = `${folder}/${vignettes[index].scan}`;
187183
} else {
188184
imgPath = `/${folder}/${vignettes[index].scan}`.replace(/\\/g, '/').replace(/\/\/+/, '/');
@@ -218,7 +214,7 @@ export default function VignetteList({
218214

219215
const editVignette = editIndex !== null ? vignettes[editIndex] : null;
220216
let imagePath;
221-
if (folder.startsWith('/api/backend')) {
217+
if (folder.startsWith('/api/vignette')) {
222218
imagePath = editVignette ? `${folder}/${editVignette.scan}` : '';
223219
} else {
224220
imagePath = editVignette
@@ -240,7 +236,7 @@ export default function VignetteList({
240236
}, [imagePath]);
241237

242238
return (
243-
<div className="relative h-screen w-full overflow-hidden bg-white">
239+
<div className="relative h-screenXX w-full overflow-hidden bg-white">
244240
{/* <div className="absolute inset-0 pl-[320px]"> */}
245241
{vignettes.length} to validate
246242
<List

0 commit comments

Comments
 (0)