Blog creation and filling dialog completed #1

Merged
neki merged 10 commits from feature/blogCreationAndFilling into develop 2025-02-01 09:10:18 +00:00
27 changed files with 1069 additions and 211 deletions

View File

@ -8,6 +8,8 @@ services:
networks:
- app-network
restart: unless-stopped
volumes:
- /etc/letsencrypt/live/nekiiinkognito.ru:/etc/nginx/ssl:ro
enshi_back:
build: ./enshi_back

View File

@ -14,9 +14,5 @@ FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Certificates
COPY ./nginx/fullchain.pem /etc/nginx/ssl/
COPY ./nginx/privkey.pem /etc/nginx/ssl/
EXPOSE 80
EXPOSE 443

View File

@ -7,8 +7,8 @@ server {
server {
listen 443 ssl http2;
# server_name nekiiinkognito.ru www.nekiiinkognito.ru;
server_name localhost;
server_name nekiiinkognito.ru www.nekiiinkognito.ru;
# server_name localhost;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;

View File

@ -1,18 +1,18 @@
{
"name": "enshi",
"version": "0.0.0",
"version": "0.1.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "enshi",
"version": "0.0.0",
"version": "0.1.7",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-form": "^0.1.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/themes": "^3.1.3",
@ -2234,18 +2234,18 @@
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz",
"integrity": "sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
"integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
@ -2264,6 +2264,27 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
@ -2280,12 +2301,12 @@
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
@ -2303,6 +2324,47 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz",

View File

@ -1,7 +1,7 @@
{
"name": "enshi",
"private": true,
"version": "0.1.7",
"version": "0.1.8",
"type": "module",
"scripts": {
"dev": "vite",
@ -15,7 +15,7 @@
"@radix-ui/react-form": "^0.1.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/themes": "^3.1.3",

View File

@ -0,0 +1,8 @@
export type Blog = {
blog_id: number;
user_id: number;
title: string;
description?: string;
category_id?: number;
created_at: Date;
}

View File

@ -13,7 +13,7 @@ const router = createBrowserRouter(routes);
export default function App() {
return (
<Theme className="h-fit" accentColor="indigo" grayColor="slate">
<Theme className="h-fit" accentColor="sky" grayColor="slate" appearance="dark">
<ToastProvider>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />

View File

@ -1,20 +1,11 @@
import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import {
Box,
Button,
Container,
Flex,
Select,
Separator,
Text,
} from "@radix-ui/themes";
import { Box, Container, Flex, Separator, Text } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { Interweave } from "interweave";
import { useAtomValue } from "jotai";
import { useParams } from "react-router-dom";
import { axiosLocalhost } from "../../api/axios/axios";
import { userAtom } from "../../AtomStore/AtomStore";
import AddPostToBlogDialog from "../Dialogs/AddPostToBlogDialog/AddPostToBlogDialog";
import ChangePostButton from "./ChangePostButton/ChangePostButton";
import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader";
import VoteButton, { DOWNVOTE, UPVOTE } from "./VoteButton/VoteButton";
@ -78,62 +69,7 @@ export default function ArticleViewer(props: TArticleViewer) {
/>
</Box>
<Dialog.Root>
<Dialog.Trigger asChild>
<Button
variant="surface"
className="h-5"
>
Add to blog
</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-blackA6 data-[state=open]:animate-overlayShow" />
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-contentShow">
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
Add this post to blog
</Dialog.Title>
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
<Flex>
<Text>
{`Add "${data.title}" to blog...`}
</Text>
<Select.Root defaultValue="apple">
<Select.Trigger />
<Select.Content>
<Select.Group>
<Select.Item value="orange">
This
</Select.Item>
<Select.Item value="apple">
This is
updated blog
</Select.Item>
<Select.Item value="grape">
This another
</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</Flex>
</Dialog.Description>
<div className="mt-[25px] flex justify-end">
<Dialog.Close asChild>
<Button>Confirm</Button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
{user ? <AddPostToBlogDialog /> : null}
</Flex>
</Flex>
<Separator size={"4"} className="mb-2" />

View File

@ -1,7 +1,10 @@
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons";
import { IconButton } from "@radix-ui/themes";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { axiosLocalhost } from "../../../api/axios/axios";
import { userAtom } from "../../../AtomStore/AtomStore";
import useToast from "../../../hooks/useToast";
export const UPVOTE = true;
export const DOWNVOTE = false;
@ -14,6 +17,9 @@ type TVoteButton = {
export default function VoteButton(props: TVoteButton) {
const queryClient = useQueryClient();
const user = useAtomValue(userAtom);
const createToast = useToast();
const { data } = useQuery({
queryKey: [props.vote + "voteCheck"],
queryFn: async () => {
@ -57,7 +63,13 @@ export default function VoteButton(props: TVoteButton) {
<IconButton
variant={data ? "solid" : "outline"}
size={"1"}
onClick={() => voteMutation.mutate()}
onClick={() => {
if(!user) {
createToast({title: "Please log in to vote", description: "You need to be logged in to vote on a post."});
return;
}
voteMutation.mutate();
}}
>
{props.vote ? <DoubleArrowUpIcon /> : <DoubleArrowDownIcon />}
</IconButton>

View File

@ -0,0 +1,155 @@
import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { Button, Card, Flex, Select, Text, Theme } from "@radix-ui/themes";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Blog } from "../../../@types/BlogTypes";
import { axiosLocalhost } from "../../../api/axios/axios";
import { userAtom } from "../../../AtomStore/AtomStore";
import useToast from "../../../hooks/useToast";
import { JSONWithInt64 } from "../../../utils/idnex";
export default function AddPostToBlogDialog() {
const navigate = useNavigate();
const user = useAtomValue(userAtom);
const [selectedBlog, setSelectedBlog] = useState<string>("");
const [isOpen, setIsOpen] = useState(false);
const params = useParams();
const createToast = useToast();
const { data, refetch } = useQuery({
queryKey: ["userBlogs"],
queryFn: async () => {
const response = await axiosLocalhost.get("/user/blogs", {
transformResponse: [(data) => data],
});
let temp = JSONWithInt64(response.data);
return temp as Blog[];
},
enabled: false,
});
const addMutation = useMutation({
mutationKey: ["addPostToBlog"],
mutationFn: async () => {
const response = await axiosLocalhost.put(
`posts/${params["postId"]}/blogs/${selectedBlog}`
);
return response.data;
},
onError: (error) => {
console.error(error);
createToast({
title: "Error!",
description: "Post have not been added",
});
},
onSuccess: () => {
console.log("Post added successfully");
createToast({
title: "Success!",
description: "Post added successfully",
});
},
onSettled: () => {
console.log("Add mutation is settled");
},
});
if (!user) {
navigate("/login");
return null;
}
return (
<Dialog.Root
open={isOpen}
onOpenChange={(e) => {
setIsOpen(e);
if (e) {
refetch();
}
}}
>
<Dialog.Trigger asChild>
<Button variant="surface" className="h-5">
<Text>Add to blog</Text>
</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/40 data-[state=open]:animate-overlayShow" />
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-md shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-contentShow">
<Theme panelBackground="solid">
<Card className="p-5">
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
<Text size={"4"}>Add this post to blog</Text>
</Dialog.Title>
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
<Flex gap={"2"} align={"center"}>
<Text>{`Add post to `}</Text>
<Select.Root
value={selectedBlog}
onValueChange={(e) => {
setSelectedBlog(e);
}}
>
<Select.Trigger className="w-40 cursor-pointer" />
<Select.Content>
<Select.Group>
{data?.map((blog, i) => {
return (
<Select.Item
key={i}
value={`${blog.blog_id}`}
>
{blog.title}
</Select.Item>
);
})}
</Select.Group>
</Select.Content>
</Select.Root>
</Flex>
</Dialog.Description>
<Flex
className="items-center justify-between"
>
{selectedBlog ? <div/> : <Text color="gray">* Please select a blog to attach the post to.</Text>}
<div className="flex flex-col justify-center">
<Dialog.Close asChild>
<Button
className="cursor-pointer"
onClick={() => {
addMutation.mutate();
}}
disabled={!selectedBlog}
>
<Text>Confirm</Text>
</Button>
</Dialog.Close>
</div>
</Flex>
<Dialog.Close asChild>
<button
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Card>
</Theme>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -0,0 +1,123 @@
import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
import { Box, Button, Card, Text, Theme } from "@radix-ui/themes";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { axiosLocalhost } from "../../../api/axios/axios";
import queryClient from "../../../api/QueryClient/QueryClient";
import useToast from "../../../hooks/useToast";
export default function BlogCreationDialog() {
const createToast = useToast();
const [title, setTitle] = useState<string>("My blog");
const [description, setDescription] = useState<string>("");
const addMutation = useMutation({
onMutate: () => {},
mutationFn: async () => {
await axiosLocalhost.post("/blogs", {
title,
description,
});
},
onSuccess: () => {
createToast({
title: `Success!`,
description: `Blog created successfully!`,
});
},
onError: (_error) => {
createToast({
title: `Error!`,
description: `Blog creation failed!`,
});
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["userBlogs"],
});
},
});
return (
<Box>
<Dialog.Root>
<Dialog.Trigger asChild>
<Button onClick={() => {}} className="w-full">
<PlusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Box>
<Theme>
<Dialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm data-[state=open]:animate-appear" />
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-md p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-appear">
<Card>
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
<Text size={"4"}>Create blog</Text>
</Dialog.Title>
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
<Text>Create your new blog.</Text>
</Dialog.Description>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="w-[90px] text-right text-[15px] text-violet11"
htmlFor="title"
>
<Text>Blog title</Text>
</label>
<input
className="inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
id="title"
defaultValue="My blog"
onChange={(e) => {
setTitle(e.target.value);
}}
/>
</fieldset>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="w-[90px] text-right text-[15px] text-violet11"
htmlFor="Description"
>
<Text>Description</Text>
</label>
<textarea
className="pt-2 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
id="Description"
placeholder="Your description..."
onChange={(e) => {
setDescription(e.target.value);
}}
/>
</fieldset>
<div className="mt-[25px] flex justify-end">
<Dialog.Close
asChild
onClick={() => {
addMutation.mutate();
}}
>
<Button className="cursor-pointer">
Create blog
</Button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Card>
</Dialog.Content>
</Theme>
</Box>
</Dialog.Portal>
</Dialog.Root>
</Box>
);
}

View File

@ -0,0 +1,20 @@
import { EnterIcon } from "@radix-ui/react-icons";
import { Flex, Text } from "@radix-ui/themes";
import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export default function LoginButton() {
const { t } = useTranslation();
return (
<Link to={"/login"}>
<Flex className="justify-between gap-2">
<Icon>
<EnterIcon />
</Icon>
<Text>{t("signIn")}</Text>
</Flex>
</Link>
);
}

View File

@ -0,0 +1,48 @@
import { ExitIcon } from "@radix-ui/react-icons";
import { Flex, Text } from "@radix-ui/themes";
import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js";
import { useMutation } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import queryClient from "../../../../../api/QueryClient/QueryClient";
import { userAtom } from "../../../../../AtomStore/AtomStore";
import useToast from "../../../../../hooks/useToast";
import { logout } from "../../../../../utils/logout";
export default function LogoutButton() {
const { t } = useTranslation();
const navigate = useNavigate();
const setUserData = useSetAtom(userAtom)
const createToast = useToast()
const logoutMutation = useMutation({
onMutate: () => {
queryClient.cancelQueries({
queryKey: ["authKey"],
});
},
mutationFn: async () => {
await logout();
},
onError: () => {},
onSuccess: () => {
setUserData(undefined)
createToast({ title: "Success!", description: "Logged out successfully"})
navigate("/");
},
});
return (
<Flex
className="justify-between gap-2"
onClick={() => logoutMutation.mutate()}
>
<Icon>
<ExitIcon />
</Icon>
<Text>{t("signOut")}</Text>
</Flex>
);
}

View File

@ -1,8 +1,6 @@
import {
EnterIcon,
ExitIcon,
LaptopIcon,
PersonIcon,
PersonIcon
} from "@radix-ui/react-icons";
import { DropdownMenu, Flex, IconButton, Text } from "@radix-ui/themes";
import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js";
@ -10,6 +8,8 @@ import { useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { userAtom } from "../../../../AtomStore/AtomStore";
import LoginButton from "./LoginButton/LoginButton";
import LogoutButton from "./LogoutButton/LogoutButton";
export default function UserButton() {
const user = useAtomValue(userAtom);
@ -19,7 +19,7 @@ export default function UserButton() {
<div className="">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton>
<IconButton className="cursor-pointer">
<PersonIcon />
</IconButton>
</DropdownMenu.Trigger>
@ -52,21 +52,9 @@ export default function UserButton() {
<DropdownMenu.Item color={user ? "red" : "green"}>
{user ? (
<Flex className="justify-between gap-2">
<Icon>
<ExitIcon />
</Icon>
<Text>{t("signOut")}</Text>
</Flex>
<LogoutButton />
) : (
<Link to={"/login"}>
<Flex className="justify-between gap-2">
<Icon>
<EnterIcon />
</Icon>
<Text>{t("signIn")}</Text>
</Flex>
</Link>
<LoginButton />
)}
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@ -0,0 +1,28 @@
import * as RadixTooltip from "@radix-ui/react-tooltip";
import { Card, Text, Theme } from "@radix-ui/themes";
type TTooltipProps = {
text: string;
} & React.PropsWithChildren;
export default function Tooltip(props: TTooltipProps) {
return (
<RadixTooltip.Provider>
<RadixTooltip.Root>
<RadixTooltip.Trigger>{props.children}</RadixTooltip.Trigger>
<RadixTooltip.Portal>
<RadixTooltip.Content side="top">
<RadixTooltip.Content>
<Theme panelBackground="translucent">
<Card className="p-1 -translate-y-1 w-fit h-fit animate-appearTooltip">
<Text>{props.text}</Text>
</Card>
</Theme>
</RadixTooltip.Content>
</RadixTooltip.Content>
</RadixTooltip.Portal>
</RadixTooltip.Root>
</RadixTooltip.Provider>
);
}

View File

@ -10,7 +10,8 @@ export default function AuthPageWrapper(props: React.PropsWithChildren) {
const navigate = useNavigate();
if (!user) {
navigate("/login");
navigate("/login", { replace: true });
return (
<Container size={"4"} className="mt-4">
<Text size={"7"}>{t("errors.unauthorized")}</Text>

View File

@ -1,19 +1,16 @@
import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
import {
Box,
Button,
Container,
Flex,
Separator,
Text,
} from "@radix-ui/themes";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Box, Container, Flex, Separator, Text } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { axiosLocalhost } from "../../api/axios/axios";
import BlogBox from "../../Components/BlogBox/BlogBox";
import BlogCreationDialog from "../../Components/Dialogs/BlogCreationDialog/BlogCreationDialog";
import { JSONWithInt64 } from "../../utils/idnex";
import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes";
const TAGS = Array.from({ length: 50 }).map(
(_, i, a) => `v1.2.0-beta.${a.length - i}`
);
export default function UserBlogsPage() {
const { data, isPending, isFetching } = useQuery({
queryKey: ["userBlogs"],
@ -36,15 +33,24 @@ export default function UserBlogsPage() {
);
return (
<Box className="size-full">
<Container size={"1"}>
<Flex direction={"column"} gap={"2"}>
<Box className="w-full max-h-full overflow-hidden">
{/* <Container size={"2"} className="w-full h-full max-h-full"> */}
<Flex
id="currentTestBox"
direction={"column"}
gap={"2"}
className="max-h-full pb-2 mx-80"
>
<Text size={"9"} className="text-center">
Your blogs
</Text>
<Separator size={"4"} className="my-2" />
<div className="overflow-hidden">
<ScrollArea.Root className="w-full overflow-hidden h-fit max-h-">
<ScrollArea.Viewport className="size-full">
<Flex direction={"column"} gap={"2"}>
{data
? data?.map((blog: any, b) => {
return (
@ -59,68 +65,23 @@ export default function UserBlogsPage() {
);
})
: null}
<Dialog.Root>
<Dialog.Trigger asChild>
<Button onClick={() => {}}>
<PlusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-blackA6 data-[state=open]:animate-overlayShow" />
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-contentShow">
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
Create blog
</Dialog.Title>
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
Create your new blog.
</Dialog.Description>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="w-[90px] text-right text-[15px] text-violet11"
htmlFor="title"
>
Blog title
</label>
<input
className="inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
id="title"
defaultValue="My blog"
/>
</fieldset>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="w-[90px] text-right text-[15px] text-violet11"
htmlFor="Description"
>
Description
</label>
<textarea
className="pt-2 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
id="Description"
placeholder="Your description..."
/>
</fieldset>
<div className="mt-[25px] flex justify-end">
<Dialog.Close asChild>
<Button>
Create blog
</Button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</Flex>
</Container>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
orientation="vertical"
color="gray"
className="rounded-xl data-[state=visible]:w-1"
>
<ScrollArea.Thumb className="relative flex-1 bg-gray-600 rounded-xl" />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</div>
<BlogCreationDialog />
</Flex>
{/* </Container> */}
</Box>
);
}

View File

@ -58,6 +58,7 @@ export default function MainPage() {
</div>
) : (
<Flex
id="mainFlex"
direction={"column"}
className="min-h-[100vh] max-h-[100vh] overflow-hidden"
>

13
enshi/src/utils/logout.ts Normal file
View File

@ -0,0 +1,13 @@
import { axiosLocalhost } from "../api/axios/axios";
export async function logout() {
try {
let response = await axiosLocalhost.delete('/auth/logout')
console.log(`Logout successful`)
return true;
}
catch (error) {
console.error(error)
return false;
}
}

View File

@ -11,6 +11,7 @@ export default {
'times': "Times New Roman"
},
animation: {
appearTooltip: "appear 0.1s",
appear: "appear 0.25s",
widthOut: "widthOut cubic-bezier(0.4, 0, 0.6, 1) 0.4s",
slideFromRight: "slideFromRight cubic-bezier(0.4, 0, 0.6, 1) 0.2s",
@ -34,6 +35,7 @@ export default {
}
},
appear: {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
widthOut: {

View File

@ -25,3 +25,15 @@ func SetCookie(c *gin.Context, params *CookieParams) {
params.HttpOnly,
)
}
func RemoveCookie(c *gin.Context, params *CookieParams) {
c.SetCookie(
params.Name,
params.Value,
-1,
params.Path,
params.Domain,
params.Secure,
params.HttpOnly,
)
}

View File

@ -1,15 +1,12 @@
FROM golang:1.23.3-alpine3.20
FROM golang:1.23.3-alpine3.20 AS builder
WORKDIR /enshi_app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o enshi_bin .
FROM alpine
COPY --from=builder /enshi_app/enshi_bin /usr/local/bin/enshi_bin
EXPOSE 9876
CMD [ "./enshi_bin" ]
ENTRYPOINT [ "/usr/local/bin/enshi_bin" ]

View File

@ -12,7 +12,7 @@ func CORSMiddleware() gin.HandlerFunc {
"authorization, Authorization, accept, origin, Cache-Control, "+
"X-Requested-With, Cookie",
)
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
c.Writer.Header().Set("Access-Control-Expose-Headers", "Access-Token, Uid, Authorization")
if c.Request.Method == "OPTIONS" {

View File

@ -0,0 +1,13 @@
package authRoutes
import (
"enshi/global"
"net/http"
"github.com/gin-gonic/gin"
)
func Logout(c *gin.Context) {
c.SetCookie("auth_cookie", "", -1, "/", global.DomainForCookies, false, true)
c.IndentedJSON(http.StatusOK, gin.H{"message": "you have been logged out"})
}

View File

@ -123,7 +123,7 @@ func SetupRotes(g *gin.Engine) error {
)
blogGroup := g.Group("/")
blogGroup.Use(middleware.BlogsMiddleware())
blogGroup.Use(MiddlewareProvider.GetMiddleware(BLOG_MIDDLEWARE))
blogGroup.POST(
"blogs",
@ -204,6 +204,11 @@ func SetupRotes(g *gin.Engine) error {
authGroup.Use(middleware.AuthMiddleware())
authGroup.GET("check", testAuth)
authGroup.DELETE(
"logout",
authRoutes.Logout,
)
temporal := g.Group("/")
temporal.Use(middleware.AuthMiddleware())

470
package-lock.json generated Normal file
View File

@ -0,0 +1,470 @@
{
"name": "Enshi_v2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@radix-ui/react-tooltip": "^1.1.7"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz",
"integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz",
"integrity": "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.4",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.1",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
"integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT",
"peer": true
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"@radix-ui/react-tooltip": "^1.1.7"
}
}