diff --git a/frontend/src/Components/CrudButtons/CrudButtons.module.scss b/frontend/src/Components/CrudButtons/CrudButtons.module.scss index b045c3b3c..d28515170 100644 --- a/frontend/src/Components/CrudButtons/CrudButtons.module.scss +++ b/frontend/src/Components/CrudButtons/CrudButtons.module.scss @@ -5,31 +5,7 @@ .row { display: flex; justify-content: flex-end; - gap: 0.5em; -} - -.crud_button { - display: flex; - justify-content: center; - align-items: center; - border: none; - background-color: $grey-3; - border-radius: 100%; - width: 1.75em; - height: 1.75em; - cursor: pointer; - color: white; - - &:hover { - filter: brightness(105%); - transform: scale(1.1); - } -} - -.red { - background-color: $red; -} - -.blue { - background-color: $blue; + gap: 0.3em; + z-index: 10; + pointer-events: all; } diff --git a/frontend/src/Components/CrudButtons/CrudButtons.stories.tsx b/frontend/src/Components/CrudButtons/CrudButtons.stories.tsx index a9d1893d0..a33a990e1 100644 --- a/frontend/src/Components/CrudButtons/CrudButtons.stories.tsx +++ b/frontend/src/Components/CrudButtons/CrudButtons.stories.tsx @@ -16,6 +16,6 @@ export const Basic: Story = { function onClick() { alert('Hello!'); } - return ; + return ; }, }; diff --git a/frontend/src/Components/CrudButtons/CrudButtons.tsx b/frontend/src/Components/CrudButtons/CrudButtons.tsx index d54b3c8b7..82d5cc7e0 100644 --- a/frontend/src/Components/CrudButtons/CrudButtons.tsx +++ b/frontend/src/Components/CrudButtons/CrudButtons.tsx @@ -9,30 +9,37 @@ type CrudButtonsProps = { onManage?: () => void; onEdit?: () => void; onDelete?: () => void; + height?: string | number; }; -export function CrudButtons({ onView, onEdit, onManage, onDelete }: CrudButtonsProps) { +export function CrudButtons({ onView, onEdit, onManage, onDelete, height }: CrudButtonsProps) { const { t } = useTranslation(); return (
- {onManage && ( - - )} {onView && ( + )} + {onEdit && ( + + )} + {onDelete && ( + + )} + {onManage && ( + )} - {onEdit && } - {onDelete && }
); } diff --git a/frontend/src/Components/EventCrudButtons/EventCrudButtons.module.scss b/frontend/src/Components/EventCrudButtons/EventCrudButtons.module.scss new file mode 100644 index 000000000..8dc2d4332 --- /dev/null +++ b/frontend/src/Components/EventCrudButtons/EventCrudButtons.module.scss @@ -0,0 +1,43 @@ +.edit_icon { + color: white; + filter: brightness(1000%); + transition: all 200ms ease-in-out; +} + +.edit_button { + pointer-events: all; + z-index: 10; + border-radius: 30%; + width: fit-content; + padding: 4px; + color: white; + cursor: pointer; + transition: all 200ms ease-in-out; + text-decoration: none; +} + +.edit_button:hover { + scale: 1.1; + filter: brightness(110%); + + .edit_icon { + rotate: 10deg; + } +} + +.default_edit { + @extend .edit_button; + background: #3498db; +} + +.detail_edit { + @extend .edit_button; + background: teal; +} + +.delete_edit { + @extend .edit_button; + background: crimson; + appearance: none; + border: none; +} diff --git a/frontend/src/Components/EventCrudButtons/EventCrudButtons.stories.tsx b/frontend/src/Components/EventCrudButtons/EventCrudButtons.stories.tsx new file mode 100644 index 000000000..03c150d0d --- /dev/null +++ b/frontend/src/Components/EventCrudButtons/EventCrudButtons.stories.tsx @@ -0,0 +1,28 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import { EventCrudButtons } from './EventCrudButtons'; + +const flexDecorator: Decorator = (Story) => ( +
+ +
+); + +const meta: Meta = { + title: 'Components/EventCrudButtons', + component: EventCrudButtons, + decorators: [flexDecorator], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + is_staff_overwrite: true, + id: '123', + title: 'Test event', + height: undefined, + have_view: true, + }, +}; diff --git a/frontend/src/Components/EventCrudButtons/EventCrudButtons.tsx b/frontend/src/Components/EventCrudButtons/EventCrudButtons.tsx new file mode 100644 index 000000000..d4eb6a0f6 --- /dev/null +++ b/frontend/src/Components/EventCrudButtons/EventCrudButtons.tsx @@ -0,0 +1,63 @@ +import type { ReactNode } from 'react'; +import { deleteEvent } from '~/api'; +import { useAuthContext } from '~/context/AuthContext'; +import { useCustomNavigate } from '~/hooks'; +import { reverse } from '~/named-urls'; +import { PERM } from '~/permissions'; +import { ROUTES } from '~/routes'; +import { hasPerm } from '~/utils'; +import { CrudButtons } from '../CrudButtons'; + +type EventCrudButtons = { + title?: ReactNode; + id?: string; + is_staff_overwrite?: boolean; + have_view?: boolean; + height?: string | number; +}; + +export function EventCrudButtons({ + title = 'event', + id, + is_staff_overwrite = false, + have_view = true, + height, +}: EventCrudButtons) { + const { user } = useAuthContext(); + const nav = useCustomNavigate(); + const isStaff = user?.is_staff || is_staff_overwrite; + const canChangeEvent = + hasPerm({ user: user, permission: PERM.SAMFUNDET_CHANGE_EVENT, obj: id }) || is_staff_overwrite; + + const viewUrl = reverse({ pattern: ROUTES.frontend.event, urlParams: { id: id } }); + const editUrl = reverse({ pattern: ROUTES.frontend.admin_events_edit, urlParams: { id: id } }); + const djangoUrl = reverse({ + pattern: ROUTES.backend.admin__samfundet_event_change, + urlParams: { objectId: id }, + }); + + return ( + nav({ url: viewUrl }) : undefined} + onEdit={canChangeEvent || isStaff ? () => nav({ url: editUrl }) : undefined} + onDelete={ + canChangeEvent || isStaff + ? () => { + const con = window.confirm(`Are you sure you want to delete ${title}`); + if (con && id) { + deleteEvent(id) + .then(() => { + alert(`${title} Has been deleted`); + }) + .catch(() => { + alert(`Failed to delete ${title}`); + }); + } + } + : undefined + } + onManage={isStaff ? () => nav({ linkTarget: 'backend', url: djangoUrl }) : undefined} + height={height} + /> + ); +} diff --git a/frontend/src/Components/EventCrudButtons/index.ts b/frontend/src/Components/EventCrudButtons/index.ts new file mode 100644 index 000000000..18b2df53e --- /dev/null +++ b/frontend/src/Components/EventCrudButtons/index.ts @@ -0,0 +1 @@ +export { EventCrudButtons } from './EventCrudButtons'; diff --git a/frontend/src/Components/IconButton/IconButton.module.scss b/frontend/src/Components/IconButton/IconButton.module.scss index dcbf146de..800e2640f 100644 --- a/frontend/src/Components/IconButton/IconButton.module.scss +++ b/frontend/src/Components/IconButton/IconButton.module.scss @@ -3,19 +3,21 @@ @use 'src/mixins' as *; .icon_button { + border: none; display: flex; - justify-content: center; align-items: center; - border: none; + justify-content: center; background-color: $grey-3; border-radius: 100%; - width: 1.75em; - height: 1.75em; + width: fit-content; + padding: 4px; cursor: pointer; color: white; + transition: all 200ms ease-in-out; &:hover { - filter: brightness(105%); + filter: brightness(110%); transform: scale(1.1); + rotate: 10deg; } } diff --git a/frontend/src/Components/IconButton/IconButton.tsx b/frontend/src/Components/IconButton/IconButton.tsx index d326d5eab..234b83f8f 100644 --- a/frontend/src/Components/IconButton/IconButton.tsx +++ b/frontend/src/Components/IconButton/IconButton.tsx @@ -12,7 +12,7 @@ type IconButtonProps = { icon: string; className?: string; border?: string; - height?: string; + height?: string | number; avatarColor?: string; } & Pick; @@ -41,7 +41,7 @@ export function IconButton({ title={title} target={target} className={classNames(styles.icon_button, className)} - style={{ backgroundColor: color, border: border, height: height }} + style={{ backgroundColor: color, border: border, height: height, width: height }} > @@ -54,7 +54,7 @@ export function IconButton({ onClick={handleOnClick} title={title} className={classNames(styles.icon_button, className)} - style={{ backgroundColor: color, border: border, height: height }} + style={{ backgroundColor: color, border: border, height: height, width: height }} > diff --git a/frontend/src/Components/ImageCard/ImageCard.module.scss b/frontend/src/Components/ImageCard/ImageCard.module.scss index 4c017eece..759696c44 100644 --- a/frontend/src/Components/ImageCard/ImageCard.module.scss +++ b/frontend/src/Components/ImageCard/ImageCard.module.scss @@ -5,13 +5,11 @@ $mobile-width: $primary-content-width-mobile; $card-border-radius: 0.5rem; $subtitle-max-height: 1rem; -$card-gradient-overlay: linear-gradient( - 180deg, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0.05) 30%, - rgba(0, 0, 0, 0.54) 80%, - rgba(0, 0, 0, 0.6) 100% -); +$card-gradient-overlay: linear-gradient(180deg, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.05) 30%, + rgba(0, 0, 0, 0.54) 80%, + rgba(0, 0, 0, 0.6) 100%); $card-text-shadow: 1px 1px 8px rgba(0, 0, 0, 0.5); $card-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); @@ -19,6 +17,7 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); // TODO color variables .container { + position: relative; display: flex; flex-direction: column; align-items: stretch; @@ -107,6 +106,7 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); .compact { width: 14em; + @include for-mobile-only { width: $mobile-width; } @@ -150,29 +150,55 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); float: right; } +.edit_bar { + opacity: 0%; + position: absolute; + display: flex; + flex-direction: row; + justify-content: end; + gap: 10px; + padding-right: 13px; + align-items: end; + width: 100%; + height: 55%; + pointer-events: none; + margin-left: 20px; + transition: all 200ms ease-in-out; + z-index: 10; +} + // Styling to container and children when it is hovered. .container:hover { .edit_button { visibility: visible; } + .card { transform: translateY(-3px); box-shadow: $card-box-shadow-hover; text-decoration: none; } + @include for-tablet-up { .subtitle { max-height: 0; opacity: 0; } } + .card_content { transform: none; } + .bottom_description { opacity: 1; max-height: 3.5em; } + + .edit_bar { + opacity: 100%; + margin-left: 0; + } } // Compact doesn't show description on hover @@ -181,8 +207,15 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); opacity: 1; max-height: $subtitle-max-height; } + .bottom_description { opacity: 0; max-height: 0; } } + +.badges { + display: flex; + align-items: center; + justify-content: space-between; +} diff --git a/frontend/src/Components/ImageCard/ImageCard.tsx b/frontend/src/Components/ImageCard/ImageCard.tsx index 6ef891988..c29f03a8b 100644 --- a/frontend/src/Components/ImageCard/ImageCard.tsx +++ b/frontend/src/Components/ImageCard/ImageCard.tsx @@ -3,6 +3,7 @@ import { t } from 'i18next'; import type { ReactNode } from 'react'; import { useEffect, useState } from 'react'; import { Skeleton } from '~/Components'; +import { EventCrudButtons } from '~/Components'; import { KEY } from '~/i18n/constants'; import { EventTicketType } from '~/types'; import { backgroundImageFromUrl } from '~/utils'; @@ -16,6 +17,7 @@ type ImageCardProps = { title?: ReactNode; subtitle?: ReactNode; description?: ReactNode; + id?: string; date?: string | Date; url?: string; imageUrl?: string; @@ -33,6 +35,7 @@ export function ImageCard({ subtitle = , description = , date, + id, url = '#', imageUrl, compact, @@ -48,6 +51,8 @@ export function ImageCard({ const [displayTicketType, setTicketType] = useState(''); const [showTicket, setShowTicket] = useState(false); + const height = compact ? 20 : 25; + useEffect(() => { if (ticket_type === EventTicketType.FREE || ticket_type === EventTicketType.REGISTRATION) { setTicketType(t(KEY.common_ticket_type_free)); @@ -68,9 +73,12 @@ export function ImageCard({ return (
+
+ {id && } +
-
+
{showTicket && }
diff --git a/frontend/src/Components/index.ts b/frontend/src/Components/index.ts index 1ffd6c92e..919b7cfb1 100644 --- a/frontend/src/Components/index.ts +++ b/frontend/src/Components/index.ts @@ -21,6 +21,7 @@ export { EventCardContainer } from './EventCardContainer'; export { EventQuery } from './EventQuery'; export { ExpandableHeader } from './ExpandableHeader'; export { ExpandableList } from './ExpandableList'; +export { EventCrudButtons } from './EventCrudButtons'; export { ExternalHostBox } from './ExternalHostBox'; export { Footer } from './Footer'; export { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useFormField } from './Forms'; diff --git a/frontend/src/Pages/EventPage/EventPage.module.scss b/frontend/src/Pages/EventPage/EventPage.module.scss index 7d3abf0d8..064283c89 100644 --- a/frontend/src/Pages/EventPage/EventPage.module.scss +++ b/frontend/src/Pages/EventPage/EventPage.module.scss @@ -21,7 +21,7 @@ .event_image { height: 45vh; margin: 0; - object-fit:cover; + object-fit: cover; border-bottom-right-radius: 0.10rem; border-bottom-left-radius: 0.10rem; } @@ -66,7 +66,7 @@ max-width: 50em; } -.expandable_header{ +.expandable_header { display: flex; align-items: center; justify-content: center; @@ -74,4 +74,12 @@ padding: 0 0.15rem 0 0.25rem; border: 1px solid $black; border-radius: 2rem; -} \ No newline at end of file +} + +.admin_panel { + display: flex; + flex-direction: row; + gap: 20px; + margin: 15px 0 10px 0; + color: white; +} diff --git a/frontend/src/Pages/EventPage/EventPage.tsx b/frontend/src/Pages/EventPage/EventPage.tsx index b47e3f85b..fd979b5ca 100644 --- a/frontend/src/Pages/EventPage/EventPage.tsx +++ b/frontend/src/Pages/EventPage/EventPage.tsx @@ -2,14 +2,18 @@ import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import { ExpandableHeader, ExternalHostBox, H1, Image, Page } from '~/Components'; +import { EventCrudButtons } from '~/Components'; import { BuyEventTicket } from '~/Components/BuyEventTicket/BuyEventTicket'; import { SamfMarkdown } from '~/Components/SamfMarkdown'; import { getEvent } from '~/api'; import { BACKEND_DOMAIN } from '~/constants'; +import { useAuthContext } from '~/context/AuthContext'; import { useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; +import { PERM } from '~/permissions'; import { eventKeys } from '~/queryKeys'; import { dbT } from '~/utils'; +import { hasPerm } from '~/utils'; import styles from './EventPage.module.scss'; import { EventInformation } from './components/EventInformation/EventInformation'; import { EventTable } from './components/EventTable'; @@ -17,6 +21,8 @@ import { EventTable } from './components/EventTable'; export function EventPage() { const { t } = useTranslation(); const { id } = useParams(); + const { user } = useAuthContext(); + const canChangeEvent = hasPerm({ user: user, permission: PERM.SAMFUNDET_CHANGE_EVENT, obj: id }); const { data: event, isLoading } = useQuery({ queryKey: id ? eventKeys.detail(Number(id)) : ['events', 'no-id'], @@ -31,6 +37,12 @@ export function EventPage() { {event && }
+ {canChangeEvent && ( +
+ +
+ )} +

{dbT(event, 'title')}

{event && } diff --git a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx index dc4062d0c..1f028d07b 100644 --- a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx +++ b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx @@ -1,7 +1,7 @@ import { Icon } from '@iconify/react'; import { type ReactNode, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, IconButton, InputField, Link, TimeDisplay } from '~/Components'; +import { Button, EventCrudButtons, IconButton, InputField, Link, TimeDisplay } from '~/Components'; import { eventQuery } from '~/Components/EventQuery/utils'; import { ImageCard } from '~/Components/ImageCard'; import { Table, type TableRow } from '~/Components/Table'; @@ -34,6 +34,7 @@ export function EventsList({ events }: EventsListProps) { { content: t(KEY.category), sortable: true }, { content: t(KEY.admin_organizer), sortable: true }, { content: t(KEY.common_buy), sortable: true }, + { content: t(KEY.common_edit), sortable: false }, ]; // TODO debounce and move header/filtering stuff to a separate component @@ -74,6 +75,13 @@ export function EventsList({ events }: EventsListProps) { event.category, event.host, event.ticket_type, + { + content: ( +
+ +
+ ), + }, ], })); } @@ -86,11 +94,12 @@ export function EventsList({ events }: EventsListProps) { diff --git a/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx b/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx index 28bd7f3a8..5e934a395 100644 --- a/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx +++ b/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx @@ -1,13 +1,10 @@ -import { Carousel, IconButton, ImageCard } from '~/Components'; +import { Carousel, ImageCard } from '~/Components'; import { BuyEventTicket } from '~/Components/BuyEventTicket/BuyEventTicket'; import { BACKEND_DOMAIN } from '~/constants'; -import { useAuthContext } from '~/context/AuthContext'; import type { EventDto, HomePageElementDto } from '~/dto'; import { reverse } from '~/named-urls'; -import { PERM } from '~/permissions'; import { ROUTES } from '~/routes'; -import { COLORS } from '~/types'; -import { dbT, hasPerm } from '~/utils'; +import { dbT } from '~/utils'; import styles from './EventCarousel.module.scss'; type EventCarouselProps = { @@ -18,8 +15,6 @@ type EventCarouselProps = { const spacing = 1.5; export function EventCarousel({ element, skeletonCount = 0 }: EventCarouselProps) { - const { user } = useAuthContext(); - const isStaff = user?.is_staff; const wrapperClass = styles.wrapper; if (!element) { @@ -37,12 +32,6 @@ export function EventCarousel({ element, skeletonCount = 0 }: EventCarouselProps {element.events.map((event: EventDto) => { const url = reverse({ pattern: ROUTES.frontend.event, urlParams: { id: event.id } }); - const editUrl = reverse({ pattern: ROUTES.frontend.admin_events_edit, urlParams: { id: event.id } }); - const detailurl = reverse({ - pattern: ROUTES.backend.admin__samfundet_event_change, - urlParams: { objectId: event.id }, - }); - const canChangeEvent = hasPerm({ user: user, permission: PERM.SAMFUNDET_CHANGE_EVENT, obj: event.id }); const event_title = dbT(event, 'title') ?? ''; const event_short_dsc = dbT(event, 'description_short') ?? ''; @@ -50,6 +39,7 @@ export function EventCarousel({ element, skeletonCount = 0 }: EventCarouselProps {event.billig && } - -
- {canChangeEvent && ( - - )} - {isStaff && canChangeEvent && ( - - )} -
); })} diff --git a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx index dbf74fab1..9aea7c3db 100644 --- a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx @@ -3,15 +3,13 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import { toast } from 'react-toastify'; -import { Button, EventQuery, TimeDisplay } from '~/Components'; -import { CrudButtons } from '~/Components/CrudButtons/CrudButtons'; +import { Button, EventCrudButtons, EventQuery, TimeDisplay } from '~/Components'; import { PagedPagination } from '~/Components/Pagination'; import { Table } from '~/Components/Table'; import { deleteEvent, getEventsUpcommingPaginated } from '~/api'; import type { EventDto } from '~/dto'; import { useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; -import { reverse } from '~/named-urls'; import { eventKeys } from '~/queryKeys'; import { ROUTES } from '~/routes'; import type { EventCategoryValue } from '~/types'; @@ -124,32 +122,7 @@ export function EventsAdminPage() { event.location, t(getTicketTypeKey(event.ticket_type)), { - content: ( - { - navigate( - reverse({ - pattern: ROUTES.frontend.event, - urlParams: { id: event.id }, - }), - ); - }} - onEdit={() => { - navigate( - reverse({ - pattern: ROUTES.frontend.admin_events_edit, - urlParams: { id: event.id }, - }), - ); - }} - onDelete={() => { - const msg = lowerCapitalize(`${t(KEY.form_confirm)} ${t(KEY.common_delete)}`); - if (window.confirm(`${msg} ${dbT(event, 'title')}`)) { - deleteSelectedEvent(event.id); - } - }} - /> - ), + content: , }, ], })); @@ -159,6 +132,7 @@ export function EventsAdminPage() { const header = ( <> diff --git a/frontend/src/routes/frontend.ts b/frontend/src/routes/frontend.ts index a6256e880..e9c7f3a60 100644 --- a/frontend/src/routes/frontend.ts +++ b/frontend/src/routes/frontend.ts @@ -100,6 +100,7 @@ export const ROUTES_FRONTEND = { admin_recruitment_room_create: '/control-panel/recruitment/:recruitmentId/room/create/', admin_recruitment_room_edit: '/control-panel/recruitment/:recruitmentId/room/edit/:roomId/', admin_recruitment_gang_position_applicants_overview: '/control-panel/recruitment/:recruitmentId/gang/:gangId/position/:positionId', + admin_recruitment_gang_position_applicants_interview_notes: '/control-panel/recruitment/:recruitmentId/gang/:gangId/position/:positionId/interview-notes/:interviewId', admin_recruitment_gang_all_applications: '/control-panel/recruitment/:recruitmentId/:gangId/all-applications/', admin_recruitment_gang_users_without_interview: '/control-panel/recruitment/:recruitmentId/:gangId/users-without-interviews/',