Merge pull request #421 from bipproduction/amalia/15-apr-25

Amalia/15 apr 25
This commit is contained in:
Amalia
2025-04-15 17:17:40 +08:00
committed by GitHub
8 changed files with 241 additions and 38 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -29,7 +29,12 @@
"@mantine/tiptap": "^7.11.0", "@mantine/tiptap": "^7.11.0",
"@prisma/client": "5.16.1", "@prisma/client": "5.16.1",
"@tabler/icons-react": "^3.7.0", "@tabler/icons-react": "^3.7.0",
"@tiptap/extension-color": "^2.11.7",
"@tiptap/extension-highlight": "^2.11.7",
"@tiptap/extension-link": "^2.4.0", "@tiptap/extension-link": "^2.4.0",
"@tiptap/extension-text-align": "^2.11.7",
"@tiptap/extension-underline": "^2.11.7",
"@tiptap/pm": "^2.11.7",
"@tiptap/react": "^2.4.0", "@tiptap/react": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0", "@tiptap/starter-kit": "^2.4.0",
"@types/lodash": "^4.17.6", "@types/lodash": "^4.17.6",

View File

@@ -3,6 +3,7 @@ import { WrapLayout } from "@/module/_global"
import { funDetectCookies, funGetUserByCookies } from "@/module/auth" import { funDetectCookies, funGetUserByCookies } from "@/module/auth"
import _ from "lodash" import _ from "lodash"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import '@mantine/tiptap/styles.css';
export default async function Layout({ children }: { children: React.ReactNode }) { export default async function Layout({ children }: { children: React.ReactNode }) {
const cookies = await funDetectCookies() const cookies = await funDetectCookies()

View File

@@ -7,7 +7,7 @@ import { IoCloseOutline } from 'react-icons/io5';
import { TEMA } from '../bin/val_global'; import { TEMA } from '../bin/val_global';
export default function NotificationCustomeCenter({ title, desc, onClick }: { title: string, desc: string, onClick: (val: string) => void, }) { export default function NotificationCustomeCenter({ title, desc, onClick }: { title: string, desc: string, onClick: (val: string) => void, }) {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false)
const tema = useHookstate(TEMA) const tema = useHookstate(TEMA)
useShallowEffect(() => { useShallowEffect(() => {
@@ -72,7 +72,15 @@ export default function NotificationCustomeCenter({ title, desc, onClick }: { ti
/> />
<Text size="xl" fw={500} ta="center">PENGUMUMAN</Text> <Text size="xl" fw={500} ta="center">PENGUMUMAN</Text>
<Text size="sm" ta="center">{desc}</Text> <Text size="sm" ta="center">{desc}</Text>
<SimpleGrid cols={2} spacing="xs" w={'90%'}> <SimpleGrid
cols={{ base: 1, sm: 2, lg: 2 }}
spacing="xs"
w={'90%'}>
{/* {
isMobile
? <></>
:
<> */}
<Button <Button
fullWidth fullWidth
radius="md" radius="md"
@@ -93,6 +101,8 @@ export default function NotificationCustomeCenter({ title, desc, onClick }: { ti
> >
Tandai Telah Dibaca Tandai Telah Dibaca
</Button> </Button>
{/* </>
} */}
</SimpleGrid> </SimpleGrid>
</Flex> </Flex>
</Box> </Box>

View File

@@ -2,8 +2,16 @@
import { keyWibu, LayoutNavbarNew, TEMA } from "@/module/_global"; import { keyWibu, LayoutNavbarNew, TEMA } from "@/module/_global";
import LayoutModal from "@/module/_global/layout/layout_modal"; import LayoutModal from "@/module/_global/layout/layout_modal";
import { useHookstate } from "@hookstate/core"; import { useHookstate } from "@hookstate/core";
import { Box, Button, Flex, Group, rem, Stack, Text, Textarea, TextInput } from "@mantine/core"; import { Box, Button, Flex, Group, rem, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { Link, RichTextEditor } from "@mantine/tiptap";
import { Color } from '@tiptap/extension-color';
import Highlight from '@tiptap/extension-highlight';
import TextAlign from '@tiptap/extension-text-align';
import TextStyle from '@tiptap/extension-text-style';
import Underline from '@tiptap/extension-underline';
import { useEditor } from "@tiptap/react";
import StarterKit from '@tiptap/starter-kit';
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@@ -36,6 +44,19 @@ export default function CreateAnnouncement() {
desc: false desc: false
}); });
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Highlight,
TextStyle,
Color,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: "",
});
async function onSubmit() { async function onSubmit() {
try { try {
@@ -74,14 +95,17 @@ export default function CreateAnnouncement() {
function onCheck() { function onCheck() {
const cek = checkAll() onValidation('desc', '')
if (!cek) setTimeout(() => {
return false const cek = checkAll()
if (!cek)
return false
if (memberValue.length == 0) if (memberValue.length == 0)
return toast.error("Error! silahkan pilih divisi") return toast.error("Error! silahkan pilih divisi")
setOpen(true) setOpen(true)
}, 500)
} }
function checkAll() { function checkAll() {
@@ -90,14 +114,16 @@ export default function CreateAnnouncement() {
setTouched(touched => ({ ...touched, title: true })) setTouched(touched => ({ ...touched, title: true }))
nilai = false nilai = false
} }
if (isData.desc === "") { if (String(editor?.getHTML()) === "" || String(editor?.getHTML()) === "<p></p>" || String(editor?.getHTML()) === "undefined") {
setTouched(touched => ({ ...touched, desc: true })) setTouched(touched => ({ ...touched, desc: true }))
nilai = false nilai = false
toast.error("Pengumuman Tidak Boleh Kosong!")
} }
return nilai return nilai
} }
function onValidation(kategori: string, val: string) { function onValidation(kategori: string, val: string) {
if (kategori == 'title') { if (kategori == 'title') {
setisData({ ...isData, title: val }) setisData({ ...isData, title: val })
@@ -107,15 +133,16 @@ export default function CreateAnnouncement() {
setTouched({ ...touched, title: false }) setTouched({ ...touched, title: false })
} }
} else if (kategori == 'desc') { } else if (kategori == 'desc') {
setisData({ ...isData, desc: val }) setisData({ ...isData, desc: String(editor?.getHTML()) })
if (val === "") { // if (val === "") {
setTouched({ ...touched, desc: true }) // setTouched({ ...touched, desc: true })
} else { // } else {
setTouched({ ...touched, desc: false }) // setTouched({ ...touched, desc: false })
} // }
} }
} }
if (isChooseMember) return <CreateUsersAnnouncement onClose={() => { setIsChooseMember(false) }} /> if (isChooseMember) return <CreateUsersAnnouncement onClose={() => { setIsChooseMember(false) }} />
return ( return (
@@ -141,7 +168,7 @@ export default function CreateAnnouncement() {
) )
} }
/> />
<Textarea {/* <Textarea
size="md" size="md"
radius={10} radius={10}
w={"100%"} w={"100%"}
@@ -162,7 +189,75 @@ export default function CreateAnnouncement() {
isData.desc == "" ? "Pengumuman Tidak Boleh Kosong" : null isData.desc == "" ? "Pengumuman Tidak Boleh Kosong" : null
) )
} }
/> /> */}
<Box>
<Group gap="xs">
<Text>Pengumuman</Text>
<Text c={'red'}>*</Text>
</Group>
<RichTextEditor editor={editor} style={{ border: `1px solid ${tema.get().utama}` }}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.Highlight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ColorPicker
colors={[
'#25262b',
'#868e96',
'#fa5252',
'#e64980',
'#be4bdb',
'#7950f2',
'#4c6ef5',
'#228be6',
'#15aabf',
'#12b886',
'#40c057',
'#82c91e',
'#fab005',
'#fd7e14',
]}
/>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Color color="#F03E3E" />
<RichTextEditor.Color color="#7048E8" />
<RichTextEditor.Color color="#1098AD" />
<RichTextEditor.Color color="#37B24D" />
<RichTextEditor.Color color="#F59F00" />
</RichTextEditor.ControlsGroup>
<RichTextEditor.UnsetColor />
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
</Box>
<Box pt={10}> <Box pt={10}>
<Group justify="space-between" style={{ <Group justify="space-between" style={{
border: `1px solid ${tema.get().utama}`, border: `1px solid ${tema.get().utama}`,

View File

@@ -95,7 +95,7 @@ export default function DetailAnnouncement({ id }: { id: string }) {
</Grid.Col> </Grid.Col>
<Grid.Col span={10}> <Grid.Col span={10}>
<Spoiler maxHeight={100} showLabel="Lebih banyak" hideLabel="Lebih sedikit"> <Spoiler maxHeight={100} showLabel="Lebih banyak" hideLabel="Lebih sedikit">
<Text>{isData?.desc}</Text> <Box dangerouslySetInnerHTML={{ __html: String(isData?.desc) }} />
</Spoiler> </Spoiler>
</Grid.Col> </Grid.Col>
</Grid> </Grid>

View File

@@ -2,8 +2,16 @@
import { LayoutNavbarNew, TEMA } from "@/module/_global"; import { LayoutNavbarNew, TEMA } from "@/module/_global";
import LayoutModal from "@/module/_global/layout/layout_modal"; import LayoutModal from "@/module/_global/layout/layout_modal";
import { useHookstate } from "@hookstate/core"; import { useHookstate } from "@hookstate/core";
import { Box, Button, Flex, Group, List, rem, Skeleton, Stack, Text, Textarea, TextInput } from "@mantine/core"; import { Box, Button, Flex, Group, List, rem, Skeleton, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { Link, RichTextEditor } from "@mantine/tiptap";
import Color from "@tiptap/extension-color";
import Highlight from '@tiptap/extension-highlight';
import TextAlign from "@tiptap/extension-text-align";
import TextStyle from "@tiptap/extension-text-style";
import Underline from "@tiptap/extension-underline";
import { useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@@ -15,7 +23,6 @@ import EditChooseMember from "./edit_choose_member";
export default function EditAnnouncement() { export default function EditAnnouncement() {
const [isOpen, setOpen] = useState(false) const [isOpen, setOpen] = useState(false)
const [loadingKonfirmasi, setLoadingKonfirmasi] = useState(false)
const [isChooseDivisi, setChooseDivisi] = useState(false) const [isChooseDivisi, setChooseDivisi] = useState(false)
const param = useParams<{ id: string }>() const param = useParams<{ id: string }>()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -32,6 +39,19 @@ export default function EditAnnouncement() {
}) })
const memberGroup = useHookstate(globalMemberEditAnnouncement) const memberGroup = useHookstate(globalMemberEditAnnouncement)
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Highlight,
TextStyle,
Color,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: body.desc,
});
async function fetchOneAnnouncement() { async function fetchOneAnnouncement() {
try { try {
@@ -60,16 +80,11 @@ export default function EditAnnouncement() {
}) })
arrNew.push(newObject) arrNew.push(newObject)
}) })
memberGroup.set(arrNew) memberGroup.set(arrNew)
} else { } else {
toast.error(res.message) toast.error(res.message)
} }
setLoading(false)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
toast.error("Gagal mendapatkan pengumuman, coba lagi nanti") toast.error("Gagal mendapatkan pengumuman, coba lagi nanti")
@@ -84,12 +99,16 @@ export default function EditAnnouncement() {
fetchOneAnnouncement() fetchOneAnnouncement()
}, []) }, [])
useShallowEffect(() => {
editor?.commands.insertContent(body.desc)
}, [editor, body.desc])
async function onSubmit() { async function onSubmit() {
try { try {
setLoadingSubmit(true) setLoadingSubmit(true)
const response = await funEditAnnouncement(param.id, { const response = await funEditAnnouncement(param.id, {
title: body.title, title: body.title,
desc: body.desc, desc: String(editor?.getHTML()),
groups: memberGroup.get() as GroupData[] groups: memberGroup.get() as GroupData[]
}) })
@@ -109,10 +128,10 @@ export default function EditAnnouncement() {
} }
function onCheck() { function onCheck() {
if (Object.values(touched).some((v) => v == true)) if (Object.values(touched).some((v) => v == true) || String(editor?.getHTML()) === "" || String(editor?.getHTML()) === "<p></p>" || String(editor?.getHTML()) === "undefined")
return false return toast.error("Error! Judul dan pengumuman tidak boleh kosong")
if (memberGroup.get().length == 0) if (memberGroup.get().length == 0)
return toast.error("Error! silahkan pilih divisi") return toast.error("Error! silahkan pilih divisi")
setOpen(true) setOpen(true)
} }
@@ -126,8 +145,7 @@ export default function EditAnnouncement() {
setTouched({ ...touched, title: false }) setTouched({ ...touched, title: false })
} }
} else if (kategori == 'desc') { } else if (kategori == 'desc') {
setBody({ ...body, desc: val }) if (String(editor?.getHTML()) === "" || String(editor?.getHTML()) === "<p></p>" || String(editor?.getHTML()) === "undefined") {
if (val === "") {
setTouched({ ...touched, desc: true }) setTouched({ ...touched, desc: true })
} else { } else {
setTouched({ ...touched, desc: false }) setTouched({ ...touched, desc: false })
@@ -166,14 +184,16 @@ export default function EditAnnouncement() {
}, },
}} }}
value={body.title} value={body.title}
onChange={(e) => { onValidation('title', e.target.value) }} onChange={(e) => {
onValidation('title', e.target.value)
}}
error={ error={
touched.title && ( touched.title && (
body.title == "" ? "Judul Tidak Boleh Kosong" : null body.title == "" ? "Judul Tidak Boleh Kosong" : null
) )
} }
/> />
<Textarea {/* <Textarea
size="md" size="md"
radius={10} radius={10}
w={"100%"} w={"100%"}
@@ -194,7 +214,76 @@ export default function EditAnnouncement() {
body.desc == "" ? "Pengumuman Tidak Boleh Kosong" : null body.desc == "" ? "Pengumuman Tidak Boleh Kosong" : null
) )
} }
/> /> */}
<Box>
<Group gap="xs">
<Text>Pengumuman</Text>
<Text c={'red'}>*</Text>
</Group>
<RichTextEditor editor={editor} style={{ border: `1px solid ${tema.get().utama}` }}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.Highlight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ColorPicker
colors={[
'#25262b',
'#868e96',
'#fa5252',
'#e64980',
'#be4bdb',
'#7950f2',
'#4c6ef5',
'#228be6',
'#15aabf',
'#12b886',
'#40c057',
'#82c91e',
'#fab005',
'#fd7e14',
]}
/>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Color color="#F03E3E" />
<RichTextEditor.Color color="#7048E8" />
<RichTextEditor.Color color="#1098AD" />
<RichTextEditor.Color color="#37B24D" />
<RichTextEditor.Color color="#F59F00" />
</RichTextEditor.ControlsGroup>
<RichTextEditor.UnsetColor />
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
</Box>
<Box pt={10} w={"100%"}> <Box pt={10} w={"100%"}>
<Group justify="space-between" style={{ <Group justify="space-between" style={{
border: `1px solid ${tema.get().utama}`, border: `1px solid ${tema.get().utama}`,

View File

@@ -182,9 +182,12 @@ export default function ListAnnouncement() {
</Grid> </Grid>
{/* <Text c={tema.get().utama} lineClamp={2}>{v.desc}</Text> */} {/* <Text c={tema.get().utama} lineClamp={2}>{v.desc}</Text> */}
<Spoiler maxHeight={50} showLabel="Lebih banyak" hideLabel="Lebih sedikit"> <Spoiler maxHeight={50} showLabel="Lebih banyak" hideLabel="Lebih sedikit">
<Text c={tema.get().utama} onClick={() => { <Box
router.push(`/announcement/${v.id}`) dangerouslySetInnerHTML={{ __html: String(v.desc) }}
}} >{v.desc}</Text> onClick={() => {
router.push(`/announcement/${v.id}`)
}}
/>
</Spoiler> </Spoiler>
</Grid.Col> </Grid.Col>
</Grid> </Grid>