From c1255ee62e138f9ebe07d8e8c1105e6203b51b43 Mon Sep 17 00:00:00 2001 From: polubis Date: Wed, 5 Nov 2025 08:10:32 +0100 Subject: [PATCH 1/6] wip --- package-lock.json | 52 +++---- package.json | 2 +- src/api-4markdown-contracts/contracts.ts | 6 +- src/api-4markdown-contracts/dtos.ts | 6 +- src/components/judge-score.tsx | 137 ++++++++++++++++++ src/components/rate-picker.tsx | 96 ++++++++++++ src/containers/document-rating.container.tsx | 62 +------- .../user-profile-stats.container.tsx | 32 +++- .../components/document-comments-list.tsx | 31 ++-- 9 files changed, 315 insertions(+), 109 deletions(-) create mode 100644 src/components/judge-score.tsx create mode 100644 src/components/rate-picker.tsx diff --git a/package-lock.json b/package-lock.json index e07572af5..f5ec7e4a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "classnames": "^2.5.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "firebase": "^12.4.0", + "firebase": "^12.5.0", "gatsby": "^5.15.0", "gatsby-plugin-manifest": "^5.15.0", "gatsby-plugin-mdx": "^5.15.0", @@ -2773,9 +2773,9 @@ "license": "MIT" }, "node_modules/@firebase/ai": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.4.0.tgz", - "integrity": "sha512-YilG6AJ/nYpCKtxZyvEzBRAQv5bU+2tBOKX4Ps0rNNSdxN39aT37kGhjATbk1kq1z5Lq7mkWglw/ajAF3lOWUg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.5.0.tgz", + "integrity": "sha512-OXv/jZLRjV9jTejWA4KOvW8gM1hNsLvQSCPwKhi2CEfe0Nap3rM6z+Ial0PGqXga0WgzhpypEvJOFvaAUFX3kg==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", @@ -2831,9 +2831,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz", - "integrity": "sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.5.tgz", + "integrity": "sha512-zyNY77xJOGwcuB+xCxF8z8lSiHvD4ox7BCsqLEHEvgqQoRjxFZ0fkROR6NV5QyXmCqRLodMM8J5d2EStOocWIw==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.7.0", @@ -2897,12 +2897,12 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app-compat": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.4.tgz", - "integrity": "sha512-T7ifGmb+awJEcp542Ek4HtNfBxcBrnuk1ggUdqyFEdsXHdq7+wVlhvE6YukTL7NS8hIkEfL7TMAPx/uCNqt30g==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.5.tgz", + "integrity": "sha512-lVG/nRnXaot0rQSZazmTNqy83ti9O3+kdwoaE0d5wahRIWNoDirbIMcGVjDDgdmf4IE6FYreWOMh0L3DV1475w==", "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", @@ -2919,9 +2919,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/auth": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", - "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", + "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.7.0", @@ -2943,12 +2943,12 @@ } }, "node_modules/@firebase/auth-compat": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.0.tgz", - "integrity": "sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.1.tgz", + "integrity": "sha512-I0o2ZiZMnMTOQfqT22ur+zcGDVSAfdNZBHo26/Tfi8EllfR1BO7aTVo2rt/ts8o/FWsK8pOALLeVBGhZt8w/vg==", "license": "Apache-2.0", "dependencies": { - "@firebase/auth": "1.11.0", + "@firebase/auth": "1.11.1", "@firebase/auth-types": "0.13.0", "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", @@ -14277,21 +14277,21 @@ } }, "node_modules/firebase": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.4.0.tgz", - "integrity": "sha512-/chNgDQ6ppPPGOQO4jctxOa/5JeQxuhaxA7Y90K0I+n/wPfoO8mRveedhVUdo7ExLcWUivnnow/ouSLYSI5Icw==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.5.0.tgz", + "integrity": "sha512-Ak8JcpH7FL6kiv0STwkv5+3CYEROO9iFWSx7OCZVvc4kIIABAIyAGs1mPGaHRxGUIApFZdMCXA7baq17uS6Mow==", "license": "Apache-2.0", "dependencies": { - "@firebase/ai": "2.4.0", + "@firebase/ai": "2.5.0", "@firebase/analytics": "0.10.19", "@firebase/analytics-compat": "0.2.25", - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "@firebase/app-check": "0.11.0", "@firebase/app-check-compat": "0.4.0", - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.11.0", - "@firebase/auth-compat": "0.6.0", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", "@firebase/data-connect": "0.3.11", "@firebase/database": "1.1.0", "@firebase/database-compat": "2.1.0", diff --git a/package.json b/package.json index 8e4378427..42f62054e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "classnames": "^2.5.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "firebase": "^12.4.0", + "firebase": "^12.5.0", "gatsby": "^5.15.0", "gatsby-plugin-manifest": "^5.15.0", "gatsby-plugin-mdx": "^5.15.0", diff --git a/src/api-4markdown-contracts/contracts.ts b/src/api-4markdown-contracts/contracts.ts index 114ca6406..842ea079e 100644 --- a/src/api-4markdown-contracts/contracts.ts +++ b/src/api-4markdown-contracts/contracts.ts @@ -10,7 +10,7 @@ import type { import { AccessGroupDto, Atoms, - CommentDto, + UserProfileCommentDto, FullMindmapDto, ImageDto, MindmapDto, @@ -185,7 +185,7 @@ type UserProfilesContracts = `getUserProfile`, { profile: UserProfileDto; - comments: CommentDto[]; + comments: UserProfileCommentDto[]; }, { profileId: Atoms["UserProfileId"]; @@ -193,7 +193,7 @@ type UserProfilesContracts = > | Contract< `addUserProfileComment`, - CommentDto, + UserProfileCommentDto, { receiverProfileId: Atoms["UserProfileId"]; comment: string; diff --git a/src/api-4markdown-contracts/dtos.ts b/src/api-4markdown-contracts/dtos.ts index cffb60f92..5955d2981 100644 --- a/src/api-4markdown-contracts/dtos.ts +++ b/src/api-4markdown-contracts/dtos.ts @@ -18,7 +18,7 @@ export type Atoms = { Slug: Brand; Url: Brand; RewriteAssistantPersona: "cleany" | "grammy" | "teacher"; - CommentId: Brand; + UserProfileCommentId: Brand; AvatarVariantId: Brand; AvatarVariant: { w: number; @@ -74,9 +74,9 @@ export type UserProfileDto = { blogUrl: Atoms["Url"] | null; }; -export type CommentDto = Prettify< +export type UserProfileCommentDto = Prettify< Atoms["Rating"] & { - id: Atoms["CommentId"]; + id: Atoms["UserProfileCommentId"]; ownerProfile: UserProfileDto; cdate: Atoms["UTCDate"]; mdate: Atoms["UTCDate"]; diff --git a/src/components/judge-score.tsx b/src/components/judge-score.tsx new file mode 100644 index 000000000..0ad72173d --- /dev/null +++ b/src/components/judge-score.tsx @@ -0,0 +1,137 @@ +import { c } from "design-system/c"; +import React from "react"; +import Popover from "design-system/popover"; +import { useSimpleFeature } from "@greenonsoftware/react-kit"; + +type JudgeScoreProps = { + score?: number | null; + votes?: number | null; + className?: string; + children?: React.ReactNode; + onRate?: (rate: number) => void; +} & React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement +>; + +const getGradientClasses = (score: number): string => { + if (score === 0) { + return "bg-gradient-to-r from-gray-400 via-gray-300 to-gray-400 dark:from-gray-800 dark:via-gray-700 dark:to-gray-800"; + } + + const normalizedScore = Math.max(0, Math.min(10, score)); + + if (normalizedScore <= 2) { + return "bg-gradient-to-r from-red-400 via-red-300 to-orange-400 dark:from-red-900 dark:via-red-800 dark:to-orange-900"; + } else if (normalizedScore <= 4) { + return "bg-gradient-to-r from-red-300 via-orange-400 to-orange-300 dark:from-red-800 dark:via-orange-900 dark:to-orange-800"; + } else if (normalizedScore <= 6) { + return "bg-gradient-to-r from-orange-300 via-yellow-300 to-yellow-200 dark:from-orange-800 dark:via-amber-800 dark:to-yellow-700"; + } else if (normalizedScore <= 8) { + return "bg-gradient-to-r from-yellow-300 via-lime-300 to-green-400 dark:from-yellow-700 dark:via-lime-700 dark:to-green-800"; + } else { + return "bg-gradient-to-r from-green-400 via-green-300 to-emerald-400 dark:from-green-800 dark:via-green-700 dark:to-emerald-800"; + } +}; + +const JudgeScore = ({ + score, + votes, + className, + children, + onRate, + type = "button", + onClick, + ...props +}: JudgeScoreProps) => { + const panel = useSimpleFeature(); + const finalScore = score ?? 0; + const gradientClasses = getGradientClasses(finalScore); + const displayText = finalScore === 0 ? "N/A" : `${finalScore}/10`; + const finalVotes = votes ?? 0; + + const handleClick = (e: React.MouseEvent) => { + panel.on(); + onClick?.(e); + }; + + const handleRateClick = (rate: number) => { + onRate?.(rate); + panel.off(); + }; + + return ( +
+ + {panel.isOn && ( + + {Array.from({ length: 10 }, (_, i) => { + const rate = i + 1; + const rateGradientClasses = getGradientClasses(rate); + return ( + + ); + })} + + )} +
+ ); +}; + +export { JudgeScore }; diff --git a/src/components/rate-picker.tsx b/src/components/rate-picker.tsx new file mode 100644 index 000000000..39a83a002 --- /dev/null +++ b/src/components/rate-picker.tsx @@ -0,0 +1,96 @@ +import { Atoms } from "api-4markdown-contracts"; +import { RATING_ICONS } from "core/rating-config"; +import { Button } from "design-system/button"; +import { c } from "design-system/c"; +import React from "react"; + +type RatePickerProps = { + className?: string; + rating: Record; + rate: Atoms["RatingCategory"] | null; + onRate: (category: Atoms["RatingCategory"], index: number) => void; +}; + +const NOTES = [ + { name: `C4`, frequency: 261.63 }, + { name: `D4`, frequency: 293.66 }, + { name: `E4`, frequency: 329.63 }, + { name: `F4`, frequency: 349.23 }, + { name: `G4`, frequency: 392.0 }, +]; + +let audioContextInstance: AudioContext | null = null; + +const getAudioContext = (): AudioContext | null => { + if (typeof window === "undefined") return null; + return audioContextInstance; +}; + +const initializeAudioContext = (): void => { + if (typeof window === "undefined" || audioContextInstance) return; + + const AudioContextClass = + window.AudioContext || (window as any).webkitAudioContext; + if (!AudioContextClass) return; + + audioContextInstance = new AudioContextClass(); +}; + +const playNote = (frequency: number): void => { + const audioContext = getAudioContext(); + if (!audioContext) return; + + if (audioContext.state === "suspended") { + audioContext.resume(); + } + + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + const now = audioContext.currentTime; + + oscillator.type = `sine`; + oscillator.frequency.setValueAtTime(frequency, now); + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + gainNode.gain.setValueAtTime(0, now); + gainNode.gain.linearRampToValueAtTime(0.3, now + 0.01); + gainNode.gain.exponentialRampToValueAtTime(0.00001, now + 0.15); + + oscillator.start(now); + oscillator.stop(now + 0.15); +}; + +const RatePicker = ({ className, rate, rating, onRate }: RatePickerProps) => { + React.useEffect(() => { + initializeAudioContext(); + }, []); + + const rateAndPlay = async ( + category: Atoms["RatingCategory"], + index: number, + ): Promise => { + playNote(NOTES[index].frequency); + onRate(category, index); + }; + + return ( +
+ {RATING_ICONS.map(([Icon, category], idx) => ( + + ))} +
+ ); +}; + +export { RatePicker }; diff --git a/src/containers/document-rating.container.tsx b/src/containers/document-rating.container.tsx index 02c13de34..7a9520c12 100644 --- a/src/containers/document-rating.container.tsx +++ b/src/containers/document-rating.container.tsx @@ -1,11 +1,9 @@ import React from "react"; -import c from "classnames"; -import { Button } from "design-system/button"; -import { RATING_ICONS } from "core/rating-config"; import { useDocumentLayoutContext } from "providers/document-layout.provider"; import { rateDocumentAct } from "acts/rate-document.act"; import throttle from "lodash.throttle"; import { Atoms } from "api-4markdown-contracts"; +import { RatePicker } from "components/rate-picker"; type DocumentRatingContainerProps = { className?: string; @@ -13,35 +11,6 @@ type DocumentRatingContainerProps = { const rateDocument = throttle(rateDocumentAct, 5000); -const NOTES = [ - { name: `C4`, frequency: 261.63 }, - { name: `D4`, frequency: 293.66 }, - { name: `E4`, frequency: 329.63 }, - { name: `F4`, frequency: 349.23 }, - { name: `G4`, frequency: 392.0 }, -]; - -const playNote = (frequency: number): void => { - const audioContext = new (window.AudioContext || window.webkitAudioContext)(); - - if (!audioContext) return; - - const oscillator = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); - - oscillator.type = `sine`; - oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); - oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); - - oscillator.start(); - gainNode.gain.exponentialRampToValueAtTime( - 0.00001, - audioContext.currentTime + 1, - ); - oscillator.stop(audioContext.currentTime + 1); -}; - const DocumentRatingContainer = ({ className, }: DocumentRatingContainerProps) => { @@ -50,9 +19,7 @@ const DocumentRatingContainer = ({ const handleClick = async ( category: Atoms["RatingCategory"], - index: number, ): Promise => { - playNote(NOTES[index].frequency); rateDocument({ documentId: document.id, category }); setDocumentLayoutState(({ document, yourRate }) => { if (yourRate === null) { @@ -95,28 +62,13 @@ const DocumentRatingContainer = ({ }); }; - React.useEffect(() => { - return () => { - rateDocument.cancel(); - }; - }, []); - return ( -
- {RATING_ICONS.map(([Icon, category], idx) => ( - - ))} -
+ ); }; diff --git a/src/features/user-profile-preview/containers/user-profile-stats.container.tsx b/src/features/user-profile-preview/containers/user-profile-stats.container.tsx index 3585f82b2..921a2e30b 100644 --- a/src/features/user-profile-preview/containers/user-profile-stats.container.tsx +++ b/src/features/user-profile-preview/containers/user-profile-stats.container.tsx @@ -4,6 +4,11 @@ import { UserSocials } from "components/user-socials"; import { formatDistance } from "date-fns"; import { AddCommentTriggerContainer } from "./add-comment-trigger.container"; import { useUserProfileState } from "../store"; +import { RatePicker } from "components/rate-picker"; +import { JudgeScore } from "components/judge-score"; +import throttle from "lodash.throttle"; + +const rateCommentThrottled = throttle(() => {}, 5000); const UserProfileStatsContainer = () => { const { stats } = useUserProfileState(); @@ -54,7 +59,26 @@ const UserProfileStatsContainer = () => { )} -
+
+

Rating

+
+ {}} + /> +
+

Trust Score

+ +
+
{comments.length > 0 && ( @@ -88,6 +112,12 @@ const UserProfileStatsContainer = () => {

{comment.content}

+ {}} + /> ))} diff --git a/src/modules/document-comments/components/document-comments-list.tsx b/src/modules/document-comments/components/document-comments-list.tsx index f62abbaf1..5d43127e5 100644 --- a/src/modules/document-comments/components/document-comments-list.tsx +++ b/src/modules/document-comments/components/document-comments-list.tsx @@ -2,7 +2,6 @@ import React, { useState } from "react"; import { BiPencil, BiTrash } from "react-icons/bi"; import { Avatar } from "design-system/avatar"; import { formatDistance } from "date-fns"; -import { RATING_ICONS } from "core/rating-config"; import { Button } from "design-system/button"; import throttle from "lodash.throttle"; import { Atoms, DocumentCommentDto } from "api-4markdown-contracts"; @@ -11,6 +10,7 @@ import { useFeature } from "@greenonsoftware/react-kit"; import { DocumentCommentDeleteModalContainer } from "../containers/document-comment-delete-modal.container"; import { rateDocumentCommentAct } from "../acts/rate-document-comment.act"; import { useDocumentCommentsContext } from "../providers/document-comments.provider"; +import { RatePicker } from "components/rate-picker"; const rateCommentThrottled = throttle(rateDocumentCommentAct, 5000); @@ -74,25 +74,16 @@ const DocumentCommentsList = ({

{comment.content}

-
- {RATING_ICONS.map(([Icon, category]) => ( - - ))} -
+ rateComment(category, comment.id)} + /> {comment.ownerProfile.id === userProfileId && (
+ )} +
+ {children && ( +
+ {children} +
+ )} + + ); + + if (mode === "outside") { + return render(content); + } + + return content; + }, +); + +const ToastSlot = () => { + const allToasts = React.useSyncExternalStore(subscribe, getSnapshot); + + if (allToasts.length === 0) { + return null; + } + + const groupedToasts = allToasts.reduce( + (acc, toast) => { + const position = toast.position ?? "top-center"; + if (!acc[position]) { + acc[position] = []; + } + acc[position].push(toast); + return acc; + }, + {} as Record, ActiveToast[]>, + ); + + return ( + <> + {Object.entries(groupedToasts).map(([position, toastsInGroup]) => ( + + {toastsInGroup.map(({ id, ...props }, idx) => ( + { + props.onClose?.(id); + removeToast(id); + }} + /> + ))} + + ))} + + ); +}; + +Toast.displayName = "Toast"; + +export type { ToastProps }; +export { Toast, ToastSlot, toast }; diff --git a/src/features/user-profile-preview/containers/user-profile-stats.container.tsx b/src/features/user-profile-preview/containers/user-profile-stats.container.tsx index 921a2e30b..7b2b00dfc 100644 --- a/src/features/user-profile-preview/containers/user-profile-stats.container.tsx +++ b/src/features/user-profile-preview/containers/user-profile-stats.container.tsx @@ -7,6 +7,7 @@ import { useUserProfileState } from "../store"; import { RatePicker } from "components/rate-picker"; import { JudgeScore } from "components/judge-score"; import throttle from "lodash.throttle"; +import { toast } from "design-system/toast"; const rateCommentThrottled = throttle(() => {}, 5000); @@ -72,7 +73,13 @@ const UserProfileStatsContainer = () => { ugly: 0, }} rate={null} - onRate={() => {}} + onRate={() => + toast.error({ + title: "Problem occured", + children: + "Something went wrong. Realy long text to see how it works. and verify that the text is not cut off. adsa adas asa sadsa", + }) + } />

Trust Score

From 60efe881ab9bb1fdade92b43eaba08096ca47911 Mon Sep 17 00:00:00 2001 From: polubis Date: Wed, 5 Nov 2025 11:18:49 +0100 Subject: [PATCH 3/6] wip --- src/components/judge-score.tsx | 27 +++- src/components/rate-picker.tsx | 63 ++------ src/design-system/toast.tsx | 2 +- src/development-kit/play-note.ts | 152 ++++++++++++++++++ .../user-profile-stats.container.tsx | 8 +- 5 files changed, 187 insertions(+), 65 deletions(-) create mode 100644 src/development-kit/play-note.ts diff --git a/src/components/judge-score.tsx b/src/components/judge-score.tsx index 0ad72173d..e70fbf0d2 100644 --- a/src/components/judge-score.tsx +++ b/src/components/judge-score.tsx @@ -2,6 +2,8 @@ import { c } from "design-system/c"; import React from "react"; import Popover from "design-system/popover"; import { useSimpleFeature } from "@greenonsoftware/react-kit"; +import { toast } from "design-system/toast"; +import { playNote } from "development-kit/play-note"; type JudgeScoreProps = { score?: number | null; @@ -34,6 +36,19 @@ const getGradientClasses = (score: number): string => { } }; +const JUDGE_SCORE_NOTES = [ + "c4", + "d4", + "e4", + "f4", + "g4", + "g#4", + "a4", + "a#4", + "b4", + "c5", +] as const; + const JudgeScore = ({ score, votes, @@ -55,8 +70,14 @@ const JudgeScore = ({ onClick?.(e); }; - const handleRateClick = (rate: number) => { + const handleRateClick = (rate: number, index: number) => { onRate?.(rate); + toast.success({ + duration: 2000, + position: "bottom-left", + title: "Rate added. Thx!", + }); + playNote(JUDGE_SCORE_NOTES[index]); panel.off(); }; @@ -105,14 +126,14 @@ const JudgeScore = ({ className="!absolute flex flex-wrap gap-2 justify-center max-w-[40rem] w-full translate-y-2.5 -translate-x-1/2 left-1/2" onBackdropClick={panel.off} > - {Array.from({ length: 10 }, (_, i) => { + {Array.from({ length: JUDGE_SCORE_NOTES.length }, (_, i) => { const rate = i + 1; const rateGradientClasses = getGradientClasses(rate); return (