From 854094128a12fc9f51ab2b1f2560404dd0d02b3a Mon Sep 17 00:00:00 2001 From: D0dii Date: Tue, 3 Sep 2024 21:06:14 +0200 Subject: [PATCH 1/8] feat:add plan sharing --- src/components/ReadonlyClassBlock.tsx | 69 +++++++++++++++++++++ src/components/ReadonlyClassSchedule.tsx | 77 ++++++++++++++++++++++++ src/components/ReadonlyScheduleTest.tsx | 20 ++++++ src/lib/sharingUtils.ts | 9 +++ src/pages/createplan/[id].tsx | 16 +++++ src/pages/shareplan/[hash].tsx | 35 +++++++++++ 6 files changed, 226 insertions(+) create mode 100644 src/components/ReadonlyClassBlock.tsx create mode 100644 src/components/ReadonlyClassSchedule.tsx create mode 100644 src/components/ReadonlyScheduleTest.tsx create mode 100644 src/lib/sharingUtils.ts create mode 100644 src/pages/shareplan/[hash].tsx diff --git a/src/components/ReadonlyClassBlock.tsx b/src/components/ReadonlyClassBlock.tsx new file mode 100644 index 0000000..416ade9 --- /dev/null +++ b/src/components/ReadonlyClassBlock.tsx @@ -0,0 +1,69 @@ +import React from "react"; + +import { cn } from "@/lib/utils"; + +const typeClasses = { + W: "bg-red-300", + L: "bg-blue-300", + C: "bg-green-300", + S: "bg-orange-300", + P: "bg-fuchsia-200", +} as const; + +function calculatePosition(startTime: string, endTime: string) { + const [startHour, startMinute] = startTime.split(":").map(Number); + const [endHour, endMinute] = endTime.split(":").map(Number); + + const startGrid = startHour * 12 - 7 * 12 - 5 + startMinute / 5; + + const startTotalMinutes = startHour * 60 + startMinute; + const endTotalMinutes = endHour * 60 + endMinute; + + const durationSpan = (endTotalMinutes - startTotalMinutes) / 5; + + return [startGrid, durationSpan]; +} + +const ReadonlyClassBlock = ({ + startTime, + endTime, + group, + courseName, + lecturer, + week, + courseType, +}: { + startTime: string; + endTime: string; + group: string; + courseName: string; + lecturer: string; + week: "" | "TN" | "TP"; + courseType: "C" | "L" | "P" | "S" | "W"; +}) => { + const position = calculatePosition(startTime, endTime); + const [startGrid, durationSpan] = position; + return ( + + ); +}; + +export { ReadonlyClassBlock }; diff --git a/src/components/ReadonlyClassSchedule.tsx b/src/components/ReadonlyClassSchedule.tsx new file mode 100644 index 0000000..fe82651 --- /dev/null +++ b/src/components/ReadonlyClassSchedule.tsx @@ -0,0 +1,77 @@ +import React from "react"; + +import type { ClassBlockProps } from "@/lib/types"; + +import { Hour } from "./Hour"; +import { ReadonlyClassBlock } from "./ReadonlyClassBlock"; + +const upperHours = [ + "7:30", + "8:00", + "9:00", + "10:00", + "11:00", + "12:00", + "13:00", + "14:00", + "15:00", + "16:00", + "16:55", + "17:50", + "18:45", + "19:40", + "20:45", + "21:40", +] as const; + +const bottomHours = [ + "8:15", + "9:15", + "10:15", + "11:15", + "12:15", + "13:15", + "14:15", + "15:15", + "16:10", + "17:05", + "18:00", + "18:55", + "19:50", + "20:55", + "21:50", +] as const; + +const ReadonlyClassSchedule = ({ + schedule, + day, +}: { + schedule: ClassBlockProps[]; + day: string; +}) => { + return ( +
+
+ {day} +
+
+
+ {upperHours.map((hour, index) => ( + + ))} + {bottomHours.map((hour, index) => ( + + ))} +
+
+
+ {schedule.map((block, index) => ( + + ))} +
+
+
+ ); +}; + +export { ReadonlyClassSchedule }; diff --git a/src/components/ReadonlyScheduleTest.tsx b/src/components/ReadonlyScheduleTest.tsx new file mode 100644 index 0000000..0f9262e --- /dev/null +++ b/src/components/ReadonlyScheduleTest.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import { ReadonlyClassSchedule } from "@/components/ReadonlyClassSchedule"; +import type { ClassBlockProps } from "@/lib/types"; + +export const ReadonlyScheduleTest = ({ + schedule, +}: { + schedule: ClassBlockProps[]; +}) => { + return ( +
+ + + + + +
+ ); +}; diff --git a/src/lib/sharingUtils.ts b/src/lib/sharingUtils.ts new file mode 100644 index 0000000..e0fb18c --- /dev/null +++ b/src/lib/sharingUtils.ts @@ -0,0 +1,9 @@ +function encodeToBase64(input: string) { + return btoa(unescape(encodeURIComponent(input))); +} + +function decodeFromBase64(encoded: string) { + return decodeURIComponent(escape(atob(encoded))); +} + +export { encodeToBase64, decodeFromBase64 }; diff --git a/src/pages/createplan/[id].tsx b/src/pages/createplan/[id].tsx index ec9723c..128f461 100644 --- a/src/pages/createplan/[id].tsx +++ b/src/pages/createplan/[id].tsx @@ -12,6 +12,7 @@ import { Seo } from "@/components/SEO"; import { ScheduleTest } from "@/components/Schedule"; import { SelectGroups } from "@/components/SelectGroups"; import { SolvroLogo } from "@/components/SolvroLogo"; +import { encodeToBase64 } from "@/lib/sharingUtils"; import type { ClassBlockProps, Course, Registration } from "@/lib/types"; import { cn } from "@/lib/utils"; @@ -280,6 +281,18 @@ const CreatePlan = ({ ), }); }; + const sharePlan = () => { + navigator.clipboard + .writeText( + `${window.location.origin}/shareplan/${encodeToBase64(JSON.stringify(plan))}`, + ) + .then(() => { + alert("Text copied to clipboard"); + }) + .catch(() => { + alert("Failed to copy text"); + }); + }; return ( <> @@ -330,6 +343,9 @@ const CreatePlan = ({ )}
+
{ + const { hash } = context.query; + + if (typeof hash !== "string") { + throw new Error(`Invalid hash ${hash?.toString()}`); + } + const plan = JSON.parse(decodeFromBase64(hash)) as { + courses: ExtendedCourse[]; + groups: ExtendedGroup[]; + }; + return { props: { plan } }; +}) satisfies GetServerSideProps<{ + plan: { courses: ExtendedCourse[]; groups: ExtendedGroup[] }; +}>; + +const SharePlan = ({ + plan, +}: InferGetServerSidePropsType) => { + const mondaySchedule = plan.groups.filter((group) => group.isChecked); + return ( +
+ +
+ ); +}; + +export default SharePlan; From d2e027da34ba2626d1433c5351b59f2cb6701461 Mon Sep 17 00:00:00 2001 From: D0dii Date: Wed, 4 Sep 2024 12:50:16 +0200 Subject: [PATCH 2/8] feat:add plan copying --- src/components/ReadonlyScheduleTest.tsx | 2 +- src/pages/createplan/[id].tsx | 7 ++--- src/pages/plans.tsx | 2 +- src/pages/shareplan/[hash].tsx | 35 ++++++++++++++++++++++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/components/ReadonlyScheduleTest.tsx b/src/components/ReadonlyScheduleTest.tsx index 0f9262e..f0ea8b1 100644 --- a/src/components/ReadonlyScheduleTest.tsx +++ b/src/components/ReadonlyScheduleTest.tsx @@ -9,7 +9,7 @@ export const ReadonlyScheduleTest = ({ schedule: ClassBlockProps[]; }) => { return ( -
+
diff --git a/src/pages/createplan/[id].tsx b/src/pages/createplan/[id].tsx index 128f461..e967690 100644 --- a/src/pages/createplan/[id].tsx +++ b/src/pages/createplan/[id].tsx @@ -223,7 +223,8 @@ export const planFamily = atomFamily( groups: mockGroups.map((mockGroup) => ({ ...mockGroup, isChecked: false, - })), + })) as ExtendedGroup[], + //klopot z typami, nie kumam zbytnio }, undefined, { getOnInit: true }, @@ -343,8 +344,8 @@ const CreatePlan = ({ )}
-
>("plansIds", []); +export const plansIds = atomWithStorage>("plansIds", []); const plansAtom = atom( (get) => get(plansIds).map((id) => get(planFamily(id))), diff --git a/src/pages/shareplan/[hash].tsx b/src/pages/shareplan/[hash].tsx index 12077fb..340ea4f 100644 --- a/src/pages/shareplan/[hash].tsx +++ b/src/pages/shareplan/[hash].tsx @@ -1,9 +1,13 @@ +import { useAtom } from "jotai"; import type { GetServerSideProps, InferGetServerSidePropsType } from "next"; +import { useRouter } from "next/router"; import { ReadonlyScheduleTest } from "@/components/ReadonlyScheduleTest"; import { decodeFromBase64 } from "@/lib/sharingUtils"; import type { ExtendedCourse, ExtendedGroup } from "../createplan/[id]"; +import { planFamily } from "../createplan/[id]"; +import { plansIds } from "../plans"; // eslint-disable-next-line @typescript-eslint/require-await export const getServerSideProps = (async (context) => { @@ -24,9 +28,38 @@ export const getServerSideProps = (async (context) => { const SharePlan = ({ plan, }: InferGetServerSidePropsType) => { + const [plans, setPlans] = useAtom(plansIds); + const [planToCopy, setPlanToCopy] = useAtom( + planFamily({ id: plans.length + 1 }), + ); + + const router = useRouter(); + const mondaySchedule = plan.groups.filter((group) => group.isChecked); + + const copyPlan = () => { + const newPlan = { + id: plans.length + 1, + groups: plan.groups, + courses: plan.courses, + }; + + void window.umami.track("Create plan", { + numberOfPlans: plans.length, + }); + + void router.push(`/plans`).then(() => { + setPlans([...plans, newPlan]); + setPlanToCopy({ + ...planToCopy, + courses: plan.courses, + groups: plan.groups, + }); + }); + }; return ( -
+
+
); From 182e70dabdd1e726c4246641ae91299253bbecc7 Mon Sep 17 00:00:00 2001 From: Bartosz Gotowski Date: Wed, 4 Sep 2024 13:08:15 +0200 Subject: [PATCH 3/8] fix: types --- src/pages/createplan/[id].tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pages/createplan/[id].tsx b/src/pages/createplan/[id].tsx index e967690..cb34884 100644 --- a/src/pages/createplan/[id].tsx +++ b/src/pages/createplan/[id].tsx @@ -185,9 +185,18 @@ const mondaySchedule = [ mockVoleyball2, ]; -const registrations = [mockRegistration1, mockRegistration2, mockRegistration3]; -const mockCourses = [mockCourse1, mockCourse2, mockCourse3, mockCourse4]; -const mockGroups = [ +const registrations: Registration[] = [ + mockRegistration1, + mockRegistration2, + mockRegistration3, +]; +const mockCourses: Course[] = [ + mockCourse1, + mockCourse2, + mockCourse3, + mockCourse4, +]; +const mockGroups: ClassBlockProps[] = [ mockL, mockW, mockS, @@ -223,8 +232,7 @@ export const planFamily = atomFamily( groups: mockGroups.map((mockGroup) => ({ ...mockGroup, isChecked: false, - })) as ExtendedGroup[], - //klopot z typami, nie kumam zbytnio + })), }, undefined, { getOnInit: true }, From 2f22a2029b0ba903282cafabef145f39269d19ca Mon Sep 17 00:00:00 2001 From: D0dii Date: Thu, 5 Sep 2024 10:44:34 +0200 Subject: [PATCH 4/8] fix: fix scrollbars --- src/components/ClassSchedule.tsx | 4 ++-- src/components/ReadonlyClassSchedule.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ClassSchedule.tsx b/src/components/ClassSchedule.tsx index 7630437..9f3ba40 100644 --- a/src/components/ClassSchedule.tsx +++ b/src/components/ClassSchedule.tsx @@ -61,8 +61,8 @@ const ClassSchedule = ({
{day}
-
-
+
+
{upperHours.map((hour, index) => ( ))} diff --git a/src/components/ReadonlyClassSchedule.tsx b/src/components/ReadonlyClassSchedule.tsx index fe82651..e19b5d3 100644 --- a/src/components/ReadonlyClassSchedule.tsx +++ b/src/components/ReadonlyClassSchedule.tsx @@ -54,8 +54,8 @@ const ReadonlyClassSchedule = ({
{day}
-
-
+
+
{upperHours.map((hour, index) => ( ))} From ffdd9db7eeaa52247d3920a9b0d76ee642b0f9c9 Mon Sep 17 00:00:00 2001 From: D0dii Date: Thu, 5 Sep 2024 10:46:15 +0200 Subject: [PATCH 5/8] fix: fix styling on share plan page --- src/components/ReadonlyScheduleTest.tsx | 2 +- src/pages/createplan/[id].tsx | 90 +++++++++++++------------ src/pages/shareplan/[hash].tsx | 30 ++++++++- 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/src/components/ReadonlyScheduleTest.tsx b/src/components/ReadonlyScheduleTest.tsx index f0ea8b1..8d37dc2 100644 --- a/src/components/ReadonlyScheduleTest.tsx +++ b/src/components/ReadonlyScheduleTest.tsx @@ -9,7 +9,7 @@ export const ReadonlyScheduleTest = ({ schedule: ClassBlockProps[]; }) => { return ( -
+
diff --git a/src/pages/createplan/[id].tsx b/src/pages/createplan/[id].tsx index cb34884..b6ae6c3 100644 --- a/src/pages/createplan/[id].tsx +++ b/src/pages/createplan/[id].tsx @@ -296,10 +296,10 @@ const CreatePlan = ({ `${window.location.origin}/shareplan/${encodeToBase64(JSON.stringify(plan))}`, ) .then(() => { - alert("Text copied to clipboard"); + alert("Plan has been copied to clipboard"); }) .catch(() => { - alert("Failed to copy text"); + alert("Something went wrong :("); }); }; @@ -313,48 +313,54 @@ const CreatePlan = ({
-
-
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - changePlanName(formData.get("name")?.toString() ?? ""); - inputRef.current?.blur(); - }} - > - { - setIsEditing(true); +
+
+ { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + changePlanName(formData.get("name")?.toString() ?? ""); + inputRef.current?.blur(); }} - onBlur={(e) => { - setIsEditing(false); - changePlanName(e.currentTarget.value); - }} - /> - {isEditing ? ( - - ) : ( - - )} - + > + { + setIsEditing(true); + }} + onBlur={(e) => { + setIsEditing(false); + changePlanName(e.currentTarget.value); + }} + /> + {isEditing ? ( + + ) : ( + + )} + +
+
- +
- +
+
+
+ +
+
+
+ +
+
+ +
+ Picture of the author +
+
); From de0e5ee8ef37b9c2e0954fbcc2db65462a0be050 Mon Sep 17 00:00:00 2001 From: D0dii Date: Thu, 5 Sep 2024 14:45:16 +0200 Subject: [PATCH 6/8] feat: add responsive share link dialog --- package-lock.json | 309 +++++++++++++++++++ package.json | 2 + public/copy.svg | 1 + src/components/SharePlanResponsiveDialog.tsx | 96 ++++++ src/components/ui/dialog.tsx | 124 ++++++++ src/components/ui/drawer.tsx | 120 +++++++ src/components/ui/input.tsx | 27 ++ src/lib/useMediaQuery.ts | 21 ++ src/pages/createplan/[id].tsx | 24 +- 9 files changed, 705 insertions(+), 19 deletions(-) create mode 100644 public/copy.svg create mode 100644 src/components/SharePlanResponsiveDialog.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/lib/useMediaQuery.ts diff --git a/package-lock.json b/package-lock.json index 4516691..6f3966c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.1.0", "@t3-oss/env-nextjs": "^0.11.0", @@ -30,6 +31,7 @@ "sharp": "^0.33.5", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.2", "zod": "^3.23.8" }, "devDependencies": { @@ -2899,6 +2901,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "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-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -2914,6 +2952,73 @@ } } }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@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-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "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-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "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-icons": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", @@ -2941,6 +3046,30 @@ } } }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@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.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", @@ -3039,6 +3168,24 @@ } } }, + "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", @@ -4150,6 +4297,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -5315,6 +5474,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6664,6 +6829,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -7106,6 +7280,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -9296,6 +9479,76 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -10686,12 +10939,68 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vaul": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.2.tgz", + "integrity": "sha512-m2A7UgAU/JMWiwUhmARK8LMvEfXiudA4trJxfZF5AtH2uBTgN855msZ2yjPnUDfa7i5glocMYLSfML8wriBtBA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.0.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/vite": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", diff --git a/package.json b/package.json index 0ec03ac..30f09e9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.1.0", "@t3-oss/env-nextjs": "^0.11.0", @@ -38,6 +39,7 @@ "sharp": "^0.33.5", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.2", "zod": "^3.23.8" }, "devDependencies": { diff --git a/public/copy.svg b/public/copy.svg new file mode 100644 index 0000000..f3b629c --- /dev/null +++ b/public/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/SharePlanResponsiveDialog.tsx b/src/components/SharePlanResponsiveDialog.tsx new file mode 100644 index 0000000..6166a08 --- /dev/null +++ b/src/components/SharePlanResponsiveDialog.tsx @@ -0,0 +1,96 @@ +import Image from "next/image"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { Input } from "@/components/ui/input"; +import { useMediaQuery } from "@/lib/useMediaQuery"; + +export const SharePlanResponsiveDialog = ({ hash }: { hash: string }) => { + const [open, setOpen] = React.useState(false); + const isDesktop = useMediaQuery("(min-width: 768px)"); + if (isDesktop) { + return ( + + + + + + + Udostępnij plan + Skopiuj link + + + + + ); + } + + return ( + + + + + + + Udostępnij plan + Skopiuj link + + + + + + + + + + ); +}; + +function CopyLink({ hash }: { hash: string }) { + const sharePlan = () => { + navigator.clipboard + .writeText(`${window.location.origin}/shareplan/${hash}`) + .catch(() => { + alert("Something went wrong :("); + }); + }; + return ( +
+ + +
+ ); +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..e03f29b --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,124 @@ +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..987d5e2 --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -0,0 +1,120 @@ +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; + +import { cn } from "@/lib/utils"; + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +); +Drawer.displayName = "Drawer"; + +const DrawerTrigger = DrawerPrimitive.Trigger; + +const DrawerPortal = DrawerPrimitive.Portal; + +const DrawerClose = DrawerPrimitive.Close; + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)); +DrawerContent.displayName = "DrawerContent"; + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DrawerHeader.displayName = "DrawerHeader"; + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DrawerFooter.displayName = "DrawerFooter"; + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + +)); +DrawerDescription.displayName = DrawerPrimitive.Description.displayName; + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..86f60b0 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + // eslint-disable-next-line react/prop-types + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/lib/useMediaQuery.ts b/src/lib/useMediaQuery.ts new file mode 100644 index 0000000..b4ce143 --- /dev/null +++ b/src/lib/useMediaQuery.ts @@ -0,0 +1,21 @@ +import * as React from "react"; + +export const useMediaQuery = (query: string) => { + const [value, setValue] = React.useState(false); + + React.useEffect(() => { + function onChange(event: MediaQueryListEvent) { + setValue(event.matches); + } + + const result = matchMedia(query); + result.addEventListener("change", onChange); + setValue(result.matches); + + return () => { + result.removeEventListener("change", onChange); + }; + }, [query]); + + return value; +}; diff --git a/src/pages/createplan/[id].tsx b/src/pages/createplan/[id].tsx index b6ae6c3..f7df7a9 100644 --- a/src/pages/createplan/[id].tsx +++ b/src/pages/createplan/[id].tsx @@ -11,6 +11,7 @@ import { IoCheckmarkOutline } from "react-icons/io5"; import { Seo } from "@/components/SEO"; import { ScheduleTest } from "@/components/Schedule"; import { SelectGroups } from "@/components/SelectGroups"; +import { SharePlanResponsiveDialog } from "@/components/SharePlanResponsiveDialog"; import { SolvroLogo } from "@/components/SolvroLogo"; import { encodeToBase64 } from "@/lib/sharingUtils"; import type { ClassBlockProps, Course, Registration } from "@/lib/types"; @@ -290,18 +291,6 @@ const CreatePlan = ({ ), }); }; - const sharePlan = () => { - navigator.clipboard - .writeText( - `${window.location.origin}/shareplan/${encodeToBase64(JSON.stringify(plan))}`, - ) - .then(() => { - alert("Plan has been copied to clipboard"); - }) - .catch(() => { - alert("Something went wrong :("); - }); - }; return ( <> @@ -313,7 +302,7 @@ const CreatePlan = ({
-
+
- +
From 2032eb3256e90469fed088a7bf7c693f4ac3b5f6 Mon Sep 17 00:00:00 2001 From: D0dii Date: Thu, 5 Sep 2024 14:51:15 +0200 Subject: [PATCH 7/8] fix: fix input props component error --- src/components/ui/input.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 86f60b0..4d09fd3 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -2,9 +2,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface InputProps - extends React.InputHTMLAttributes {} +export type InputProps = React.InputHTMLAttributes; const Input = React.forwardRef( // eslint-disable-next-line react/prop-types From 2acbc934d1ac5d13e1e5d56e1f1c24f12013512e Mon Sep 17 00:00:00 2001 From: D0dii Date: Thu, 5 Sep 2024 15:23:30 +0200 Subject: [PATCH 8/8] fix: fix drawer styling --- src/components/SharePlanResponsiveDialog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SharePlanResponsiveDialog.tsx b/src/components/SharePlanResponsiveDialog.tsx index 6166a08..d22b05f 100644 --- a/src/components/SharePlanResponsiveDialog.tsx +++ b/src/components/SharePlanResponsiveDialog.tsx @@ -55,13 +55,13 @@ export const SharePlanResponsiveDialog = ({ hash }: { hash: string }) => { Udostępnij plan - - + + Udostępnij plan Skopiuj link - + @@ -80,7 +80,7 @@ function CopyLink({ hash }: { hash: string }) { }); }; return ( -
+