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

feat: add bal widget config page #47

Merged
merged 13 commits into from
Feb 1, 2024
3 changes: 2 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ KEYCLOAK_ISSUER=
KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET=
NEXTAUTH_URL=
NEXTAUTH_SECRET=
NEXTAUTH_SECRET=
HCAPTCHA_SECRET_KEY=
258 changes: 258 additions & 0 deletions components/bal-widget/bal-widget-config-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import React, { useState, useMemo, useEffect } from "react";
import styled from "styled-components";
import { Input } from "@codegouvfr/react-dsfr/Input";
import { Button } from "@codegouvfr/react-dsfr/Button";
import { Checkbox } from "@codegouvfr/react-dsfr/Checkbox";

import { BALWidgetConfig } from "../../types/bal-widget";
import { MultiStringInput } from "../multi-string-input";
import { MultiLinkInput } from "../multi-link-input";
import { MultiSelectInput } from "../multi-select-input";
import { ClientApiDepotType } from "types/api-depot";
import { SourceMoissoneurType } from "types/moissoneur";
import { getClients } from "@/lib/api-depot";
import { getSources } from "@/lib/api-moissonneur-bal";

type BALWidgetConfigFormProps = {
config: BALWidgetConfig;
onSubmit: (data: BALWidgetConfig) => Promise<void>;
};

const StyledForm = styled.form`
h3,
h4 {
margin-bottom: 1rem;
}

section {
margin: 1.5rem 0;
}

.form-controls {
display: flex;
align-items: center;

> :not(:first-child) {
margin-left: 1rem;
}
}
`;

const defaultConfig: BALWidgetConfig = {
global: {
title: "Centre d'aide Base Adresse Locale",
hideWidget: false,
showOnPages: [],
},
communes: {
outdatedApiDepotClients: [],
outdatedHarvestSources: [],
},
gitbook: {
welcomeBlockTitle: "Ces articles pourraient vous aider",
topArticles: [],
},
contactUs: {
welcomeBlockTitle: "Nous contacter",
subjects: [],
},
};

export const BALWidgetConfigForm = ({
onSubmit,
config: baseConfig,
}: BALWidgetConfigFormProps) => {
const initialConfig = useMemo(
() => (baseConfig ? { ...baseConfig } : { ...defaultConfig }),
[baseConfig]
);

const [formData, setFormData] = useState<BALWidgetConfig>(initialConfig);
const [apiDepotClients, setApiDepotClients] = useState<ClientApiDepotType[]>(
[]
);
const [harvestSources, setHarvestSources] = useState<SourceMoissoneurType[]>(
[]
);

const canPublish = useMemo(() => {
return JSON.stringify(formData) !== JSON.stringify(initialConfig);
}, [formData, initialConfig]);

useEffect(() => {
async function fetchData() {
try {
const clients = await getClients();
setApiDepotClients(clients);
} catch (error) {
console.log(error);
}
try {
const sources = await getSources();
setHarvestSources(sources);
} catch (error) {
console.log(error);
}
}

fetchData();
}, []);

const handleEdit =
(section: keyof BALWidgetConfig, property: string) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { value } = e.target;
setFormData((state) => ({
...state,
[section]: {
...state[section],
[property]: value,
},
}));
};

const handleToggle =
(section: keyof BALWidgetConfig, property: string) => () => {
setFormData((state) => ({
...state,
[section]: {
...state[section],
[property]: !state[section][property],
},
}));
};

const resetForm = () => {
setFormData(initialConfig);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await onSubmit(formData);
};

return (
<StyledForm onSubmit={handleSubmit} className="fr-my-4w">
<h3>Configuration du widget</h3>
<section>
<h4>Globale</h4>
<Input
label="Titre du widget"
nativeInputProps={{
required: true,
value: formData.global.title,
onChange: handleEdit("global", "title"),
}}
/>
<Checkbox
className="perimeter-checkbox"
options={[
{
label: "Cacher le widget",
nativeInputProps: {
checked: formData.global.hideWidget,
onChange: handleToggle("global", "hideWidget"),
},
},
]}
/>
<MultiStringInput
label="Afficher le widget uniquement sur les pages :"
value={formData.global.showOnPages}
onChange={(value) =>
handleEdit("global", "showOnPages")({ target: { value } } as any)
}
placeholder="Path de la page autorisée (/programme-bal)"
/>
</section>
<section>
<h4>Aide aux communes</h4>
<MultiSelectInput
label="Clients API Dépôt caducs"
value={formData.communes.outdatedApiDepotClients}
options={apiDepotClients.map((client) => ({
value: client._id,
label: client.nom,
}))}
placeholder="Sélectionner les clients API Dépôt caducs"
onChange={(value) =>
setFormData((state) => ({
...state,
communes: {
...state.communes,
outdatedApiDepotClients: value,
},
}))
}
/>
<MultiSelectInput
label="Sources moissonnées caduques"
value={formData.communes.outdatedHarvestSources}
options={harvestSources.map((source) => ({
value: source._id,
label: source.title,
}))}
placeholder="Sélectionner les sources moissonnées caduques"
onChange={(value) =>
setFormData((state) => ({
...state,
communes: {
...state.communes,
outdatedHarvestSources: value,
},
}))
}
/>
</section>
<section>
<h4>Gitbook</h4>
<Input
label="Titre sur la page d'accueil"
nativeInputProps={{
required: true,
value: formData.gitbook.welcomeBlockTitle,
onChange: handleEdit("gitbook", "welcomeBlockTitle"),
}}
/>
<MultiLinkInput
label="Articles populaires :"
placeholders={[
"Comment puis-je obtenir une adresse ?",
"Path Gitbook de l'article (/utiliser-la-ban/mon-article)",
]}
value={formData.gitbook.topArticles}
onChange={(value) =>
handleEdit("gitbook", "topArticles")({ target: { value } } as any)
}
/>
</section>
<section>
<h4>Formulaire de contact</h4>
<Input
label="Titre sur la page d'accueil"
nativeInputProps={{
required: true,
value: formData.contactUs.welcomeBlockTitle,
onChange: handleEdit("contactUs", "welcomeBlockTitle"),
}}
/>
<MultiStringInput
label="Sujets du formulaire de contact :"
placeholder="Je souhaite publier une BAL"
value={formData.contactUs.subjects}
onChange={(value) =>
handleEdit("contactUs", "subjects")({ target: { value } } as any)
}
/>
</section>
<div className="form-controls">
<Button disabled={!canPublish} type="submit" iconId="fr-icon-save-line">
Publier
</Button>
<Button disabled={!canPublish} type="button" onClick={resetForm}>
Annuler
</Button>
</div>
</StyledForm>
);
};
8 changes: 8 additions & 0 deletions components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import Link from "next/link";
import { signIn, signOut } from "next-auth/react";
import type { Session } from "next-auth";
Expand Down Expand Up @@ -138,6 +139,13 @@ const Header = ({ session }: HeaderProps) => (
</a>
</Link>
</li>
<li>
<Link href="/bal-widget">
<a className="fr-nav__link" target="_self">
BAL Widget
</a>
</Link>
</li>
</ul>
</nav>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/moissonneur-bal/moissonneur-source-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface MoissoneurSourceItemProps {
model: string;
type: string;
_deleted: boolean;
_updated?: Date;
_updated?: string;
}

const MoissoneurSourceItem = ({
Expand Down
88 changes: 88 additions & 0 deletions components/multi-link-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from "react";
import { Button } from "@codegouvfr/react-dsfr/Button";
import styled from "styled-components";
import { BALWidgetLink } from "../types/bal-widget";

type MultiLinkInputProps = {
label: string;
value: BALWidgetLink[];
placeholders?: string[];
onChange: (value: BALWidgetLink[]) => void;
};

const StyledWrapper = styled.div`
> div {
display: flex;
align-items: center;
margin-bottom: 1rem;

> :not(:first-child) {
margin-left: 1rem;
}
}
`;

export const MultiLinkInput = ({
label,
value,
onChange,
placeholders = [],
}: MultiLinkInputProps) => {
const addValue = () => {
onChange([...value, { label: "", url: "" }]);
};

const removeValue = (index: number) => {
onChange(value.filter((_, i) => i !== index));
};

const handleChange = (
index: number,
property: keyof BALWidgetLink,
newValue: string
) => {
onChange(
value.map((v, i) => (i === index ? { ...v, [property]: newValue } : v))
);
};

return (
<StyledWrapper>
<div>
<label>{label}</label>
<Button
type="button"
title="Ajouter"
onClick={addValue}
iconId="fr-icon-add-line"
/>
</div>
{value.map(({ label, url }, index) => (
<div key={index}>
<input
required
className="fr-input"
type="text"
value={label}
onChange={(e) => handleChange(index, "label", e.target.value)}
placeholder={placeholders[0]}
/>
<input
required
className="fr-input"
type="text"
value={url}
onChange={(e) => handleChange(index, "url", e.target.value)}
placeholder={placeholders[1]}
/>
<Button
type="button"
title="Supprimer"
onClick={() => removeValue(index)}
iconId="fr-icon-delete-bin-line"
/>
</div>
))}
</StyledWrapper>
);
};
Loading
Loading