first push

This commit is contained in:
2026-05-16 21:29:22 +02:00
commit 25edd4fac7
68 changed files with 3475 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="PhotoShareHelri.styles.css" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<!-- Add chart.js reference if chart components are used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add chartjs-plugin-datalabels.min.js reference if chart components with data label feature is used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add sortable.js reference if SortableList component is used in your application. -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<script src="_framework/blazor.web.js"></script>
<script src="js/download.js"></script>
</body>
</html>
@@ -0,0 +1,88 @@
@using MudBlazor
@if (IsOpen && !string.IsNullOrWhiteSpace(CurrentImage))
{
<MudOverlay Visible="true" DarkBackground="true" ZIndex="1300" @onclick="CloseFromOverlay">
<MudPaper Class="pa-4 mx-auto"
@onclick:stopPropagation="true"
Style="
width:95%;
max-width:1200px;
margin-top:5vh;
background-color:#1e1e1e;
color:white;
position:relative;
border-radius:16px;">
<div class="text-center">
<MudImage Src="@CurrentImage"
Fluid="true"
Class="mb-3"
Style="max-height:75vh; object-fit:contain;" />
<MudStack Row="true"
Justify="Justify.SpaceBetween"
AlignItems="AlignItems.Center"
Class="mt-2">
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
OnClick="OnPrevious"
Disabled="@(!CanPrevious)">
⬅ Précédent
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
Disabled="@(SelectedImages.Count() >= MaxSelect && !SelectedImages.Contains(CurrentImage))"
OnClick="Toggle">
@(IsSelector ? (IsSelected ? "Retirer" : "Choisir") : "Télécharger")
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
OnClick="OnNext"
Disabled="@(!CanNext)">
Suivant ➡
</MudButton>
</MudStack>
<MudText Class="mt-3" Align="Align.Center" Typo="Typo.body1">
@(CurrentIndex + 1) / @Total
</MudText>
</div>
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Inherit"
OnClick="OnClose"
Style="position:absolute; top:10px; right:10px;" />
</MudPaper>
</MudOverlay>
}
@code {
[Parameter] public bool IsOpen { get; set; }
[Parameter] public string? CurrentImage { get; set; }
[Parameter] public bool IsSelector { get; set; }
[Parameter] public int CurrentIndex { get; set; }
[Parameter] public int Total { get; set; }
[Parameter] public HashSet<string> SelectedImages { get; set; }
[Parameter] public int MaxSelect { get; set; }
[Parameter] public bool CanNext { get; set; }
[Parameter] public bool CanPrevious { get; set; }
[Parameter] public bool IsSelected { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback OnNext { get; set; }
[Parameter] public EventCallback OnPrevious { get; set; }
[Parameter] public EventCallback Toggle { get; set; }
private async Task CloseFromOverlay()
{
await OnClose.InvokeAsync();
}
}
@@ -0,0 +1,79 @@
.gallery {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.gallery-thumb {
cursor: pointer;
transition: transform 0.2s;
}
.gallery-thumb:hover {
transform: scale(1.05);
}
.header-selection {
display: flex;
align-items: center;
width: 60%;
}
.gallery-wrapper {
display: flex;
flex-direction: column;
}
.gallery-actions {
display: flex;
justify-content: flex-end;
margin: 1rem 0;
}
.titles {
flex: 1;
text-align: center;
}
.image-container {
position: relative;
display: inline-block;
width: 300px;
height: 300px;
overflow: hidden; /* cache le dépassement */
border-radius: 8px;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: contain;
background-color: #000;
transition: opacity 0.2s, border 0.2s, transform 0.2s;
}
/* Image sélectionnée */
.image-container.selected img {
opacity: 0.6;
border: 3px solid limegreen;
}
/* Bouton overlay */
.select-btn {
position: absolute;
bottom: 8px;
right: 8px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
font-size: 18px;
cursor: pointer;
transition: background-color 0.2s;
}
.select-btn:hover {
background-color: rgba(0, 128, 0, 0.8);
}
@@ -0,0 +1,29 @@
@using MudBlazor
@inherits LayoutComponentBase
@* Required *@
<MudThemeProvider />
<MudPopoverProvider />
@* Needed for dialogs *@
<MudDialogProvider />
@* Needed for snackbars *@
<MudSnackbarProvider />
<MudLayout>
<!-- Ton menu -->
<NavMenu />
<MudMainContent>
<MudBreakpointProvider>
@Body
</MudBreakpointProvider>
</MudMainContent>
</MudLayout>
<div id="blazor-error-ui">
Une erreur dans le traitement est survenue.
<a href="" class="reload">Recharger</a>
</div>
@@ -0,0 +1,112 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.main-wrapper {
flex: 1;
display: flex;
justify-content: center;
}
.content {
width: 100%;
max-width: 1200px;
}
.navbar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 1px) {
.page {
flex-direction: column;
}
.navbar {
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
.content-inner {
display: flex;
flex-direction: column;
align-items: center;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -0,0 +1,175 @@
@using Microsoft.AspNetCore.Components.Routing
@using ModelsLib
@using MudBlazor
@inject NavigationManager Navigation
@rendermode InteractiveServer
@inject IWebHostEnvironment Env
@inject PhotoService ShootingDb
@inject IDialogService DialogService
<MudHidden Breakpoint="Breakpoint.MdAndDown">
<MudAppBar Color="Color.Dark" Elevation="4">
<!-- Logo -->
<MudText Typo="Typo.h6" Class="ml-2">
<MudNavLink Href="/" Class="nav-link">
Photo Sharing Helrira
</MudNavLink>
</MudText>
<MudSpacer />
<!-- Menu desktop -->
<MudNavMenu Class="d-flex">
<MudNavLink Href="/shootingdesire" Class="nav-link">
Projets
</MudNavLink>
<MudNavLink Href="/Contact" Class="nav-link">
Contact
</MudNavLink>
</MudNavMenu>
<!-- Recherche desktop -->
<MudStack Row="true" Spacing="2" Class="ml-4">
<MudTextField @bind-Value="Id"
Label="Recherche"
Variant="Variant.Outlined"
Style="color: white;"
InputStyle="color: white;" />
<MudButton OnClick="SendSelection"
Variant="Variant.Outlined"
Color="Color.Inherit">
Rechercher
</MudButton>
</MudStack>
</MudAppBar>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.LgAndUp">
<!-- Drawer mobile -->
<!-- AppBar mobile -->
<MudAppBar Color="Color.Dark" Elevation="4">
<!-- Bouton burger -->
<MudIconButton Icon="@Icons.Material.Filled.Menu"
Color="Color.Default"
OnClick="@(() => _drawerOpen = true)" />
<MudText Typo="Typo.h6" Class="ml-2">
<MudNavLink Href="/" Class="nav-link">
Photo Sharing Helrira
</MudNavLink>
</MudText>
<MudSpacer />
<!-- Bouton recherche -->
<MudIconButton Icon="@Icons.Material.Filled.Search"
Color="Color.Default"
OnClick="OpenSearchDialogAsync" />
</MudAppBar>
<!-- Drawer mobile -->
<MudDrawer @bind-Open="_drawerOpen"
Anchor="Anchor.Left"
Variant="DrawerVariant.Temporary"
Color="Color.Dark"
Class="custom-drawer drawer-fixed-width">
<MudStack Spacing="2" Class="pa-3" Style="display:flex; flex-direction:column; height:100%;">
<MudNavMenu Class="flex-grow-1">
<MudNavLink Href="/shootingdesire">Projets</MudNavLink>
<MudNavLink Href="/Contact">Contact</MudNavLink>
</MudNavMenu>
</MudStack>
</MudDrawer>
</MudHidden>
@code {
private string Id { get; set; } = "";
private bool _drawerOpen = false;
private async Task OpenSearchDialogAsync()
{
try
{
var parameters = new DialogParameters();
parameters.Add("OnSearch", EventCallback.Factory.Create<string>(this, SendSelectionMobil));
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialogRef = await DialogService.ShowAsync<SearchDialog>("", parameters, options);
var result = await dialogRef.Result;
if (!result.Canceled)
{
Console.WriteLine("Résultat : " + result.Data);
}
}
catch (Exception ex)
{
Console.WriteLine("ERREUR DIALOG : " + ex.Message);
Console.WriteLine(ex.StackTrace);
}
}
void ToggleDrawer() => _drawerOpen = !_drawerOpen;
/// <summary>
/// a la validation de la saisie
/// </summary>
private async void SendSelectionMobil(string value)
{
Id = value;
//si une saisie est faite dans le champs
SendSelection();
}
/// <summary>
/// a la validation de la saisie
/// </summary>
private async void SendSelection()
{
//si une saisie est faite dans le champs
if (!string.IsNullOrWhiteSpace(Id))
{
string idSaisie = Id.ToLower().Trim();//passage en full minuscule + suppression espace
var path = Path.Combine(Env.WebRootPath, "Photos", idSaisie);
bool dossierExiste = Directory.Exists(path);//verification de présence du dossier
if (dossierExiste)
{
ShootingData datas = ShootingDb.GetInfosShooting(idSaisie);
if (datas == null)
{
await ShootingDb.AddShootingInfoPerDefault(idSaisie);
}
else
{
if (datas.IsPhotosSelected)
{
Navigation.NavigateTo($"/Share?Id={idSaisie}");//redirection vers le shooting post retouche
}
else
{
Navigation.NavigateTo($"/PhotoSelect?Id={idSaisie}");//redirection vers le shooting à selectionné
}
}
}
else
{
Navigation.NavigateTo($"/PhotoSelectError");//redirection vers page d'erreur
}
}
}
}
@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.drawer-fixed-width {
width: 250px; /* largeur fixe */
min-width: 250px; /* ne peut pas rétrécir */
max-width: 250px; /* ne peut pas grandir */
}
.drawer-fixed-width .mud-drawer-content {
overflow-x: hidden; /* empêche le scroll horizontal */
}
.custom-drawer.mud-shrink {
height: 100vh !important; /* prend toute la hauteur de l’écran */
min-height: 100vh !important;
max-height: 100vh !important;
overflow-y: auto; /* scroll uniquement si contenu > hauteur */
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.nav-item {
font-size: 1.1rem;
padding-bottom: 0.5rem;
font-weight: bold;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-top: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: auto;
background-color: rgba(255,255,255,0.1);
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.5);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
@@ -0,0 +1,33 @@
@using MudBlazor
<MudDialog>
<DialogContent>
<MudTextField @bind-Value="SearchTerm"
Label="Recherche"
Variant="Variant.Outlined"
FullWidth="true" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Class="mt-2"
OnClick="Submit">
Rechercher
</MudButton>
</DialogContent>
</MudDialog>
@code {
private string SearchTerm { get; set; } = "";
[Parameter] public EventCallback<string> OnSearch { get; set; }
[CascadingParameter] IMudDialogInstance Dialog { get; set; }
private async Task Submit()
{
if (OnSearch.HasDelegate)
await OnSearch.InvokeAsync(SearchTerm);
Dialog?.Close(DialogResult.Ok(SearchTerm)); // IMPORTANT
}
}
@@ -0,0 +1,12 @@
.drawer-fixed-width {
width: 250px;
min-width: 250px;
max-width: 250px;
height: 100vh;
display: flex;
flex-direction: column;
}
.custom-drawer .mud-drawer-content {
overflow-y: auto;
}
@@ -0,0 +1,103 @@
@page "/Contact"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using ModelsLib
@inject EmailService mailService
@inject IJSRuntime JS
@rendermode InteractiveServer
@inject NavigationManager Navigation
<!-- Contact -->
<section class="contact-section">
<MudContainer MaxWidth="MaxWidth.Small" Class="text-center">
<!-- Titre -->
<MudText Typo="Typo.h5" Class="mb-2">
Contact
</MudText>
<!-- Sous-titre -->
<MudText Typo="Typo.body1" Class="mb-6">
Une question ? Un projet ? Envie d'une collaboration ? Écrivez-moi directement.
</MudText>
<!-- Formulaire -->
<EditForm EditContext="@editContext" OnValidSubmit="EnvoyerMessage">
<DataAnnotationsValidator />
<ValidationSummary />
<MudStack Spacing="2">
<MudTextField @bind-Value="contact.Name"
Label="Votre nom"
Variant="Variant.Outlined"
Required="true" />
<MudTextField @bind-Value="contact.Email"
Label="Votre adresse mail"
Variant="Variant.Outlined"
Required="true" />
<MudTextField @bind-Value="contact.Title"
Label="Sujet du message"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="contact.Message"
Label="Message"
Variant="Variant.Outlined"
Lines="6"
TextArea="true" />
<MudButton ButtonType="ButtonType.Submit"
Variant="Variant.Filled"
Color="Color.Primary"
Class="mt-4">
Laisser un message
</MudButton>
</MudStack>
</EditForm>
<!-- Message succès -->
@if (messageEnvoye)
{
<MudAlert Severity="Severity.Success" Class="mt-6">
✅ Message envoyé, merci !
</MudAlert>
}
</MudContainer>
</section>
@code {
#region properties
private ContactModel contact = new();
private EditContext? editContext;
private bool messageEnvoye = false;
#endregion
/// <summary>
/// Chargement de la page
/// </summary>
protected override async Task OnInitializedAsync()
{
editContext = new EditContext(contact);
}
/// <summary>
/// Méthode d'envoi de mail
/// </summary>
private void EnvoyerMessage()
{
var body = new StringBuilder();
body.Append("<p>").Append(contact.Message).Append("</p>");
mailService.EnvoyerMailContactAsync(contact.Email,contact.Name+" : " +contact.Title, body).GetAwaiter();
messageEnvoye = true;
contact = new ContactModel();
editContext = new EditContext(contact);
}
}
@@ -0,0 +1,61 @@
body {
}
.contact-section {
padding: 80px 20px;
background: #f4f4f4;
}
.contact-title {
font-size: 40px;
font-weight: 700;
margin-bottom: 10px;
}
.contact-subtitle {
color: #777;
margin-bottom: 50px;
}
.contact-form {
max-width: 800px;
margin: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.contact-input {
padding: 15px;
border: 1px solid #ddd;
font-size: 16px;
width: 100%;
}
.contact-textarea {
padding: 15px;
border: 1px solid #ddd;
font-size: 16px;
width: 100%;
min-height: 180px;
}
.contact-button {
width: 250px;
padding: 15px;
border: 2px solid #222;
background: white;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.contact-button:hover {
background: #222;
color: white;
}
.success-message {
margin-top: 20px;
color: green;
font-weight: 500;
}
@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
@@ -0,0 +1,83 @@
@page "/"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using ModelsLib
@using ToolsPhotoShare
@inject EmailService mailService
@inject IJSRuntime JS
@inject ConfigAppServices _configDb
@rendermode InteractiveServer
@using Microsoft.EntityFrameworkCore;
@inject NavigationManager Navigation
<PageTitle>Accueil</PageTitle>
<main class="home-container">
<!-- Hero -->
<section class="hero-section text-center">
<div class="badge">Photographe Amateur</div>
<h1 class="hero-title">Hello,<br /><span class="italic">Moi c'est @DataPage.TitlePseudo</span></h1>
<p class="hero-subtitle">
@DataPage.SubTitle1
</p>
<p class="hero-subtitle">
@DataPage.SubTitle2
</p>
</section>
<section class="social-section text-center">
<div class="social-buttons">
<NavLink href="/ShootingDesire">
<h2 class="social-title">Mes envies de shooting</h2>
</NavLink>
</div>
</section>
<!-- Social -->
<section class="social-section text-center">
<h2 class="social-title">Où me retrouver ?</h2>
<p class="social-subtitle">Suivez mon travail et mes derniers shootings</p>
<div class="social-buttons">
<a href="@DataPage.InstaLink">
<div class="qr-wrapper">
<img src="@DataPage.InstaQrCode" alt="QR Code Instagram" class="qr-image" />
</div>
</a>
</div>
</section>
</main>
<footer class="footer">
<div class="footer-line"></div>
<p>© 2026 @DataPage.Credentials — Droits réservés</p>
</footer>
@code {
#region properties
private EntityHome DataPage = new();
#endregion
/// <summary>
/// Chargement de la page
/// </summary>
protected override async Task OnInitializedAsync()
{
if (!await _configDb.HasAnyParametersAsync())
{
await _configDb.FillConfigTableWithDataPerDefault();
}
DataPage = await _configDb.readParametersApplication();
}
}
@@ -0,0 +1,209 @@
/* --- Containers --- */
.home-container {
width: 90%; /* occupe 90% de l’écran sur petits écrans */
max-width: 800px; /* limite la largeur sur grands écrans */
margin: 0 auto;
padding: 3rem 1.5rem;
display: flex;
flex-direction: column;
gap: 3rem;
overflow-y: scroll; /* Permet le scroll vertical */
scrollbar-width: none; /* Firefox */
}
.home-container::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}
/* --- Hero --- */
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
border-radius: 9999px;
border: 1px solid #d1d5db;
color: #71717a;
}
.hero-title {
font-family: 'Playfair Display', serif;
font-size: 2.5rem;
font-weight: 500;
}
.hero-subtitle {
color: #71717a;
font-weight: 300;
line-height: 1.6;
}
.hero-image-wrapper {
position: relative;
margin-top: 3rem;
border-radius: 1rem;
overflow: hidden;
}
.hero-image {
width: 100%;
height: auto;
display: block;
}
.hero-overlay {
position: absolute;
inset: 0;
background: rgba(255,255,255,0.4);
pointer-events: none;
border-radius: 1rem;
}
.dark .hero-overlay {
background: rgba(18,18,18,0.7);
}
/* --- Social --- */
.social-title {
font-family: 'Playfair Display', serif;
font-size: 1.75rem;
font-style: italic;
}
.social-subtitle {
color: #71717a;
font-weight: 300;
}
.social-buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.instagram-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 2rem;
border: 1px solid #d1d5db;
border-radius: 9999px;
background: white;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.instagram-button:hover {
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.qr-wrapper {
padding: 1.5rem;
background: white;
border: 1px solid #d1d5db;
border-radius: 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
.qr-image {
width: 128px;
height: 128px;
}
.qr-text {
margin-top: 0.5rem;
font-size: 0.625rem;
color: #71717a;
letter-spacing: 0.05em;
text-transform: uppercase;
font-weight: 600;
}
/* --- Contact --- */
.contact-title {
font-family: 'Playfair Display', serif;
font-size: 2rem;
}
.contact-subtitle {
color: #71717a;
font-weight: 300;
margin-bottom: 1.5rem;
}
.contact-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.contact-input, .contact-textarea {
border: none;
border-bottom: 1px solid #d1d5db;
padding: 0.75rem 0;
font-size: 1rem;
outline: none;
background: transparent;
color: #1a1a1a;
}
.contact-input::placeholder, .contact-textarea::placeholder {
color: #d1d5db;
}
.contact-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1.5rem;
border-radius: 9999px;
background: #1a1a1a;
color: white;
font-weight: 500;
border: none;
cursor: pointer;
margin: 0 auto;
transition: all 0.2s;
}
.contact-button:hover {
opacity: 0.9;
}
.success-message {
color: green;
font-weight: 500;
}
/* --- Footer --- */
.footer {
text-align: center;
color: #71717a;
font-size: 0.625rem;
text-transform: uppercase;
letter-spacing: 0.1em;
padding-bottom: 3rem;
}
.footer-line {
height: 1px;
width: 3rem;
background: #d1d5db;
margin: 0 auto 1rem auto;
}
/* --- Dark mode toggle --- */
.dark-toggle {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.75rem;
border-radius: 9999px;
background: white;
border: 1px solid #d1d5db;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
@@ -0,0 +1,9 @@
@page "/PhotoSelectError"
<h3>Oups !!!</h3>
Il semblerait que ce shooting soit inexistant ou bien que cet recherche est infructueuse.
@code {
}
@@ -0,0 +1,154 @@
@page "/Share"
@rendermode InteractiveServer
@using BlazorBootstrap;
@using Microsoft.AspNetCore.WebUtilities
@using ModelsLib
@using PhotoShareHelri.Components.Blocs
@using System.Text
@inject NavigationManager NavManager
@inject EmailService mailService
@inject PhotoService ShootingDb
@inject IJSRuntime JS
@using MudBlazor
<MudContainer MaxWidth="MaxWidth.Large" Class="d-flex justify-center mt-6 mb-6">
<MudPaper Elevation="3" Class="pa-6 selection-wrapper">
<!-- Header -->
<MudStack Spacing="1" AlignItems="AlignItems.Center" Class="mb-4 text-center">
<MudText Typo="Typo.h4">Résultat du shooting</MudText>
<MudText Typo="Typo.h6" Class="title-shoot">@Title</MudText>
</MudStack>
<!-- Galerie -->
<MudGrid Justify="Justify.Center" Spacing="3">
@for (int index = 0; index < Images.Count; index++)
{
var img = Images[index];
<MudItem xs="12" sm="6" md="4" lg="3" xl="2">
<MudPaper Class="image-card pa-2 position-relative" Elevation="2">
<MudImage Src="@img"
Alt="Photo sélection"
Class="gallery-thumb"
Fluid="true"
@onclick="() => OpenModalZoom(img)" />
</MudPaper>
</MudItem>
}
</MudGrid>
</MudPaper>
</MudContainer>
@* Modal de zoom sur une image *@
<ModalDiapo IsOpen="@IsModalZoomOpen"
CurrentImage="@CurrentImage"
CurrentIndex="@CurrentIndex"
IsSelector=false
Total="@Images.Count"
CanNext="@CanNext"
CanPrevious="@CanPrevious"
MaxSelect="@MaxSelect"
SelectedImages="@SelectedImages"
IsSelected="@(CurrentImage != null && @SelectedImages.Contains(@CurrentImage))"
OnClose="CloseModalZoom"
OnNext="NextModalZoom"
OnPrevious="PreviousModalZoom"
Toggle="() => ToggleImage(CurrentImage!)" />
@code {
#region Variables
HashSet<string> SelectedImages = new();
private int MaxSelect = 1;
// Liste des images disponibles
private List<string> Images = new();
public string Title { get; set; }
bool IsModalZoomOpen = false;
string? CurrentImage;
int CurrentIndex =>
CurrentImage == null ? -1 : Images.IndexOf(CurrentImage);
bool CanNext => CurrentIndex >= 0 && CurrentIndex < Images.Count - 1;
bool CanPrevious => CurrentIndex > 0;
private string ShootingId;
#endregion
#region modal zoom
/// <summary>
/// Ouverture de la modal à partir du clic sur image voulu
/// </summary>
void OpenModalZoom(string img)
{
CurrentImage = img;
IsModalZoomOpen = true;
}
///<summary>
/// fermeture de la modal
/// </summary>
void CloseModalZoom()
{
IsModalZoomOpen = false;
}
/// <summary>
/// photo suivante dans la modal
/// dans la liste des photos affichés
/// </summary>
void NextModalZoom()
{
if (CanNext)
CurrentImage = Images[CurrentIndex + 1];
}
/// <summary>
/// photo précédente
/// </summary>
void PreviousModalZoom()
{
if (CanPrevious)
CurrentImage = Images[CurrentIndex - 1];
}
#endregion
/// <summary>
/// arrivé sur la page
/// </summary>
protected override async Task OnInitializedAsync()
{
//récupération de l'identifiant de shooting, si identifiant null ou si recherche inexistante redirection vers la page de recherche de shooting
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("Id", out var id))
{
if (!string.IsNullOrEmpty(id))
{
ShootingId = id;
Images = ShootingDb.LireContenu(ShootingId, true);
}
}
// Code exécuté au chargement du composant
}
/// <summary>
/// Déclenche le téléchargement de l'image originale sur la modal de visualisation
/// </summary>
/// <param name="img">image sélectionné par son hash</param>
private async Task ToggleImage(string img)
{
var filePath = $"{img}"; // chemin dans wwwroot
await JS.InvokeVoidAsync("downloadFile", filePath, $"{img}");
}
}
@@ -0,0 +1,80 @@
.selection-wrapper {
width: 100%;
max-width: 1400px;
border-radius: 20px;
}
.title-shoot {
opacity: 0.85;
}
.image-card {
border-radius: 16px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
background-color: white;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.gallery-thumb {
width: 100%;
height: 250px;
object-fit: cover;
border-radius: 12px;
cursor: pointer;
display: block;
}
.select-fab {
position: absolute !important;
top: 14px;
right: 14px;
z-index: 10;
}
/* Mobile */
@media (max-width: 768px) {
.selection-wrapper {
padding: 20px 12px !important;
border-radius: 14px;
}
.gallery-thumb {
height: 220px;
}
}
.select-icon-btn {
position: absolute !important;
top: 12px;
right: 12px;
z-index: 10;
background-color: rgba(255, 255, 255, 0.92) !important;
backdrop-filter: blur(4px);
border-radius: 50%;
box-shadow: 0 4px 12px rgba(0,0,0,0.18);
transition: all 0.2s ease;
}
.select-icon-btn:hover {
transform: scale(1.08);
}
.select-icon-btn.selected {
border: 2px solid #2e7d32;
color: #2e7d32 !important;
}
.select-icon-btn.not-selected {
border: 2px solid #c62828;
color: #c62828 !important;
}
.select-icon-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@@ -0,0 +1,224 @@
@page "/PhotoSelect"
@rendermode InteractiveServer
@using BlazorBootstrap;
@using Microsoft.AspNetCore.WebUtilities
@using ModelsLib
@using PhotoShareHelri.Components.Blocs
@using System.Text
@inject NavigationManager NavManager
@inject EmailService mailService
@inject PhotoService ShootingDb
@inject IDialogService DialogService
@using MudBlazor
<MudContainer MaxWidth="MaxWidth.Large" Class="d-flex justify-center mt-6 mb-6">
<MudPaper Elevation="3" Class="pa-6 selection-wrapper">
<!-- Header -->
<MudStack Spacing="1" AlignItems="AlignItems.Center" Class="mb-4 text-center">
<MudText Typo="Typo.h4">Sélection des photos</MudText>
<MudText Typo="Typo.h6" Class="title-shoot">@Title</MudText>
</MudStack>
<!-- Action top -->
<MudStack AlignItems="AlignItems.Center" Class="mb-3">
<MudButton Variant="Variant.Outlined"
Color="Color.Dark"
Disabled="@(SelectedImages.Count == 0)"
OnClick="SendSelection">
Valider la sélection
</MudButton>
</MudStack>
<!-- Compteur -->
<MudText Typo="Typo.body1" Align="Align.Center" Class="mb-4">
@SelectedImages.Count / @MaxSelect photo(s) sélectionnée(s)
</MudText>
<!-- Galerie -->
<MudGrid Justify="Justify.Center" Spacing="3">
@for (int index = 0; index < Images.Count; index++)
{
var img = Images[index];
var isSelected = SelectedImages.Contains(img);
var buttonClass = $"select-icon-btn {(isSelected ? "selected" : "not-selected")}";
<MudItem xs="12" sm="6" md="4" lg="3" xl="2">
<MudPaper Class="image-card pa-2 position-relative" Elevation="2">
<MudImage Src="@img"
Alt="Photo sélection"
Class="gallery-thumb"
Fluid="true"
@onclick="() => OpenModalZoom(img)" />
<div @onclick:stopPropagation="true">
<MudIconButton Icon="@(isSelected? Icons.Material.Filled.Check : Icons.Material.Outlined.RadioButtonUnchecked)"
Class="@buttonClass"
Disabled="@(SelectedImages.Count >= MaxSelect && !isSelected)"
OnClick="() => ToggleImage(img)" />
</div>
</MudPaper>
</MudItem>
}
</MudGrid>
<!-- Action bottom -->
<MudStack AlignItems="AlignItems.Center" Class="mt-5">
<MudButton Variant="Variant.Outlined"
Disabled="@(SelectedImages.Count == 0)"
Color="Color.Dark"
OnClick="SendSelection">
Valider la sélection
</MudButton>
</MudStack>
</MudPaper>
</MudContainer>
<ModalDiapo IsOpen="@IsModalZoomOpen"
CurrentImage="@CurrentImage"
CurrentIndex="@CurrentIndex"
IsSelector="true"
Total="@Images.Count"
CanNext="@CanNext"
CanPrevious="@CanPrevious"
MaxSelect="@MaxSelect"
SelectedImages="@SelectedImages"
IsSelected="@(CurrentImage != null && SelectedImages.Contains(CurrentImage))"
OnClose="CloseModalZoom"
OnNext="NextModalZoom"
OnPrevious="PreviousModalZoom"
Toggle="() => ToggleImage(CurrentImage!)" />
@code {
#region Variables
HashSet<string> SelectedImages = new();
private int MaxSelect = 1;
// Liste des images disponibles
private List<string> Images = new();
public string Title { get; set; }
bool IsModalZoomOpen = false;
string? CurrentImage;
int CurrentIndex =>
CurrentImage == null ? -1 : Images.IndexOf(CurrentImage);
bool CanNext => CurrentIndex >= 0 && CurrentIndex < Images.Count - 1;
bool CanPrevious => CurrentIndex > 0;
private string ShootingId;
private ShootingData Datas { get; set; }
#endregion
#region modal zoom
/// <summary>
/// Ouverture de la modal à partir du clic sur image voulu
/// </summary>
void OpenModalZoom(string img)
{
CurrentImage = img;
IsModalZoomOpen = true;
}
///<summary>
/// fermeture de la modal
/// </summary>
void CloseModalZoom()
{
IsModalZoomOpen = false;
}
/// <summary>
/// photo suivante dans la modal
/// dans la liste des photos affichés
/// </summary>
void NextModalZoom()
{
if (CanNext)
CurrentImage = Images[CurrentIndex + 1];
}
/// <summary>
/// photo précédente
/// </summary>
void PreviousModalZoom()
{
if (CanPrevious)
CurrentImage = Images[CurrentIndex - 1];
}
#endregion
/// <summary>
/// arrivé sur la page
/// </summary>
protected override async Task OnInitializedAsync()
{
//récupération de l'identifiant de shooting, si identifiant null ou si recherche inexistante redirection vers la page de recherche de shooting
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("Id", out var id))
{
Datas = ShootingDb.GetInfosShooting(id);
if (!string.IsNullOrEmpty(id))
{
ShootingId = id;
Images = ShootingDb.LireContenu(ShootingId);
}
Title = "Résultat du shooting " + Datas.Key;
}
}
/// <summary>
/// Séléction de l'image dans la liste des photos choisies
/// </summary>
/// <param name="img">image sélectionné par son hash</param>
private void ToggleImage(string img)
{
if (SelectedImages.Contains(img))
{
SelectedImages.Remove(img);
}
else
{
SelectedImages.Add(img);
}
}
/// <summary>
/// envoi par mail de la sélection des photos
/// </summary>
private async void SendSelection()
{
if (SelectedImages == null || SelectedImages.Count == 0)
return;
var fichiers = string.Join(";", SelectedImages)
.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(Path.GetFileName)
.ToList();
var body = new StringBuilder();
body.Append("<h3>Photos sélectionnées 📸</h3>");
body.Append("<ul>");
foreach (var fichier in fichiers)
{
body.Append($"<li>{fichier}</li>");
}
body.Append("</ul>");
mailService.EnvoyerMailAsync("Confirmation sélection du shooting" + ShootingId, body).GetAwaiter();
Datas.IsPhotosSelected = true;
await ShootingDb.UpdateShootingInfo(Datas);
}
}
@@ -0,0 +1,80 @@
.selection-wrapper {
width: 100%;
max-width: 1400px;
border-radius: 20px;
}
.title-shoot {
opacity: 0.85;
}
.image-card {
border-radius: 16px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
background-color: white;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.gallery-thumb {
width: 100%;
height: 250px;
object-fit: cover;
border-radius: 12px;
cursor: pointer;
display: block;
}
.select-fab {
position: absolute !important;
top: 14px;
right: 14px;
z-index: 10;
}
/* Mobile */
@media (max-width: 768px) {
.selection-wrapper {
padding: 20px 12px !important;
border-radius: 14px;
}
.gallery-thumb {
height: 220px;
}
}
.select-icon-btn {
position: absolute !important;
top: 12px;
right: 12px;
z-index: 10;
background-color: rgba(255, 255, 255, 0.92) !important;
backdrop-filter: blur(4px);
border-radius: 50%;
box-shadow: 0 4px 12px rgba(0,0,0,0.18);
transition: all 0.2s ease;
}
.select-icon-btn:hover {
transform: scale(1.08);
}
.select-icon-btn.selected {
border: 2px solid #2e7d32;
color: #2e7d32 !important;
}
.select-icon-btn.not-selected {
border: 2px solid #c62828;
color: #c62828 !important;
}
.select-icon-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@@ -0,0 +1,58 @@
@page "/ShootingDesire"
@rendermode InteractiveServer
@inject IWebHostEnvironment Env
@inject DesireService DesireDb
<PageTitle>Mes Envies de shooting futur</PageTitle>
<main class="home-container">
<section class="shooting-section">
<MudContainer MaxWidth="MaxWidth.Medium" Class="text-center">
<!-- Titre -->
<MudText Typo="Typo.h4" Class="mb-4">
Mes envies de shootings
</MudText>
<!-- Intro -->
<MudText Typo="Typo.body1" Class="mb-6">
Envie de partager un moment ensemble ?
Un de ces projets vous parle et vous souhaiteriez qu'on le réalise ensemble ?
Contactez-moi afin que l'on puisse en discuter et mettre
tout en œuvre pour la réalisation !
</MudText>
<!-- Cartes -->
<MudGrid Justify="Justify.Center" Spacing="3">
<MudItem xs="12" sm="8" md="6">
<MudPaper Class="pa-4 text-center" Elevation="2">
@foreach (var envie in EnviesShooting)
{
<MudText Typo="Typo.body1">
📷 @envie
</MudText>
}
<!-- Texte bas -->
<MudText Typo="Typo.body2" Class="mt-6">
Et bien d'autres encore !!!
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
</main>
@code {
private List<string> EnviesShooting;
protected override void OnInitialized()
{
EnviesShooting = DesireDb.GetDEsireShooting();
}
}
@@ -0,0 +1,133 @@
.social-title {
font-family: 'Playfair Display', serif;
font-size: 1.75rem;
font-style: italic;
}
/* --- Containers --- */
.home-container {
width: 90%; /* occupe 90% de l’écran sur petits écrans */
max-width: 800px; /* limite la largeur sur grands écrans */
margin: 0 auto;
padding: 3rem 1.5rem;
display: flex;
flex-direction: column;
gap: 3rem;
overflow-y: scroll; /* Permet le scroll vertical */
scrollbar-width: none; /* Firefox */
}
.home-container::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}
.social-subtitle {
color: #71717a;
font-weight: 300;
}
.social-buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.instagram-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 2rem;
border: 1px solid #d1d5db;
.title {
margin-top: 40px;
margin-bottom: 20px;
}
.intro {
max-width: 800px;
margin: auto;
margin-bottom: 40px;
}
.shooting-container {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(220px,1fr));
gap: 20px;
max-width: 900px;
margin: auto;
}
.shooting-card {
background: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 6px 15px rgba(0,0,0,0.15);
transition: transform 0.2s, box-shadow 0.2s;
font-weight: 500;
}
.shooting-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0,0,0,0.25);
}
border-radius: 9999px;
background: white;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.instagram-button:hover {
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.qr-wrapper {
padding: 1.5rem;
background: white;
border: 1px solid #d1d5db;
border-radius: 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
.qr-image {
width: 128px;
height: 128px;
}
.qr-text {
margin-top: 0.5rem;
font-size: 0.625rem;
color: #71717a;
letter-spacing: 0.05em;
text-transform: uppercase;
font-weight: 600;
}
.gallery-wrapper {
display: flex;
flex-direction: column;
}
.header-selection {
display: flex;
align-items: center;
width: 60%;
}
.titles {
flex: 1;
text-align: center;
}
.shooting-box {
border: 2px solid #444;
border-radius: 12px;
padding: 20px;
max-width: 500px;
margin: auto;
background-color: #f8f8f8;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
+11
View File
@@ -0,0 +1,11 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using PhotoShareHelri
@using PhotoShareHelri.Components
@using MudBlazor