Skip to content

Commit

Permalink
Merge pull request #357 from kpi-ua/KB-250-announcements-slider
Browse files Browse the repository at this point in the history
Kb 250 announcements slider
  • Loading branch information
Markusplay authored Jan 31, 2025
2 parents e7074e9 + 7c7dd44 commit e8c4967
Show file tree
Hide file tree
Showing 32 changed files with 535 additions and 155 deletions.
124 changes: 124 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/actions/announcement.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use server';

import { campusFetch } from '@/lib/client';
import { Announcement } from '@/types/announcement';

export const getAnnouncements = async () => {
try {
const response = await campusFetch<Announcement[]>('announcements');

if (!response.ok) {
return [];
}

return response.json();
} catch (error) {
return [];
}
};
14 changes: 6 additions & 8 deletions src/actions/auth.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { campusFetch } from '@/lib/client';
import { User } from '@/types/user';
import { AuthResponse } from '@/types/auth-response';

const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN;
const OLD_CAMPUS_URL = process.env.OLD_CAMPUS_URL;
Expand All @@ -18,7 +19,7 @@ export async function loginWithCredentials(username: string, password: string, r
grant_type: 'password',
};

const response = await campusFetch('oauth/token', {
const response = await campusFetch<AuthResponse>('oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand All @@ -36,8 +37,7 @@ export async function loginWithCredentials(username: string, password: string, r
return null;
}

const userResponse = await campusFetch('profile', {
method: 'GET',
const userResponse = await campusFetch<User>('profile', {
headers: {
Authorization: `Bearer ${jsonResponse.access_token}`,
},
Expand All @@ -52,7 +52,7 @@ export async function loginWithCredentials(username: string, password: string, r
const token = JWT.decode(access_token) as { exp: number };
const tokenExpiresAt = new Date(token.exp * 1000);

const user: User = await userResponse.json();
const user = await userResponse.json();

const expires = rememberMe ? tokenExpiresAt : undefined;

Expand Down Expand Up @@ -94,10 +94,8 @@ export async function resetPassword(username: string, recaptchaToken: string) {
}
}

export async function getUserDetails(): Promise<User | null> {
const userResponse = await campusFetch('profile', {
method: 'GET',
});
export async function getUserDetails() {
const userResponse = await campusFetch<User>('profile');

if (!userResponse.ok) {
return null;
Expand Down
6 changes: 2 additions & 4 deletions src/actions/group.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import { campusFetch } from '@/lib/client';
import { Group } from '@/types/group';
import queryString from 'query-string';

export async function searchByGroupName(search: string): Promise<Group[]> {
export async function searchByGroupName(search: string) {
try {
const query = queryString.stringify({ name: search });

const response = await campusFetch(`group/find?${query}`, {
method: 'GET',
});
const response = await campusFetch<Group[]>(`group/find?${query}`);

if (response.status < 200 || response.status >= 300) {
return [];
Expand Down
8 changes: 4 additions & 4 deletions src/actions/profile.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import { campusFetch } from '@/lib/client';
import { revalidatePath } from 'next/cache';
import { getUserDetails } from '@/actions/auth.actions';

export async function getContacts(): Promise<Contact[]> {
export async function getContacts() {
try {
const response = await campusFetch('profile/contacts');
const response = await campusFetch<Contact[]>('profile/contacts');

return response.json();
} catch (error) {
return [];
}
}

export async function getContactTypes(): Promise<ContactType[]> {
export async function getContactTypes() {
try {
const response = await campusFetch('profile/contacts/types');
const response = await campusFetch<ContactType[]>('profile/contacts/types');

return response.json();
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { Heading3 } from '@/components/typography/headers';
import { Paragraph } from '@/components/typography/paragraph';
import { AspectRatio } from '@/components/ui/aspect-ratio';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

interface AnnouncementSlideProps {
title: React.ReactNode;
description: React.ReactNode;
link?: string;
linkTitle?: string;
image?: string;
}

export const AnnouncementSlide = ({ title, description, link, linkTitle, image }: AnnouncementSlideProps) => {
const t = useTranslations('private.main.cards.carousel');

return (
<div className="flex flex-col gap-8 lg:flex-row">
<div className="mb-4 max-w-full grow overflow-y-hidden">
<Heading3>{title}</Heading3>
<Paragraph
className={cn('mb-0 grow', {
'line-clamp-[6] lg:line-clamp-[6] xl:line-clamp-[8]': !!link,
'line-clamp-[8] lg:line-clamp-[7] xl:line-clamp-[9]': !link,
})}
>
{description}
</Paragraph>
{link && (
<Button variant="secondary" className="mt-4" asChild>
<Link href={link} target="_blank" rel="noopener noreferrer">
{linkTitle || t('default-link-title')}
</Link>
</Button>
)}
</div>
<div className={cn('hidden min-w-[300px] basis-[300px] lg:block xl:min-w-[400px] xl:basis-[400px]')}>
<AspectRatio ratio={4 / 3.5}>
{image && <img src={image} alt={title as string} className="h-full w-full rounded-md object-cover" />}
</AspectRatio>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getAnnouncements } from '@/actions/announcement.actions';
import { AnnouncementsCarousel } from './announcements-carousel';
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';

interface AnnouncementsCardProps {
className?: string;
}

export const AnnouncementsCard = async ({ className }: AnnouncementsCardProps) => {
const announcements = await getAnnouncements();

return (
<Card className={cn(className)}>
<CardContent className="flex h-full gap-8 space-y-1.5 p-10">
<AnnouncementsCarousel announcements={announcements} />
</CardContent>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';

import { Carousel, CarouselApi, CarouselContent, CarouselItem } from '@/components/ui/carousel';

import Autoplay from 'embla-carousel-autoplay';
import { AnnouncementSlide } from './announcement-slide';
import { Announcement } from '@/types/announcement';
import { DotButton } from './dot-button';
import { useState } from 'react';
import { useDotButton } from './use-dot-button';
import { DefaultAnnouncementSlide } from './default-announcement-slide';

const AUTOPLAY_DELAY = 10_000; // 10 seconds

interface AnnouncementsCarouselProps {
announcements: Announcement[];
}

const Slide = ({ children }: { children: React.ReactNode }) => <CarouselItem className="pl-4">{children}</CarouselItem>;

export const AnnouncementsCarousel = ({ announcements }: AnnouncementsCarouselProps) => {
const [api, setApi] = useState<CarouselApi>();
const { selectedIndex, scrollSnaps, onDotButtonClick } = useDotButton(api);

return (
<Carousel
opts={{ loop: true, align: 'center' }}
plugins={[Autoplay({ delay: AUTOPLAY_DELAY })]}
className="w-full"
setApi={setApi}
>
<CarouselContent className="-ml-4 mb-4">
<Slide>
<DefaultAnnouncementSlide />
</Slide>
{announcements.map((announcement) => (
<Slide key={announcement.id}>
<AnnouncementSlide
title={announcement.title}
description={announcement.description}
link={announcement.link}
image={announcement.image}
/>
</Slide>
))}
</CarouselContent>
<div className="absolute -bottom-4 -left-4 flex gap-[8px]">
{scrollSnaps.map((_, index) => (
<DotButton key={index} onClick={() => onDotButtonClick(index)} selected={selectedIndex === index} />
))}
</div>
</Carousel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useTranslations } from 'next-intl';
import { AnnouncementSlide } from './announcement-slide';

export const DefaultAnnouncementSlide = () => {
const t = useTranslations('private.main.cards.carousel.default-slide');

const title = t.rich('title-base', {
systemname: () => <span className="text-brand-700">«{t('title-system-name')}»</span>,
});

return (
<AnnouncementSlide
title={title}
description={t('description')}
link="#"
linkTitle={t('link-title')}
image="/images/Saly-10.png"
/>
);
};
Loading

0 comments on commit e8c4967

Please sign in to comment.