- I - Architecture du projet
- II - Fonctionnalités
- III - Documentation des fonctionnalités
- IV - Installation
- V - Lancement de l'application
- VI - Liens utiles
Outils Open Source
- Front-End : ReactJS
- Back-End : NestJS
- Base de données : JSON
- Présentation : Marp
- Distribution : YunoHost
data/bundles.json
dist
src/
src/bundle
src/bundle/dto
src/domain
src/install
src/types
src/user
src/app.controller.spec.ts
src/app.controller.ts
src/app.module.ts
src/app.service.ts
src/main.ts
test/
.eslintrc.js
.gitignore
.prettierrc
docker-compose.yml
DocTechnique.md
lefthook.yml
nest-cli.json
package.json
pnpm-lock.yaml
Presentation.md
README.md
tsconfig.build.json
tsconfig.json
public
src
src/api
src/api/app.ts
src/assets/bundle.ts
src/components
src/components/layout
src/components/ui
src/components/bundle-list.tsx
src/components/create-form.tsx
src/components/form-install.tsx
src/components/site-footer.tsx
src/components/site-header.tsx
src/lib
src/lib/api.ts
src/lib/reactQuery.ts
src/lib/utils.ts
src/pages
src/pages/bundle.tsx
src/pages/create-bundle.tsx
src/pages/error.tsx
src/pages/home.tsx
src/types
src/types/interface.ts
src/App.tsx
src/Router.tsx
src/index.css
src/main.tsx
src/vite-env.d.ts
.eslintrc.cjs
.gitignore
.prettierignore
.prettierrc
README.md
components.json
index.html
lefthook.yml
package.json
pnpm-lock.yaml
postcss.config.js
tailwind.config.js
tsconfig.json
tsconfig.node.json
vite.config.ts
- Téléchargement simplifié de Bundles et d'Applications YunoHost
- Installation de Bundles YunoHost
- Installation d'Applications YunoHost
Nous avons opté pour un design épuré et pour une expérience utilisateur simplifiée.
Le dossier src/components
contient les composants de l'application.
src/components/form-install.tsx
Le composant FormInstall
est conçu pour gérer le processus d'installation de bundles d'applications.
Il permet à l'utilisateur de créer et d'installer un ensemble d'applications regroupées sous forme de bundle. Il collecte des informations nécessaires, telles que le domaine, l'administrateur, le mot de passe, et les applications à installer.
// imports
export default function FormInstall({ id }: { id: number }) {
const { data: admins, isLoading: isLoadingAdmins } = useGetAdmins();
const { data: bundle, isLoading: isLoadingBundle } = useGetOneBundle(id);
const { data: allDomains, isLoading: isLoadingDomains } = useGetDomains();
const [selectedApps, setSelectedApps] = React.useState<string[]>([]);
const navigate = useNavigate();
const handleSelectionChange = (selectedApp: string) => {
setSelectedApps((prevSelectedApps) => {
const updatedSelectedApps = prevSelectedApps.includes(selectedApp)
? prevSelectedApps.filter((app) => app !== selectedApp)
: [...prevSelectedApps, selectedApp];
form.setValue('apps', updatedSelectedApps);
return updatedSelectedApps;
});
};
const formSchema = z.object({
domain: z.string(),
user: z.string().min(1, "Le nom d'utilisateur est requis"),
password: z.string().min(6, 'Le mot de passe doit avoir au moins 6 caractères'),
apps: z.array(z.string()),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});
async function onSubmit(data: z.infer<typeof formSchema>) {
const appData = selectedApps.map((selectedApp) => {
const app = bundle?.apps?.find((app) => app.app_name === selectedApp);
return {
appName: app?.app_name || selectedApp,
domain: data.domain,
path: '/' + selectedApp,
admin: data.user,
password: data.password,
};
});
console.log(appData);
try {
await api.post(`${import.meta.env.VITE_API_URL}/api/install`, appData);
const eventSource = new EventSource('https://dcm1tlg2.nohost.me/api/install/updates');
eventSource.onmessage = (event) => {
console.log("Mise à jour de l'installation:", event.data);
// Vérifiez si le message contient 'Installation completed'
if (event.data.includes('Installation completed')) {
eventSource.close(); // Fermez l'EventSource
navigate('/'); // Redirigez vers la page d'accueil
}
};
} catch (error) {
console.error('Erreur lors de la création du bundle :', error);
}
}
etc...
}
src/components/create-form.tsx
Le composant BundleNew
a pour objectif de permettre à l'utilisateur de créer un nouveau bundle en fournissant des informations telles que le nom du bundle, la description et les applications incluses.
// imports
export default function BundleNew() {
const { data: apps } = useGetApps();
const options = Object.keys(apps?.apps || {}).map((key) => ({
value: apps?.apps[key]?.manifest?.id || key,
label: apps?.apps[key]?.manifest?.name || key,
description: apps?.apps[key]?.manifest?.description || '',
}));
const [selectedApps, setSelectedApps] = useState<string[]>([]);
const navigate = useNavigate();
const formSchema = z.object({
title: z
.string()
.min(2, {
message: "Le nom du bundle doit être d'au moins 2 caractères.",
})
.max(30, {
message: 'Le nom du bundle ne peut pas dépasser 30 caractères.',
}),
description: z.string(),
apps: z.array(
z.object({
appName: z.string(),
description: z.string(),
})
),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
description: '',
apps: [],
},
});
const handleSelectionChange = (selectedOptions: string[]) => {
const updatedApps = selectedOptions.map((appValue) => ({
appName: appValue,
description: options.find((option) => option.value === appValue)?.description.fr || '',
}));
form.setValue('apps', updatedApps);
console.log(updatedApps);
setSelectedApps(selectedOptions);
};
async function onSubmit(data: z.infer<typeof formSchema>) {
try {
await api.post(`${import.meta.env.VITE_API_URL}/api/bundle/`, data);
navigate('/');
} catch (error) {
console.error('Erreur lors de la création du bundle :', error);
}
}
etc...
}
src/components/bundle-list.tsx
Le composant BundleList
a pour but d'afficher une liste de bundles disponibles dans l'application.
// imports
export default function BundleList() {
const { data: bundles, isLoading, error } = useGetBundles();
if (isLoading) return <div>Loading bundles...</div>;
if (error) return <div>Aucun Bundle trouvé</div>;
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{bundles &&
bundles.map((bundle) => (
<Card key={bundle.id}>
<CardHeader>
<CardTitle>{bundle.title}</CardTitle>
<CardDescription>{bundle.description}</CardDescription>
<a href={`/bundle/${bundle.id}`} className={cn(buttonVariants({ variant: 'link' }), 'w-fit p-0')}>
Voir ce bundle
</a>
</CardHeader>
</Card>
))}
</div>
);
}
data/bundles.json
Le fichier bundles.json
contient tous les Bundles des applications YunoHost.
{
"id": 1,
"title": "Bundle Association",
"description": "description du bundle",
"apps": [
{
"app_name": "yeswiki",
"name": "YesWiki",
"logo": "https://github.com/YunoHost/apps/blob/master/logos/yeswiki.png",
"description": "Wiki facile et rapide à prendre en main",
"screenshot": "https://github.com/YesWiki/yeswiki/blob/doryphore-dev/files/GererSite_modele_19880101000000_23001231235959.jpg"
},
{
"app_name": "paheko",
"name": "Paheko",
"logo": "https://github.com/YunoHost/apps/blob/master/logos/paheko.png",
"description": "Logiciel de gestion d'association",
"screenshot": "https://github.com/YunoHost-Apps/paheko_ynh/blob/master/doc/screenshots/screenshot.png"
},
etc...
]
}
src/bundle
Le fichier bundle.controller.ts
est un contrôleur NestJS responsable de la gestion des requêtes liées aux Bundles dans l'application YunoHost.
Pour respecter une certaine architecture d'application le dossier src/bundle
contient toutes les configurations de route des Bundles.
import { Controller, Param } from '@nestjs/common';
import { BundleService } from './bundle.service';
import { Post, Body, Get } from '@nestjs/common';
import { BundleData } from 'src/types/interface';
import { CreateBundleDto } from './dto/create-bundle.dto';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@ApiTags('Bundle')
@Controller('bundle')
export class BundleController {
constructor(private readonly bundleService: BundleService) {}
// Créer un Bundle
@Post()
@ApiResponse({
status: 200,
description: "Création d'un bundle.",
})
async createBundle(@Body() bundleData: CreateBundleDto): Promise<void> {
await this.bundleService.createBundle(bundleData);
}
etc...
}
src/install
Le fichier install.controller.ts
est un contrôleur NestJS responsable de la gestion des requêtes liées à l'installation d'applications et à la récupération d'informations sur les Bundles et les applications dans l'application YunoHost.
Pour respecter une certaine architecture d'application le dossier src/install
contient toutes les configurations des routes des applications.
import { Controller, Param } from '@nestjs/common';
import { Post, Body, Get, Res, HttpStatus } from '@nestjs/common';
import { InstallService } from './install.service';
import { ApiResponse, ApiTags, ApiBody } from '@nestjs/swagger';
@ApiTags('Install')
@Controller('install')
export class InstallController {
constructor(private install: InstallService) {}
// Installer une application
@Post('')
@ApiBody({
description: "Les données de l'application à installer",
type: [Object],
})
@ApiResponse({
status: 200,
description: "Installation d'une application.",
})
async installApps(@Body() appsData: any[], @Res() response) {
try {
const results = await this.install.installApps(appsData);
response.status(HttpStatus.OK).json({ results });
} catch (error) {
response
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ error: error.message });
}
}
// Récupérer tous les Bundles
@Get('api/bundle')
@ApiResponse({
status: 200,
description: 'Récupération de tous les Bundles.',
})
async getAllBundles(@Res() response) {
try {
const bundles = await this.install.getAllBundles();
console.log({ message: 'Récupération des bundles effectuée !', bundles });
response.status(HttpStatus.OK).json({ bundles });
} catch (error) {
console.log({
message: 'Erreur lors de la récupération des bundles : ',
error,
});
response
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ error: error.message });
}
}
etc...
}
$ pnpm install
# Dévelopment
$ pnpm run start
ou
$ npm run start
# Watch Mode
$ pnpm run start:dev
ou
$ npm run start:dev
# Production Mode
$ pnpm run start:prod
ou
$ pnpm run start:prod
- Axel VERGLAS
- Lisa AU
- Dorine HENRY
- Pierre Arthur TOUTOUOM YOUNTEBO
- El Mehdi BADOUH
- Steven YAMBOS