nice calendar !
This commit is contained in:
parent
3f30729de9
commit
9008ba2c18
@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 5.2.17 on 2026-09-25 15:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0003_event_visibility'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='linked_user',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='user',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='visibility',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=2, editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='is_public',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,33 +1,23 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class Event(models.Model):
|
class Event(models.Model):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
user = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
User,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='created_events',
|
related_name='created_events',
|
||||||
)
|
editable=False,
|
||||||
linked_user = models.ForeignKey(
|
|
||||||
User,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='linked_events',
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
)
|
)
|
||||||
start_date = models.DateField()
|
start_date = models.DateField()
|
||||||
end_date = models.DateField()
|
end_date = models.DateField()
|
||||||
start_time = models.TimeField(null=True, blank=True)
|
start_time = models.TimeField(null=True, blank=True)
|
||||||
end_time = models.TimeField(null=True, blank=True)
|
end_time = models.TimeField(null=True, blank=True)
|
||||||
color = models.CharField(max_length=7, null=True, blank=True) # Hex color code
|
color = models.CharField(max_length=7, null=True, blank=True)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
visibility = models.CharField(max_length=10, default='public', choices=[
|
is_public = models.BooleanField(default=True)
|
||||||
('public', 'Public'),
|
|
||||||
('private', 'Private'),
|
|
||||||
])
|
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|||||||
@ -1,91 +1,54 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Event
|
from .models import Event
|
||||||
from users.serializers import UserSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(serializers.ModelSerializer):
|
class EventSerializer(serializers.ModelSerializer):
|
||||||
user = UserSerializer(read_only=True)
|
|
||||||
linked_user = UserSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
'id', 'created_by', 'start_date', 'end_date',
|
||||||
'user',
|
'start_time', 'end_time', 'color', 'name',
|
||||||
'linked_user',
|
'description', 'is_public', 'created_at', 'updated_at',
|
||||||
'start_date',
|
|
||||||
'end_date',
|
|
||||||
'start_time',
|
|
||||||
'end_time',
|
|
||||||
'color',
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'visibility',
|
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
|
||||||
]
|
|
||||||
read_only_fields = [
|
|
||||||
'id',
|
|
||||||
'user',
|
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
|
||||||
]
|
]
|
||||||
|
read_only_fields = ['id', 'created_by', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
|
||||||
class EventCreateSerializer(serializers.ModelSerializer):
|
class EventCreateSerializer(serializers.ModelSerializer):
|
||||||
color = serializers.RegexField(r'^#[0-9a-fA-F]{6}$', required=False, allow_null=True, allow_blank=True)
|
color = serializers.RegexField(
|
||||||
|
r'^#[0-9a-fA-F]{6}$', required=False,
|
||||||
|
allow_null=True, allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
fields = [
|
fields = [
|
||||||
'linked_user',
|
'start_date', 'end_date', 'start_time', 'end_time',
|
||||||
'start_date',
|
'color', 'name', 'description', 'is_public',
|
||||||
'end_date',
|
|
||||||
'start_time',
|
|
||||||
'end_time',
|
|
||||||
'color',
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'visibility',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
request = self.context.get('request')
|
||||||
|
if not getattr(getattr(request, 'user', None), 'is_admin', False):
|
||||||
|
# En création comme en PATCH, les événements non-admin restent privés.
|
||||||
|
attrs['is_public'] = False
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
field: attrs.get(field, getattr(self.instance, field, None))
|
field: attrs.get(field, getattr(self.instance, field, None))
|
||||||
for field in ('start_date', 'end_date', 'start_time', 'end_time')
|
for field in ('start_date', 'end_date', 'start_time', 'end_time')
|
||||||
}
|
}
|
||||||
|
if values['start_date'] is None or values['end_date'] is None:
|
||||||
|
raise serializers.ValidationError('Les dates de début et de fin sont requises.')
|
||||||
if values['end_date'] < values['start_date']:
|
if values['end_date'] < values['start_date']:
|
||||||
raise serializers.ValidationError({'end_date': 'La fin doit suivre le début.'})
|
raise serializers.ValidationError({'end_date': 'La fin doit suivre le début.'})
|
||||||
if (values['start_date'] == values['end_date']
|
if (
|
||||||
and values['start_time'] is not None and values['end_time'] is not None
|
values['start_date'] == values['end_date']
|
||||||
and values['end_time'] <= values['start_time']):
|
and values['start_time'] is not None
|
||||||
|
and values['end_time'] is not None
|
||||||
|
and values['end_time'] <= values['start_time']
|
||||||
|
):
|
||||||
raise serializers.ValidationError({'end_time': 'La fin doit suivre le début.'})
|
raise serializers.ValidationError({'end_time': 'La fin doit suivre le début.'})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
request = self.context.get('request')
|
|
||||||
|
|
||||||
if request is None or not request.user.is_authenticated:
|
|
||||||
raise serializers.ValidationError({
|
|
||||||
'user': 'Un utilisateur authentifié est requis.'
|
|
||||||
})
|
|
||||||
|
|
||||||
validated_data['user'] = request.user
|
|
||||||
|
|
||||||
return Event.objects.create(**validated_data)
|
|
||||||
|
|
||||||
|
|
||||||
class EventUpdateSerializer(EventCreateSerializer):
|
class EventUpdateSerializer(EventCreateSerializer):
|
||||||
class Meta:
|
pass
|
||||||
model = Event
|
|
||||||
fields = [
|
|
||||||
'linked_user',
|
|
||||||
'start_date',
|
|
||||||
'end_date',
|
|
||||||
'start_time',
|
|
||||||
'end_time',
|
|
||||||
'color',
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'visibility',
|
|
||||||
]
|
|
||||||
|
|||||||
@ -1,66 +1,68 @@
|
|||||||
|
from django.db.models import Q
|
||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .models import Event
|
from .models import Event
|
||||||
from .serializers import EventSerializer, EventCreateSerializer, EventUpdateSerializer
|
from .serializers import EventSerializer, EventCreateSerializer, EventUpdateSerializer
|
||||||
|
|
||||||
|
|
||||||
class EventListView(generics.ListCreateAPIView):
|
class IsEventCreatorOrReadOnly(BasePermission):
|
||||||
|
message = 'Seul le créateur peut modifier ou supprimer cet événement.'
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return request.method in SAFE_METHODS or obj.created_by_id == request.user.pk
|
||||||
|
|
||||||
|
|
||||||
|
class VisibleEventsMixin:
|
||||||
|
def get_queryset(self):
|
||||||
|
return Event.objects.filter(
|
||||||
|
Q(is_public=True) | Q(created_by=self.request.user)
|
||||||
|
).order_by('start_date', 'start_time', 'pk')
|
||||||
|
|
||||||
|
|
||||||
|
class EventListView(VisibleEventsMixin, generics.ListCreateAPIView):
|
||||||
serializer_class = EventSerializer
|
serializer_class = EventSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user
|
|
||||||
return Event.objects.filter(user=user).select_related('user', 'linked_user')
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
serializer.save(user=self.request.user)
|
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = EventCreateSerializer(data=request.data, context=self.get_serializer_context())
|
serializer = EventCreateSerializer(
|
||||||
|
data=request.data, context=self.get_serializer_context(),
|
||||||
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
event = serializer.save(user=request.user)
|
event = serializer.save(created_by=request.user)
|
||||||
return Response(EventSerializer(event).data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
EventSerializer(event, context=self.get_serializer_context()).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventDetailView(generics.RetrieveUpdateDestroyAPIView):
|
class EventDetailView(VisibleEventsMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
serializer_class = EventSerializer
|
serializer_class = EventSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, IsEventCreatorOrReadOnly]
|
||||||
lookup_url_kwarg = 'id'
|
lookup_url_kwarg = 'id'
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user
|
|
||||||
return Event.objects.filter(user=user).select_related('user', 'linked_user')
|
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
partial = kwargs.pop('partial', False)
|
partial = kwargs.pop('partial', False)
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
|
|
||||||
# Use the update serializer for updates
|
|
||||||
serializer = EventUpdateSerializer(
|
serializer = EventUpdateSerializer(
|
||||||
instance,
|
instance, data=request.data, partial=partial,
|
||||||
data=request.data,
|
|
||||||
partial=partial,
|
|
||||||
context=self.get_serializer_context(),
|
context=self.get_serializer_context(),
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
self.perform_update(serializer)
|
self.perform_update(serializer)
|
||||||
|
return Response(
|
||||||
return Response(EventSerializer(serializer.instance).data)
|
EventSerializer(serializer.instance, context=self.get_serializer_context()).data,
|
||||||
|
)
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
self.perform_destroy(self.get_object())
|
||||||
self.perform_destroy(instance)
|
|
||||||
return Response(
|
return Response(
|
||||||
{'message': 'Event deleted successfully'},
|
{'message': 'Event deleted successfully'},
|
||||||
status=status.HTTP_200_OK
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventCreateView(EventListView):
|
class EventCreateView(EventListView):
|
||||||
http_method_names = ['post', 'options']
|
http_method_names = ['post', 'options']
|
||||||
serializer_class = EventCreateSerializer
|
serializer_class = EventCreateSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
serializer.save(user=self.request.user)
|
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { eventApi } from '../api/event';
|
import { eventApi } from '../api/event';
|
||||||
|
import { useAuth } from '../context/useAuth';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
||||||
@ -104,6 +105,12 @@ const EVENT_LANE_GAP = 11;
|
|||||||
const EVENT_LEFT_PADDING = 3;
|
const EVENT_LEFT_PADDING = 3;
|
||||||
|
|
||||||
const CalendarPage = () => {
|
const CalendarPage = () => {
|
||||||
|
const { user, isAdmin } = useAuth();
|
||||||
|
const canManageEvent = (event) => {
|
||||||
|
const creatorId = event?.created_by?.id ?? event?.created_by;
|
||||||
|
return user?.id != null && creatorId != null &&
|
||||||
|
String(creatorId) === String(user.id);
|
||||||
|
};
|
||||||
const [requestError, setRequestError] = useState('');
|
const [requestError, setRequestError] = useState('');
|
||||||
const [year, setYear] = useState(
|
const [year, setYear] = useState(
|
||||||
new Date().getFullYear(),
|
new Date().getFullYear(),
|
||||||
@ -163,6 +170,7 @@ const CalendarPage = () => {
|
|||||||
start_time: '',
|
start_time: '',
|
||||||
end_time: '',
|
end_time: '',
|
||||||
color: DEFAULT_EVENT_COLOR,
|
color: DEFAULT_EVENT_COLOR,
|
||||||
|
is_public: Boolean(isAdmin),
|
||||||
});
|
});
|
||||||
const calendarViewportRef = useRef(null);
|
const calendarViewportRef = useRef(null);
|
||||||
/* SÉLECTION D'UNE PÉRIODE */
|
/* SÉLECTION D'UNE PÉRIODE */
|
||||||
@ -631,12 +639,14 @@ const CalendarPage = () => {
|
|||||||
start_time: '',
|
start_time: '',
|
||||||
end_time: '',
|
end_time: '',
|
||||||
color: DEFAULT_EVENT_COLOR,
|
color: DEFAULT_EVENT_COLOR,
|
||||||
|
is_public: Boolean(isAdmin),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const openCreateDialog = (
|
const openCreateDialog = (
|
||||||
startDate = '',
|
startDate = '',
|
||||||
endDate = startDate,
|
endDate = startDate,
|
||||||
) => {
|
) => {
|
||||||
|
setRequestError('');
|
||||||
setEditingEvent(null);
|
setEditingEvent(null);
|
||||||
setEventForm({
|
setEventForm({
|
||||||
name: '',
|
name: '',
|
||||||
@ -646,6 +656,7 @@ const CalendarPage = () => {
|
|||||||
start_time: '',
|
start_time: '',
|
||||||
end_time: '',
|
end_time: '',
|
||||||
color: DEFAULT_EVENT_COLOR,
|
color: DEFAULT_EVENT_COLOR,
|
||||||
|
is_public: Boolean(isAdmin),
|
||||||
});
|
});
|
||||||
clearSelection();
|
clearSelection();
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
@ -654,6 +665,8 @@ const CalendarPage = () => {
|
|||||||
const openEditDialog = (
|
const openEditDialog = (
|
||||||
event,
|
event,
|
||||||
) => {
|
) => {
|
||||||
|
if (!canManageEvent(event)) return;
|
||||||
|
setRequestError('');
|
||||||
setEditingEvent(event);
|
setEditingEvent(event);
|
||||||
setEventForm({
|
setEventForm({
|
||||||
name: event?.name ?? '',
|
name: event?.name ?? '',
|
||||||
@ -680,6 +693,7 @@ const CalendarPage = () => {
|
|||||||
color:
|
color:
|
||||||
event?.color ||
|
event?.color ||
|
||||||
DEFAULT_EVENT_COLOR,
|
DEFAULT_EVENT_COLOR,
|
||||||
|
is_public: Boolean(isAdmin) && event?.is_public === true,
|
||||||
});
|
});
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
setEventDialogOpen(true);
|
setEventDialogOpen(true);
|
||||||
@ -724,6 +738,7 @@ const CalendarPage = () => {
|
|||||||
submitEvent,
|
submitEvent,
|
||||||
) => {
|
) => {
|
||||||
setRequestError('');
|
setRequestError('');
|
||||||
|
if (savingEvent || (editingEvent && !canManageEvent(editingEvent))) return;
|
||||||
if (!eventForm.name.trim()) {
|
if (!eventForm.name.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -764,6 +779,7 @@ const CalendarPage = () => {
|
|||||||
color:
|
color:
|
||||||
eventForm.color ||
|
eventForm.color ||
|
||||||
DEFAULT_EVENT_COLOR,
|
DEFAULT_EVENT_COLOR,
|
||||||
|
is_public: Boolean(isAdmin) && eventForm.is_public,
|
||||||
};
|
};
|
||||||
if (submitEvent) {
|
if (submitEvent) {
|
||||||
await eventApi.updateEvent(
|
await eventApi.updateEvent(
|
||||||
@ -795,6 +811,7 @@ const CalendarPage = () => {
|
|||||||
event,
|
event,
|
||||||
) => {
|
) => {
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
|
if (!canManageEvent(event)) return;
|
||||||
const confirmed =
|
const confirmed =
|
||||||
window.confirm(
|
window.confirm(
|
||||||
`Supprimer l’événement « ${
|
`Supprimer l’événement « ${
|
||||||
@ -1032,8 +1049,13 @@ const CalendarPage = () => {
|
|||||||
: 'Créer un événement'}
|
: 'Créer un événement'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{contextMenu.type === 'event' && !canManageEvent(contextMenu.event) && (
|
||||||
|
<p className="px-2 py-1.5 text-sm text-muted-foreground">
|
||||||
|
Événement public · Lecture seule
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{contextMenu.type ===
|
{contextMenu.type ===
|
||||||
'event' && (
|
'event' && canManageEvent(contextMenu.event) && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -1255,57 +1277,63 @@ const CalendarPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* COULEUR */}
|
{/* COULEUR ET VISIBILITÉ */}
|
||||||
<div className="grid gap-2">
|
<div className="flex items-start gap-4">
|
||||||
<Label htmlFor="event-color">
|
<div className="grid min-w-0 flex-1 gap-2">
|
||||||
Couleur
|
<Label htmlFor="event-color">Couleur</Label>
|
||||||
</Label>
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<div className="flex items-center gap-3">
|
<input
|
||||||
<input
|
id="event-color"
|
||||||
id="event-color"
|
type="color"
|
||||||
type="color"
|
value={/^#[0-9a-fA-F]{6}$/.test(eventForm.color)
|
||||||
value={
|
? eventForm.color : DEFAULT_EVENT_COLOR}
|
||||||
eventForm.color?.startsWith(
|
onChange={(event) => handleFormChange('color', event.target.value)}
|
||||||
'#',
|
className="h-10 w-14 shrink-0 cursor-pointer rounded-md border border-input bg-background p-1"
|
||||||
)
|
/>
|
||||||
? eventForm.color
|
<Input
|
||||||
: '#3b82f6'
|
aria-label="Code hexadécimal de la couleur"
|
||||||
}
|
value={eventForm.color}
|
||||||
onChange={(
|
onChange={(event) => handleFormChange('color', event.target.value)}
|
||||||
event,
|
placeholder="#2563eb"
|
||||||
) =>
|
className="min-w-0 flex-1"
|
||||||
handleFormChange(
|
/>
|
||||||
'color',
|
</div>
|
||||||
event.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="
|
|
||||||
h-10
|
|
||||||
w-14
|
|
||||||
cursor-pointer
|
|
||||||
rounded-md
|
|
||||||
border
|
|
||||||
border-input
|
|
||||||
bg-background
|
|
||||||
p-1
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
eventForm.color
|
|
||||||
}
|
|
||||||
onChange={(
|
|
||||||
event,
|
|
||||||
) =>
|
|
||||||
handleFormChange(
|
|
||||||
'color',
|
|
||||||
event.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="#3b82f6"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="grid shrink-0 gap-2">
|
||||||
|
<Label htmlFor="event-is-public">Visibilité</Label>
|
||||||
|
<div className="flex h-10 items-center gap-2">
|
||||||
|
<button
|
||||||
|
id="event-is-public"
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-label="Événement public"
|
||||||
|
aria-checked={eventForm.is_public}
|
||||||
|
aria-describedby="event-visibility-help"
|
||||||
|
disabled={savingEvent}
|
||||||
|
onClick={() => setEventForm((current) => ({
|
||||||
|
...current,
|
||||||
|
is_public: !current.is_public,
|
||||||
|
}))}
|
||||||
|
className={`inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 ${eventForm.is_public ? 'bg-primary' : 'bg-input'}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`pointer-events-none block h-5 w-5 rounded-full bg-background shadow-sm transition-transform ${eventForm.is_public ? 'translate-x-5' : 'translate-x-0'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<Label htmlFor="event-is-public" className="w-10 cursor-pointer">
|
||||||
|
{eventForm.is_public ? 'Public' : 'Privé'}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<p id="event-visibility-help" className="text-xs text-muted-foreground">
|
||||||
|
{isAdmin && eventForm.is_public
|
||||||
|
? 'Visible par tous les utilisateurs connectés. Seul toi peux le modifier ou le supprimer.'
|
||||||
|
: 'Visible uniquement par toi dans ton calendrier.'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user