mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 05:46:17 +00:00
feat: added review functionality
This commit is contained in:
@@ -40,6 +40,12 @@
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
"@tiptap/extension-bold": "^3.6.2",
|
||||
"@tiptap/extension-italic": "^3.6.2",
|
||||
"@tiptap/extension-link": "^3.6.2",
|
||||
"@tiptap/extension-underline": "^3.6.2",
|
||||
"@tiptap/react": "^3.6.2",
|
||||
"@tiptap/starter-kit": "^3.6.2",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"axios-cookiejar-support": "^5.0.5",
|
||||
@@ -63,6 +69,7 @@
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.544.0",
|
||||
"parse-torrent": "^11.0.18",
|
||||
"rc-virtual-list": "^3.18.3",
|
||||
"react-dnd": "^16.0.1",
|
||||
|
||||
@@ -198,6 +198,29 @@
|
||||
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.",
|
||||
"achievements": "Achievements",
|
||||
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
"reviews": "Reviews",
|
||||
"leave_a_review": "Leave a Review",
|
||||
"write_review_placeholder": "Share your thoughts about this game...",
|
||||
"sort_newest": "Newest",
|
||||
"sort_by": "Sort by",
|
||||
"no_reviews_yet": "No reviews yet",
|
||||
"be_first_to_review": "Be the first to share your thoughts about this game!",
|
||||
"sort_oldest": "Oldest",
|
||||
"sort_highest_score": "Highest Score",
|
||||
"sort_lowest_score": "Lowest Score",
|
||||
"sort_most_voted": "Most Voted",
|
||||
"rating": "Rating",
|
||||
"submit_review": "Submit Review",
|
||||
"submitting": "Submitting...",
|
||||
"loading_reviews": "Loading reviews...",
|
||||
"loading_more_reviews": "Loading more reviews...",
|
||||
"load_more_reviews": "Load More Reviews",
|
||||
"youve_played_for_hours": "You've played for {{hours}} hours",
|
||||
"would_you_recommend_this_game": "Would you like to leave a review to this game?",
|
||||
"yes": "Yes",
|
||||
"maybe_later": "Maybe Later",
|
||||
"cloud_save": "Cloud save",
|
||||
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
||||
"backups": "Backups",
|
||||
|
||||
17
src/main/events/catalogue/check-game-review.ts
Normal file
17
src/main/events/catalogue/check-game-review.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const checkGameReview = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
return HydraApi.get(
|
||||
`/games/${shop}/${objectId}/reviews/check`,
|
||||
null,
|
||||
{ needsAuth: true }
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("checkGameReview", checkGameReview);
|
||||
18
src/main/events/catalogue/create-game-review.ts
Normal file
18
src/main/events/catalogue/create-game-review.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const createGameReview = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewHtml: string,
|
||||
score: number
|
||||
) => {
|
||||
return HydraApi.post(`/games/${shop}/${objectId}/reviews`, {
|
||||
reviewHtml,
|
||||
score,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("createGameReview", createGameReview);
|
||||
14
src/main/events/catalogue/delete-review.ts
Normal file
14
src/main/events/catalogue/delete-review.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const deleteReview = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewId: string
|
||||
) => {
|
||||
return HydraApi.delete(`/games/${shop}/${objectId}/reviews/${reviewId}`);
|
||||
};
|
||||
|
||||
registerEvent("deleteReview", deleteReview);
|
||||
26
src/main/events/catalogue/get-game-reviews.ts
Normal file
26
src/main/events/catalogue/get-game-reviews.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const getGameReviews = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
take: number = 20,
|
||||
skip: number = 0,
|
||||
sortBy: string = "newest"
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
take: take.toString(),
|
||||
skip: skip.toString(),
|
||||
sortBy,
|
||||
});
|
||||
|
||||
return HydraApi.get(
|
||||
`/games/${shop}/${objectId}/reviews?${params.toString()}`,
|
||||
null,
|
||||
{ needsAuth: false }
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("getGameReviews", getGameReviews);
|
||||
15
src/main/events/catalogue/vote-review.ts
Normal file
15
src/main/events/catalogue/vote-review.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const voteReview = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewId: string,
|
||||
voteType: 'upvote' | 'downvote'
|
||||
) => {
|
||||
return HydraApi.put(`/games/${shop}/${objectId}/reviews/${reviewId}/${voteType}`, {});
|
||||
};
|
||||
|
||||
registerEvent("voteReview", voteReview);
|
||||
@@ -11,6 +11,11 @@ import "./catalogue/get-game-stats";
|
||||
import "./catalogue/get-trending-games";
|
||||
import "./catalogue/get-publishers";
|
||||
import "./catalogue/get-developers";
|
||||
import "./catalogue/create-game-review";
|
||||
import "./catalogue/get-game-reviews";
|
||||
import "./catalogue/vote-review";
|
||||
import "./catalogue/delete-review";
|
||||
import "./catalogue/check-game-review";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
|
||||
@@ -77,6 +77,32 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
getGameStats: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
||||
createGameReview: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewHtml: string,
|
||||
score: number
|
||||
) => ipcRenderer.invoke("createGameReview", shop, objectId, reviewHtml, score),
|
||||
getGameReviews: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
take?: number,
|
||||
skip?: number,
|
||||
sortBy?: string
|
||||
) => ipcRenderer.invoke("getGameReviews", shop, objectId, take, skip, sortBy),
|
||||
voteReview: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewId: string,
|
||||
voteType: "upvote" | "downvote"
|
||||
) => ipcRenderer.invoke("voteReview", shop, objectId, reviewId, voteType),
|
||||
deleteReview: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewId: string
|
||||
) => ipcRenderer.invoke("deleteReview", shop, objectId, reviewId),
|
||||
checkGameReview: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("checkGameReview", shop, objectId),
|
||||
onUpdateAchievements: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||
import { DownloadIcon, PeopleIcon, StarIcon } from "@primer/octicons-react";
|
||||
import type { GameStats } from "@types";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
@@ -107,6 +107,14 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
{stats ? numberFormatter.format(stats.playerCount) : "…"}
|
||||
</span>
|
||||
</div>
|
||||
{stats?.averageScore && (
|
||||
<div className="game-card__specifics-item">
|
||||
<StarIcon />
|
||||
<span>
|
||||
{stats.averageScore.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
28
src/renderer/src/declaration.d.ts
vendored
28
src/renderer/src/declaration.d.ts
vendored
@@ -93,6 +93,34 @@ declare global {
|
||||
) => Promise<HowLongToBeatCategory[] | null>;
|
||||
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
||||
getTrendingGames: () => Promise<TrendingGame[]>;
|
||||
createGameReview: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewHtml: string,
|
||||
score: number
|
||||
) => Promise<void>;
|
||||
getGameReviews: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
take?: number,
|
||||
skip?: number,
|
||||
sortBy?: string
|
||||
) => Promise<any[]>;
|
||||
voteReview: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewId: string,
|
||||
voteType: 'upvote' | 'downvote'
|
||||
) => Promise<void>;
|
||||
deleteReview: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
reviewId: string
|
||||
) => Promise<void>;
|
||||
checkGameReview: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<{ hasReviewed: boolean }>;
|
||||
onUpdateAchievements: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
&__preview {
|
||||
width: 100%;
|
||||
padding: globals.$spacing-unit 0;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -1,33 +1,45 @@
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { PencilIcon } from "@primer/octicons-react";
|
||||
import { PencilIcon, TrashIcon, ClockIcon } from "@primer/octicons-react";
|
||||
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Bold from '@tiptap/extension-bold';
|
||||
import Italic from '@tiptap/extension-italic';
|
||||
import Underline from '@tiptap/extension-underline';
|
||||
import type { GameReview } from "@types";
|
||||
|
||||
import { HeroPanel } from "./hero";
|
||||
import { DescriptionHeader } from "./description-header/description-header";
|
||||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
import { EditGameModal } from "./modals";
|
||||
import { ReviewSortOptions } from "./review-sort-options";
|
||||
import { ReviewPromptBanner } from "./review-prompt-banner";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
import { AuthPage } from "@shared";
|
||||
|
||||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
||||
import { useUserDetails, useLibrary } from "@renderer/hooks";
|
||||
import { useUserDetails, useLibrary, useDate } from "@renderer/hooks";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import "./game-details.scss";
|
||||
|
||||
export function GameDetailsContent() {
|
||||
const heroRef = useRef<HTMLDivElement | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { objectId, shopDetails, game, hasNSFWContentBlocked, updateGame } =
|
||||
const { objectId, shopDetails, game, hasNSFWContentBlocked, updateGame, shop } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
const { updateLibrary } = useLibrary();
|
||||
const { formatDistance } = useDate();
|
||||
|
||||
const { setShowCloudSyncModal, getGameArtifacts } =
|
||||
useContext(cloudSyncContext);
|
||||
@@ -80,6 +92,41 @@ export function GameDetailsContent() {
|
||||
|
||||
const [backdropOpacity, setBackdropOpacity] = useState(1);
|
||||
const [showEditGameModal, setShowEditGameModal] = useState(false);
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
|
||||
// Reviews state management
|
||||
const [reviews, setReviews] = useState<GameReview[]>([]);
|
||||
const [reviewsLoading, setReviewsLoading] = useState(false);
|
||||
const [reviewScore, setReviewScore] = useState(5);
|
||||
const [submittingReview, setSubmittingReview] = useState(false);
|
||||
const [reviewsSortBy, setReviewsSortBy] = useState("newest");
|
||||
const [reviewsPage, setReviewsPage] = useState(0);
|
||||
const [hasMoreReviews, setHasMoreReviews] = useState(true);
|
||||
const [visibleBlockedReviews, setVisibleBlockedReviews] = useState<Set<string>>(new Set());
|
||||
const [totalReviewCount, setTotalReviewCount] = useState(0);
|
||||
const [showReviewForm, setShowReviewForm] = useState(false);
|
||||
|
||||
// Review prompt banner state
|
||||
const [showReviewPrompt, setShowReviewPrompt] = useState(false);
|
||||
const [hasUserReviewed, setHasUserReviewed] = useState(false);
|
||||
const [reviewCheckLoading, setReviewCheckLoading] = useState(false);
|
||||
|
||||
// Tiptap editor for review input
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Bold,
|
||||
Italic,
|
||||
Underline,
|
||||
],
|
||||
content: '',
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'game-details__review-editor',
|
||||
'data-placeholder': t("write_review_placeholder"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBackdropOpacity(1);
|
||||
@@ -114,6 +161,188 @@ export function GameDetailsContent() {
|
||||
|
||||
const isCustomGame = game?.shop === "custom";
|
||||
|
||||
// Reviews functions
|
||||
const checkUserReview = async () => {
|
||||
if (!objectId || !userDetails) return;
|
||||
|
||||
setReviewCheckLoading(true);
|
||||
try {
|
||||
const response = await window.electron.checkGameReview(shop, objectId);
|
||||
const hasReviewed = (response as any)?.hasReviewed || false;
|
||||
setHasUserReviewed(hasReviewed);
|
||||
|
||||
// Show prompt only if user hasn't reviewed and has played the game
|
||||
if (!hasReviewed && game?.playTimeInMilliseconds && game.playTimeInMilliseconds > 0) {
|
||||
setShowReviewPrompt(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check user review:", error);
|
||||
} finally {
|
||||
setReviewCheckLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadReviews = async (reset = false) => {
|
||||
if (!objectId) return;
|
||||
|
||||
setReviewsLoading(true);
|
||||
try {
|
||||
const skip = reset ? 0 : reviewsPage * 20;
|
||||
const response = await window.electron.getGameReviews(
|
||||
shop,
|
||||
objectId,
|
||||
20,
|
||||
skip,
|
||||
reviewsSortBy
|
||||
);
|
||||
|
||||
// Handle the response structure: { totalCount: number, reviews: Review[] }
|
||||
const reviewsData = (response as any)?.reviews || [];
|
||||
const reviewCount = (response as any)?.totalCount || 0;
|
||||
|
||||
if (reset) {
|
||||
setReviews(reviewsData);
|
||||
setReviewsPage(0);
|
||||
setTotalReviewCount(reviewCount);
|
||||
} else {
|
||||
setReviews(prev => [...prev, ...reviewsData]);
|
||||
}
|
||||
|
||||
setHasMoreReviews(reviewsData.length === 20);
|
||||
} catch (error) {
|
||||
console.error("Failed to load reviews:", error);
|
||||
} finally {
|
||||
setReviewsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVoteReview = async (reviewId: string, voteType: 'upvote' | 'downvote') => {
|
||||
if (!objectId) return;
|
||||
|
||||
try {
|
||||
await window.electron.voteReview(shop, objectId, reviewId, voteType);
|
||||
// Reload reviews to get updated vote counts
|
||||
loadReviews(true);
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${voteType} review:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteReview = async (reviewId: string) => {
|
||||
if (!objectId) return;
|
||||
|
||||
try {
|
||||
await window.electron.deleteReview(shop, objectId, reviewId);
|
||||
// Reload reviews after deletion
|
||||
loadReviews(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete review:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitReview = async () => {
|
||||
console.log("handleSubmitReview called");
|
||||
console.log("game:", game);
|
||||
console.log("objectId:", objectId);
|
||||
|
||||
const reviewHtml = editor?.getHTML() || '';
|
||||
console.log("reviewHtml:", reviewHtml);
|
||||
console.log("reviewScore:", reviewScore);
|
||||
console.log("submittingReview:", submittingReview);
|
||||
|
||||
if (!objectId || !reviewHtml.trim() || submittingReview) {
|
||||
console.log("Early return - validation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Starting review submission...");
|
||||
setSubmittingReview(true);
|
||||
try {
|
||||
console.log("Calling window.electron.createGameReview...");
|
||||
await window.electron.createGameReview(
|
||||
shop,
|
||||
objectId,
|
||||
reviewHtml,
|
||||
reviewScore
|
||||
);
|
||||
|
||||
console.log("Review submitted successfully");
|
||||
editor?.commands.clearContent();
|
||||
setReviewScore(5);
|
||||
await loadReviews(true); // Reload reviews after submission
|
||||
setShowReviewForm(false); // Hide the review form after successful submission
|
||||
setShowReviewPrompt(false); // Hide the prompt banner
|
||||
setHasUserReviewed(true); // Update the review status
|
||||
} catch (error) {
|
||||
console.error("Failed to submit review:", error);
|
||||
} finally {
|
||||
setSubmittingReview(false);
|
||||
console.log("Review submission completed");
|
||||
}
|
||||
};
|
||||
|
||||
// Review prompt banner handlers
|
||||
const handleReviewPromptYes = () => {
|
||||
setShowReviewPrompt(false);
|
||||
setShowReviewForm(true);
|
||||
|
||||
// Scroll to review form
|
||||
setTimeout(() => {
|
||||
const reviewFormElement = document.querySelector('.game-details__review-form');
|
||||
if (reviewFormElement) {
|
||||
reviewFormElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleReviewPromptLater = () => {
|
||||
setShowReviewPrompt(false);
|
||||
};
|
||||
|
||||
const handleSortChange = (newSortBy: string) => {
|
||||
setReviewsSortBy(newSortBy);
|
||||
setReviewsPage(0);
|
||||
setHasMoreReviews(true);
|
||||
loadReviews(true);
|
||||
};
|
||||
|
||||
const toggleBlockedReview = (reviewId: string) => {
|
||||
setVisibleBlockedReviews(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(reviewId)) {
|
||||
newSet.delete(reviewId);
|
||||
} else {
|
||||
newSet.add(reviewId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const loadMoreReviews = () => {
|
||||
if (!reviewsLoading && hasMoreReviews) {
|
||||
setReviewsPage(prev => prev + 1);
|
||||
loadReviews(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load reviews when component mounts or sort changes
|
||||
useEffect(() => {
|
||||
if (objectId && (game || shop)) {
|
||||
loadReviews(true);
|
||||
checkUserReview(); // Check if user has reviewed this game
|
||||
}
|
||||
}, [game, shop, objectId, reviewsSortBy, userDetails]);
|
||||
|
||||
// Load more reviews when page changes
|
||||
useEffect(() => {
|
||||
if (reviewsPage > 0) {
|
||||
loadReviews(false);
|
||||
}
|
||||
}, [reviewsPage]);
|
||||
|
||||
// Helper function to get image with custom asset priority
|
||||
const getImageWithCustomPriority = (
|
||||
customUrl: string | null | undefined,
|
||||
@@ -227,6 +456,14 @@ export function GameDetailsContent() {
|
||||
|
||||
<div className="game-details__description-container">
|
||||
<div className="game-details__description-content">
|
||||
{/* Review Prompt Banner */}
|
||||
{showReviewPrompt && userDetails && game?.playTimeInMilliseconds && !hasUserReviewed && !reviewCheckLoading && (
|
||||
<ReviewPromptBanner
|
||||
onYesClick={handleReviewPromptYes}
|
||||
onLaterClick={handleReviewPromptLater}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DescriptionHeader />
|
||||
<GallerySlider />
|
||||
|
||||
@@ -234,8 +471,237 @@ export function GameDetailsContent() {
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: aboutTheGame,
|
||||
}}
|
||||
className="game-details__description"
|
||||
className={`game-details__description ${
|
||||
isDescriptionExpanded ? 'game-details__description--expanded' : 'game-details__description--collapsed'
|
||||
}`}
|
||||
/>
|
||||
|
||||
{aboutTheGame && aboutTheGame.length > 500 && (
|
||||
<button
|
||||
type="button"
|
||||
className="game-details__description-toggle"
|
||||
onClick={() => setIsDescriptionExpanded(!isDescriptionExpanded)}
|
||||
>
|
||||
{isDescriptionExpanded ? t("show_less") : t("show_more")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="game-details__reviews-section">
|
||||
{showReviewForm && (
|
||||
<>
|
||||
<div className="game-details__reviews-header">
|
||||
<h3 className="game-details__reviews-title">{t("leave_a_review")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="game-details__review-form">
|
||||
<div className="game-details__review-input-container">
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
className="game-details__review-input"
|
||||
/>
|
||||
<div className="game-details__review-input-bottom">
|
||||
<div className="game-details__review-editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editor?.chain().focus().toggleBold().run()}
|
||||
className={`game-details__editor-button ${editor?.isActive('bold') ? 'is-active' : ''}`}
|
||||
disabled={!editor}
|
||||
>
|
||||
<strong>B</strong>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editor?.chain().focus().toggleItalic().run()}
|
||||
className={`game-details__editor-button ${editor?.isActive('italic') ? 'is-active' : ''}`}
|
||||
disabled={!editor}
|
||||
>
|
||||
<em>I</em>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editor?.chain().focus().toggleUnderline().run()}
|
||||
className={`game-details__editor-button ${editor?.isActive('underline') ? 'is-active' : ''}`}
|
||||
disabled={!editor}
|
||||
>
|
||||
<u>U</u>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="game-details__review-submit-button"
|
||||
onClick={handleSubmitReview}
|
||||
disabled={!editor?.getHTML().trim() || submittingReview}
|
||||
>
|
||||
{submittingReview ? t("submitting") : t("submit_review")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="game-details__review-form-bottom">
|
||||
<div className="game-details__review-score-container">
|
||||
<label className="game-details__review-score-label">
|
||||
{t("rating")}
|
||||
</label>
|
||||
<select
|
||||
className="game-details__review-score-select"
|
||||
value={reviewScore}
|
||||
onChange={(e) => setReviewScore(Number(e.target.value))}
|
||||
>
|
||||
<option value={1}>1/10</option>
|
||||
<option value={2}>2/10</option>
|
||||
<option value={3}>3/10</option>
|
||||
<option value={4}>4/10</option>
|
||||
<option value={5}>5/10</option>
|
||||
<option value={6}>6/10</option>
|
||||
<option value={7}>7/10</option>
|
||||
<option value={8}>8/10</option>
|
||||
<option value={9}>9/10</option>
|
||||
<option value={10}>10/10</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showReviewForm && (
|
||||
<div className="game-details__reviews-separator"></div>
|
||||
)}
|
||||
|
||||
<div className="game-details__reviews-list">
|
||||
<div className="game-details__reviews-list-header">
|
||||
<div className="game-details__reviews-title-group">
|
||||
<h3 className="game-details__reviews-title">
|
||||
{t("reviews")}
|
||||
</h3>
|
||||
<span className="game-details__reviews-badge">
|
||||
{totalReviewCount}
|
||||
</span>
|
||||
</div>
|
||||
<ReviewSortOptions
|
||||
sortBy={reviewsSortBy as any}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{reviewsLoading && reviews.length === 0 && (
|
||||
<div className="game-details__reviews-loading">
|
||||
{t("loading_reviews")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!reviewsLoading && reviews.length === 0 && (
|
||||
<div className="game-details__reviews-empty">
|
||||
<div className="game-details__reviews-empty-icon">📝</div>
|
||||
<h4 className="game-details__reviews-empty-title">
|
||||
{t("no_reviews_yet")}
|
||||
</h4>
|
||||
<p className="game-details__reviews-empty-message">
|
||||
{t("be_first_to_review")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reviews.map((review, index) => (
|
||||
<div key={index} className="game-details__review-item">
|
||||
{review.isBlocked && !visibleBlockedReviews.has(review.id) ? (
|
||||
<div className="game-details__blocked-review-simple">
|
||||
Review from blocked user —
|
||||
<button
|
||||
className="game-details__blocked-review-show-link"
|
||||
onClick={() => toggleBlockedReview(review.id)}
|
||||
>
|
||||
Show
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="game-details__review-header">
|
||||
<div className="game-details__review-user">
|
||||
{review.user?.profileImageUrl && (
|
||||
<img
|
||||
src={review.user.profileImageUrl}
|
||||
alt={review.user.displayName || 'User'}
|
||||
className="game-details__review-avatar"
|
||||
/>
|
||||
)}
|
||||
<div className="game-details__review-user-info">
|
||||
<div
|
||||
className="game-details__review-display-name game-details__review-display-name--clickable"
|
||||
onClick={() => review.user?.id && navigate(`/profile/${review.user.id}`)}
|
||||
>
|
||||
{review.user?.displayName || 'Anonymous'}
|
||||
</div>
|
||||
<div className="game-details__review-date">
|
||||
<ClockIcon size={12} />
|
||||
{formatDistance(new Date(review.createdAt), new Date(), { addSuffix: true })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="game-details__review-score">
|
||||
{review.score}/10
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="game-details__review-content"
|
||||
dangerouslySetInnerHTML={{ __html: review.reviewHtml }}
|
||||
/>
|
||||
<div className="game-details__review-actions">
|
||||
<div className="game-details__review-votes">
|
||||
<button
|
||||
className={`game-details__vote-button game-details__vote-button--upvote ${review.hasUpvoted ? 'game-details__vote-button--active' : ''}`}
|
||||
onClick={() => handleVoteReview(review.id, 'upvote')}
|
||||
>
|
||||
<ThumbsUp size={16} />
|
||||
<span>{review.upvotes || 0}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`game-details__vote-button game-details__vote-button--downvote ${review.hasDownvoted ? 'game-details__vote-button--active' : ''}`}
|
||||
onClick={() => handleVoteReview(review.id, 'downvote')}
|
||||
>
|
||||
<ThumbsDown size={16} />
|
||||
<span>{review.downvotes || 0}</span>
|
||||
</button>
|
||||
</div>
|
||||
{userDetails?.id === review.user?.id && (
|
||||
<button
|
||||
className="game-details__delete-review-button"
|
||||
onClick={() => handleDeleteReview(review.id)}
|
||||
title={t("delete_review")}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</button>
|
||||
)}
|
||||
{review.isBlocked && visibleBlockedReviews.has(review.id) && (
|
||||
<button
|
||||
className="game-details__blocked-review-hide-link"
|
||||
onClick={() => toggleBlockedReview(review.id)}
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{hasMoreReviews && !reviewsLoading && (
|
||||
<button
|
||||
className="game-details__load-more-reviews"
|
||||
onClick={loadMoreReviews}
|
||||
>
|
||||
{t("load_more_reviews")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{reviewsLoading && reviews.length > 0 && (
|
||||
<div className="game-details__reviews-loading">
|
||||
{t("loading_more_reviews")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{game?.shop !== "custom" && <Sidebar />}
|
||||
|
||||
@@ -35,6 +35,7 @@ export function GameDetailsSkeleton() {
|
||||
))}
|
||||
<Skeleton className="game-details__hero-image-skeleton" />
|
||||
<Skeleton />
|
||||
<Skeleton width={120} height={36} className="game-details__description-toggle" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-sidebar">
|
||||
|
||||
@@ -27,6 +27,418 @@ $hero-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&__review-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
margin-bottom: calc(globals.$spacing-unit * 3);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
&__review-form-controls {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__review-form-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__review-score-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 0.75);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__review-score-label {
|
||||
display: block;
|
||||
font-size: globals.$body-font-size;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
|
||||
&__review-score-select {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 4px;
|
||||
padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1);
|
||||
color: globals.$body-color;
|
||||
font-size: globals.$body-font-size;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: globals.$brand-teal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: globals.$dark-background-color;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__reviews-sort {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 0.75);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&__reviews-sort-label {
|
||||
display: block;
|
||||
font-size: globals.$body-font-size;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
|
||||
&__reviews-sort-select {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 4px;
|
||||
padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1);
|
||||
color: globals.$body-color;
|
||||
font-size: globals.$body-font-size;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: globals.$brand-teal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: globals.$dark-background-color;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__review-submit-button {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid globals.$border-color;
|
||||
color: globals.$body-color;
|
||||
padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: globals.$small-font-size;
|
||||
font-family: inherit;
|
||||
transition: all ease 0.2s;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
cursor: not-allowed;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__reviews-list {
|
||||
margin-top: calc(globals.$spacing-unit * 3);
|
||||
}
|
||||
|
||||
&__reviews-separator {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: calc(globals.$spacing-unit * 3) 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__reviews-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
padding-bottom: calc(globals.$spacing-unit * 1);
|
||||
}
|
||||
|
||||
&__reviews-empty {
|
||||
text-align: center;
|
||||
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__reviews-empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&__reviews-empty-title {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 600;
|
||||
margin: 0 0 calc(globals.$spacing-unit * 1) 0;
|
||||
}
|
||||
|
||||
&__reviews-empty-message {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: globals.$small-font-size;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__review-item {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__review-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: calc(globals.$spacing-unit * 1.5);
|
||||
}
|
||||
|
||||
&__review-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit * 1);
|
||||
}
|
||||
|
||||
&__review-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&__review-user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 0.25);
|
||||
}
|
||||
|
||||
&__review-display-name {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: globals.$small-font-size;
|
||||
font-weight: 600;
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__review-actions {
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__review-votes {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__vote-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&--upvote:hover {
|
||||
color: #4caf50;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
&--downvote:hover {
|
||||
color: #f44336;
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
&--active {
|
||||
&.game-details__vote-button--upvote {
|
||||
svg {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.game-details__vote-button--downvote {
|
||||
svg {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&__delete-review-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
color: #f44336;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
border-color: #f44336;
|
||||
color: #ff5722;
|
||||
}
|
||||
}
|
||||
|
||||
&__blocked-review-simple {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: globals.$small-font-size;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit * 0.5);
|
||||
}
|
||||
|
||||
&__blocked-review-show-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ffc107;
|
||||
font-size: globals.$small-font-size;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding: 0;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #ffeb3b;
|
||||
}
|
||||
}
|
||||
|
||||
&__blocked-review-hide-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: globals.$small-font-size;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding: 0;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&__review-score {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: calc(globals.$spacing-unit * 0.5) calc(globals.$spacing-unit * 1);
|
||||
border-radius: 4px;
|
||||
font-size: globals.$small-font-size;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&__review-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: globals.$small-font-size;
|
||||
}
|
||||
|
||||
&__review-content {
|
||||
color: globals.$body-color;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__reviews-loading {
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__load-more-reviews {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid globals.$border-color;
|
||||
color: globals.$body-color;
|
||||
padding: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 2);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: globals.$body-font-size;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
margin-top: calc(globals.$spacing-unit * 2);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: globals.$brand-teal;
|
||||
}
|
||||
}
|
||||
|
||||
&__hero {
|
||||
width: 100%;
|
||||
height: $hero-height;
|
||||
@@ -192,6 +604,8 @@ $hero-height: 300px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__description {
|
||||
@@ -203,6 +617,7 @@ $hero-height: 300px;
|
||||
margin-right: auto;
|
||||
overflow-x: auto;
|
||||
min-height: auto;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 60%;
|
||||
@@ -212,6 +627,27 @@ $hero-height: 300px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&--collapsed {
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background: linear-gradient(transparent, globals.$background-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--expanded {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
border-radius: 5px;
|
||||
@@ -237,6 +673,24 @@ $hero-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&__description-toggle {
|
||||
background: none;
|
||||
border: 1px solid globals.$border-color;
|
||||
color: globals.$body-color;
|
||||
padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: globals.$body-font-size;
|
||||
margin-top: calc(globals.$spacing-unit * 1.5);
|
||||
transition: all 0.2s ease;
|
||||
align-self: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: globals.$brand-teal;
|
||||
}
|
||||
}
|
||||
|
||||
&__description-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -367,4 +821,206 @@ $hero-height: 300px;
|
||||
flex: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&__reviews-section {
|
||||
margin-top: calc(globals.$spacing-unit * 3);
|
||||
padding-top: calc(globals.$spacing-unit * 3);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&__reviews-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__reviews-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: globals.$muted-color;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__reviews-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__reviews-badge {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__leave-review-cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit * 0.5);
|
||||
padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5);
|
||||
background: linear-gradient(135deg, globals.$brand-teal, globals.$brand-blue);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: calc(globals.$spacing-unit);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(globals.$brand-teal, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__review-input-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__review-input-bottom {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__review-editor-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&__editor-button {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
color: globals.$body-color;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: globals.$brand-teal;
|
||||
border-color: globals.$brand-teal;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&__review-input {
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 6px;
|
||||
padding: calc(globals.$spacing-unit * 1.5);
|
||||
padding-bottom: calc(globals.$spacing-unit * 3.5);
|
||||
color: globals.$body-color;
|
||||
font-size: globals.$body-font-size;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
min-height: 100px;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: globals.$brand-teal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
outline: none;
|
||||
min-height: 80px;
|
||||
|
||||
&:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: rgba(208, 209, 215, 0.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.review-prompt-banner {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
margin-bottom: calc(globals.$spacing-unit * 3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: calc(globals.$spacing-unit * 2.5);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 0.5);
|
||||
}
|
||||
|
||||
&__playtime {
|
||||
font-size: globals.$body-font-size;
|
||||
color: globals.$body-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__question {
|
||||
font-size: globals.$small-font-size;
|
||||
color: globals.$muted-color;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
44
src/renderer/src/pages/game-details/review-prompt-banner.tsx
Normal file
44
src/renderer/src/pages/game-details/review-prompt-banner.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@renderer/components";
|
||||
import "./review-prompt-banner.scss";
|
||||
|
||||
interface ReviewPromptBannerProps {
|
||||
onYesClick: () => void;
|
||||
onLaterClick: () => void;
|
||||
}
|
||||
|
||||
export function ReviewPromptBanner({
|
||||
onYesClick,
|
||||
onLaterClick,
|
||||
}: ReviewPromptBannerProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
return (
|
||||
<div className="review-prompt-banner">
|
||||
<div className="review-prompt-banner__content">
|
||||
<div className="review-prompt-banner__text">
|
||||
<span className="review-prompt-banner__playtime">
|
||||
You've seemed to enjoy this game
|
||||
</span>
|
||||
<span className="review-prompt-banner__question">
|
||||
{t("would_you_recommend_this_game")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="review-prompt-banner__actions">
|
||||
<Button
|
||||
theme="primary"
|
||||
onClick={onYesClick}
|
||||
>
|
||||
{t("yes")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={onLaterClick}
|
||||
>
|
||||
{t("maybe_later")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
src/renderer/src/pages/game-details/review-sort-options.scss
Normal file
72
src/renderer/src/pages/game-details/review-sort-options.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.review-sort-options {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(globals.$spacing-unit);
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit);
|
||||
font-size: 14px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: calc(globals.$spacing-unit * 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
&__option {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
transition: all ease 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__separator {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 14px;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/renderer/src/pages/game-details/review-sort-options.tsx
Normal file
60
src/renderer/src/pages/game-details/review-sort-options.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { CalendarIcon, StarIcon, ThumbsupIcon, ClockIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./review-sort-options.scss";
|
||||
|
||||
type ReviewSortOption = "newest" | "oldest" | "score_high" | "score_low" | "most_voted";
|
||||
|
||||
interface ReviewSortOptionsProps {
|
||||
sortBy: ReviewSortOption;
|
||||
onSortChange: (sortBy: ReviewSortOption) => void;
|
||||
}
|
||||
|
||||
export function ReviewSortOptions({ sortBy, onSortChange }: ReviewSortOptionsProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
return (
|
||||
<div className="review-sort-options__container">
|
||||
<div className="review-sort-options__options">
|
||||
<button
|
||||
className={`review-sort-options__option ${sortBy === "newest" ? "active" : ""}`}
|
||||
onClick={() => onSortChange("newest")}
|
||||
>
|
||||
<CalendarIcon size={16} />
|
||||
<span>{t("sort_newest")}</span>
|
||||
</button>
|
||||
<span className="review-sort-options__separator">|</span>
|
||||
<button
|
||||
className={`review-sort-options__option ${sortBy === "oldest" ? "active" : ""}`}
|
||||
onClick={() => onSortChange("oldest")}
|
||||
>
|
||||
<ClockIcon size={16} />
|
||||
<span>{t("sort_oldest")}</span>
|
||||
</button>
|
||||
<span className="review-sort-options__separator">|</span>
|
||||
<button
|
||||
className={`review-sort-options__option ${sortBy === "score_high" ? "active" : ""}`}
|
||||
onClick={() => onSortChange("score_high")}
|
||||
>
|
||||
<StarIcon size={16} />
|
||||
<span>{t("sort_highest_score")}</span>
|
||||
</button>
|
||||
<span className="review-sort-options__separator">|</span>
|
||||
<button
|
||||
className={`review-sort-options__option ${sortBy === "score_low" ? "active" : ""}`}
|
||||
onClick={() => onSortChange("score_low")}
|
||||
>
|
||||
<StarIcon size={16} />
|
||||
<span>{t("sort_lowest_score")}</span>
|
||||
</button>
|
||||
<span className="review-sort-options__separator">|</span>
|
||||
<button
|
||||
className={`review-sort-options__option ${sortBy === "most_voted" ? "active" : ""}`}
|
||||
onClick={() => onSortChange("most_voted")}
|
||||
>
|
||||
<ThumbsupIcon size={16} />
|
||||
<span>{t("sort_most_voted")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -234,6 +234,25 @@ export interface GameStats {
|
||||
downloadCount: number;
|
||||
playerCount: number;
|
||||
assets: ShopAssets | null;
|
||||
averageScore: number | null;
|
||||
}
|
||||
|
||||
export interface GameReview {
|
||||
id: string;
|
||||
reviewHtml: string;
|
||||
score: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
upvotes: number;
|
||||
downvotes: number;
|
||||
isBlocked: boolean;
|
||||
hasUpvoted: boolean;
|
||||
hasDownvoted: boolean;
|
||||
user: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface TrendingGame extends ShopAssets {
|
||||
|
||||
446
yarn.lock
446
yarn.lock
@@ -2073,6 +2073,11 @@
|
||||
redux-thunk "^3.1.0"
|
||||
reselect "^5.1.0"
|
||||
|
||||
"@remirror/core-constants@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f"
|
||||
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
|
||||
|
||||
"@remix-run/router@1.19.2":
|
||||
version "1.19.2"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273"
|
||||
@@ -2840,6 +2845,201 @@
|
||||
dependencies:
|
||||
uint8-util "^2.2.5"
|
||||
|
||||
"@tiptap/core@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.6.2.tgz#abda4116e4a39779fca7070e316b9ed9fdcded7e"
|
||||
integrity sha512-XKZYrCVFsyQGF6dXQR73YR222l/76wkKfZ+2/4LCrem5qtcOarmv5pYxjUBG8mRuBPskTTBImSFTeQltJIUNCg==
|
||||
|
||||
"@tiptap/extension-blockquote@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-3.6.2.tgz#01b589565c87a691e586e189ddcbcdc5f35618fc"
|
||||
integrity sha512-TSl41UZhi3ugJMDaf91CA4F5NeFylgTSm6GqnZAHOE6IREdCpAK3qej2zaW3EzfpzxW7sRGLlytkZRvpeyjgJA==
|
||||
|
||||
"@tiptap/extension-bold@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.6.2.tgz#ed721961daf3210c7ba4433a5aeae981043c2d77"
|
||||
integrity sha512-Q9KO8CCPCAXYqHzIw8b/ookVmrfqfCg2cyh9h9Hvw6nhO4LOOnJMcGVmWsrpFItbwCGMafI5iY9SbSj7RpCyuw==
|
||||
|
||||
"@tiptap/extension-bubble-menu@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.6.2.tgz#237d84f217c8da52c0bc5265a36557fb27d64eaf"
|
||||
integrity sha512-OF5CxCmYExcXZjcectwAeujSeDZ4IltPy+SsqBZLbQRDts9PQhzv5azGDvYdL2eMMkT3yhO2gWkXxSHMxI3O6w==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.0.0"
|
||||
|
||||
"@tiptap/extension-bullet-list@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.6.2.tgz#be20b6c795c53bc0d199bdc4dd9f01b6270a1bee"
|
||||
integrity sha512-Y5Uhir+za7xMm6RAe592aNNlLvCayVSQt2HfSckOr+c/v/Zd2bFUHv0ef6l/nUzUhDBs32Bg9SvfWx/yyMyNEw==
|
||||
|
||||
"@tiptap/extension-code-block@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-3.6.2.tgz#cb3f6f607dcfb36e3eff25255fdcfdedfb3940a7"
|
||||
integrity sha512-5jfoiQ/3AUrIyuVU1NmEXar6sZFnY7wDFf3ZU2zpcBUG++yg/CmpOe5bXpoolczhl58cM/jyBG5gumQjyOxLNg==
|
||||
|
||||
"@tiptap/extension-code@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.6.2.tgz#5c6500d748fd4f52ddbe01ff114d4933c7a09e8f"
|
||||
integrity sha512-U6jilbcpCxtLZAgJrTapXzzVJTXnS78kJITFSOLyGCTyGSm6PXatQ4hnaxVGmNet66GySONGjhwAVZ8+l94Rwg==
|
||||
|
||||
"@tiptap/extension-document@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.6.2.tgz#5c3f3a85d12868f5d4e6d6d258b8fa0b8000b778"
|
||||
integrity sha512-4qg3KWL3aO1M7hfDpZR6/vSo7Cfqr3McyGUfqb/BXqYDW1DwT8jJkDTcHrGU7WUKRlWgoyPyzM8pZiGlP0uQHg==
|
||||
|
||||
"@tiptap/extension-dropcursor@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.6.2.tgz#22a64a4da25ac17cf0cd33e1e924762000152817"
|
||||
integrity sha512-6R5sma/i2TKd5h9OpIcy3a0wOGp5BNT/zIgnE/1HTmKi40eNcCAVe8sxd6+iWA5ETONP1E48kDy4hqA5ZzZCiQ==
|
||||
|
||||
"@tiptap/extension-floating-menu@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.6.2.tgz#cc9c97cdd5fa55407631d3135e00ca8051516444"
|
||||
integrity sha512-ym7YMKGY3QhFUKUS6JYOwtdi8s2PeGmOhu7TwI9/U0LmGbELeKJBJl2BP1yB+Sjpv25pVL++CwJQ6dsrjDlZ8g==
|
||||
|
||||
"@tiptap/extension-gapcursor@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.6.2.tgz#790c94d20a5b8ded4c0d38960254d24704a2bc08"
|
||||
integrity sha512-gXg+EvUKlv3ZO1GxKkRmAsi/V4yyA8AzLW6ppOcYrM2CKf6epmPaVRgAjdwHCA6cm3QuCBJyWeGTCAjhjNakhw==
|
||||
|
||||
"@tiptap/extension-hard-break@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.6.2.tgz#3c379d9104cd7d9e942277f22ba62c57fae267ad"
|
||||
integrity sha512-ncuPBHhGY58QjluJvEH6vXotaa1QZ/vphXBGAr55kiATZwMIEHgwh2Hgc6AiFTcw057gabGn6jNFDfRB+HjbmA==
|
||||
|
||||
"@tiptap/extension-heading@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.6.2.tgz#3884c309de60c9d61f1bb60c521410b3a0d88ed7"
|
||||
integrity sha512-JQ2yjwXGAiwGc+MhS1mULBr354MHfmWqVDQLRg8ey6LkdXggTDDJ1Ni3GrUS7B5YcA/ICdhr4krXaQpNkT5Syw==
|
||||
|
||||
"@tiptap/extension-horizontal-rule@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.6.2.tgz#f5680b3209bc48bf8635f3674355bd3d47f15622"
|
||||
integrity sha512-3TlPqedPDM9QkRTUPhOTxNxQVPSsBwlsuLrAZOgyM1y871Xi7M1DFX0h9LLXuqzPndYzUY16NjrfBGFJX+O56w==
|
||||
|
||||
"@tiptap/extension-italic@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.6.2.tgz#ea314f5e723499c9e7a1021ad7836693db9c653c"
|
||||
integrity sha512-46zYKqM3o9w1A2G9hWr0ERGbJpqIncoH45XIfLdAI6ZldZVVf+NeXMGwjOPf4+03cZ5/emk3MRTnVp9vF4ToIg==
|
||||
|
||||
"@tiptap/extension-link@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.6.2.tgz#5577d100cd3b735247db327b15d91de025cc76b6"
|
||||
integrity sha512-3yiRDWa187h30e6iUOJeejZLsbzbJthLfBwTeJGx7pHh7RngsEW82npBRuqLoI3udhJGTkXbzwAFZ9qOGOjl1Q==
|
||||
dependencies:
|
||||
linkifyjs "^4.3.2"
|
||||
|
||||
"@tiptap/extension-list-item@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-3.6.2.tgz#705f782a872e4bbb6f0e125fe277c45aeefe8161"
|
||||
integrity sha512-ma/D2GKylpNB04FfNI3tDMY+C9nz7Yk85H21YTIGv8QL5KlDK97L6orydmx6IVRc2nNMZQVitBIEKDOXcczX9w==
|
||||
|
||||
"@tiptap/extension-list-keymap@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.6.2.tgz#f14e173325b443a89dbbca7f418b76ec3d5c9a21"
|
||||
integrity sha512-1kl/lggH+LL/FUwcSx8p761ebk9L5ZGK06mGyDDU9XiGLS310CktZYLnpEuFgn/oMPbRHo26oNl9SXLn1/U53A==
|
||||
|
||||
"@tiptap/extension-list@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list/-/extension-list-3.6.2.tgz#beb4d965f48085fa7f69197e10109cde8c175046"
|
||||
integrity sha512-ZLaEHGVq4eL26hZZFE9e7RArk2rEjcVstN/YTRTKElTnLaf58kLTKN3nlgy1PWGwzfWGUuXURBuEBLaq5l6djg==
|
||||
|
||||
"@tiptap/extension-ordered-list@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.6.2.tgz#43b83757f67264ff0050c03825e780da43680c1d"
|
||||
integrity sha512-KdJ5MLIw19N+XiqQ2COXGtaq9TzUbtlLE5dgYCJQ2EumeZKIGELvUnHjrnIB9gH/gRlMs+hprLTh23xVUDJovg==
|
||||
|
||||
"@tiptap/extension-paragraph@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.6.2.tgz#d6cc89cdc369e463dd7dd4eb9121718441c984a0"
|
||||
integrity sha512-jeJWj2xKib3392iHQEcB7wYZ30dUgXuwqpCTwtN9eANor+Zvv6CpDKBs1R2al6BYFbIJCgKeTulqxce0yoC80g==
|
||||
|
||||
"@tiptap/extension-strike@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.6.2.tgz#2dab3f253a4ecfd525c5609ab5edb9325a6364c2"
|
||||
integrity sha512-976u5WaioIN/0xCjl/UIEypmzACzxgVz6OGgfIsYyreMUiPjhhgzXb0A/2Po5p3nZpKcaMcxifOdhqdw+lDpIQ==
|
||||
|
||||
"@tiptap/extension-text@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.6.2.tgz#77313173a9f91208e40d298bc2d40b39371b8fca"
|
||||
integrity sha512-fFSUEv1H3lM92yr6jZdELk0gog8rPTK5hTf08kP8RsY8pA80Br1ADVenejrMV4UNTmT1JWTXGBGhMqfQFHUvAQ==
|
||||
|
||||
"@tiptap/extension-underline@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.6.2.tgz#9f0dfb9722bd3d0cd144fc955bcb94a3fcf5eac2"
|
||||
integrity sha512-IrG6vjxTMI2EeyhZCtx0sNTEu83PsAvzIh4vxmG1fUi/RYokks+sFbgGMuq0jtO96iVNEszlpAC/vaqfxFJwew==
|
||||
|
||||
"@tiptap/extensions@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.6.2.tgz#591fbd5b9fa41f98f69dbd7d21d5d38a2241d94b"
|
||||
integrity sha512-tg7/DgaI6SpkeawryapUtNoBxsJUMJl3+nSjTfTvsaNXed+BHzLPsvmPbzlF9ScrAbVEx8nj6CCkneECYIQ4CQ==
|
||||
|
||||
"@tiptap/pm@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.6.2.tgz#2121d4917f92d11229529a26955a7033aa8a8843"
|
||||
integrity sha512-g+NXjqjbj6NfHOMl22uNWVYIu8oCq7RFfbnpohPMsSKJLaHYE8mJR++7T6P5R9FoqhIFdwizg1jTpwRU5CHqXQ==
|
||||
dependencies:
|
||||
prosemirror-changeset "^2.3.0"
|
||||
prosemirror-collab "^1.3.1"
|
||||
prosemirror-commands "^1.6.2"
|
||||
prosemirror-dropcursor "^1.8.1"
|
||||
prosemirror-gapcursor "^1.3.2"
|
||||
prosemirror-history "^1.4.1"
|
||||
prosemirror-inputrules "^1.4.0"
|
||||
prosemirror-keymap "^1.2.2"
|
||||
prosemirror-markdown "^1.13.1"
|
||||
prosemirror-menu "^1.2.4"
|
||||
prosemirror-model "^1.24.1"
|
||||
prosemirror-schema-basic "^1.2.3"
|
||||
prosemirror-schema-list "^1.5.0"
|
||||
prosemirror-state "^1.4.3"
|
||||
prosemirror-tables "^1.6.4"
|
||||
prosemirror-trailing-node "^3.0.0"
|
||||
prosemirror-transform "^1.10.2"
|
||||
prosemirror-view "^1.38.1"
|
||||
|
||||
"@tiptap/react@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-3.6.2.tgz#5495776c9051a60ece7522da176c9f211a67c7df"
|
||||
integrity sha512-jgG+bM/GDvI6jnqW3YyLtr/vOR6iO2ta9PYVzoWqNYIxISsMOJeRfinsIqB8l6hkiGZApn9bQji6oUXTc59fgA==
|
||||
dependencies:
|
||||
"@types/use-sync-external-store" "^0.0.6"
|
||||
fast-deep-equal "^3.1.3"
|
||||
use-sync-external-store "^1.4.0"
|
||||
optionalDependencies:
|
||||
"@tiptap/extension-bubble-menu" "^3.6.2"
|
||||
"@tiptap/extension-floating-menu" "^3.6.2"
|
||||
|
||||
"@tiptap/starter-kit@^3.6.2":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-3.6.2.tgz#ddd5612d4836a87082254779c9f152bb51e757bc"
|
||||
integrity sha512-nPzraIx/f1cOUNqG1LSC0OTnEu3mudcN3jQVuyGh3dvdOnik7FUciJEVfHKnloAyeoijidEeiLpiGHInp2uREg==
|
||||
dependencies:
|
||||
"@tiptap/core" "^3.6.2"
|
||||
"@tiptap/extension-blockquote" "^3.6.2"
|
||||
"@tiptap/extension-bold" "^3.6.2"
|
||||
"@tiptap/extension-bullet-list" "^3.6.2"
|
||||
"@tiptap/extension-code" "^3.6.2"
|
||||
"@tiptap/extension-code-block" "^3.6.2"
|
||||
"@tiptap/extension-document" "^3.6.2"
|
||||
"@tiptap/extension-dropcursor" "^3.6.2"
|
||||
"@tiptap/extension-gapcursor" "^3.6.2"
|
||||
"@tiptap/extension-hard-break" "^3.6.2"
|
||||
"@tiptap/extension-heading" "^3.6.2"
|
||||
"@tiptap/extension-horizontal-rule" "^3.6.2"
|
||||
"@tiptap/extension-italic" "^3.6.2"
|
||||
"@tiptap/extension-link" "^3.6.2"
|
||||
"@tiptap/extension-list" "^3.6.2"
|
||||
"@tiptap/extension-list-item" "^3.6.2"
|
||||
"@tiptap/extension-list-keymap" "^3.6.2"
|
||||
"@tiptap/extension-ordered-list" "^3.6.2"
|
||||
"@tiptap/extension-paragraph" "^3.6.2"
|
||||
"@tiptap/extension-strike" "^3.6.2"
|
||||
"@tiptap/extension-text" "^3.6.2"
|
||||
"@tiptap/extension-underline" "^3.6.2"
|
||||
"@tiptap/extensions" "^3.6.2"
|
||||
"@tiptap/pm" "^3.6.2"
|
||||
|
||||
"@tokenizer/inflate@^0.2.6":
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.2.7.tgz#32dd9dfc9abe457c89b3d9b760fc0690c85a103b"
|
||||
@@ -3014,6 +3214,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/linkify-it@^5":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
|
||||
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
|
||||
|
||||
"@types/lodash-es@^4.17.12":
|
||||
version "4.17.12"
|
||||
resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz"
|
||||
@@ -3033,6 +3238,19 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/markdown-it@^14.0.0":
|
||||
version "14.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
|
||||
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
|
||||
dependencies:
|
||||
"@types/linkify-it" "^5"
|
||||
"@types/mdurl" "^2"
|
||||
|
||||
"@types/mdurl@^2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
|
||||
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
||||
@@ -3125,6 +3343,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz"
|
||||
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
|
||||
|
||||
"@types/use-sync-external-store@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
|
||||
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
|
||||
|
||||
"@types/user-agents@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/user-agents/-/user-agents-1.0.4.tgz"
|
||||
@@ -4209,6 +4432,11 @@ create-require@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
crelt@^1.0.0:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
||||
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
|
||||
|
||||
cross-fetch-ponyfill@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz#5c5524e3bd3374e71d5016c2327e416369a57527"
|
||||
@@ -6614,6 +6842,18 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
linkify-it@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
|
||||
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
|
||||
dependencies:
|
||||
uc.micro "^2.0.0"
|
||||
|
||||
linkifyjs@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
|
||||
integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==
|
||||
|
||||
locate-path@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
|
||||
@@ -6779,6 +7019,11 @@ lru-cache@^7.7.1:
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
|
||||
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
|
||||
|
||||
lucide-react@^0.544.0:
|
||||
version "0.544.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.544.0.tgz#4719953c10fd53a64dd8343bb0ed16ec79f3eeef"
|
||||
integrity sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==
|
||||
|
||||
magic-string@^0.30.17:
|
||||
version "0.30.17"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||
@@ -6822,6 +7067,18 @@ make-fetch-happen@^10.2.1:
|
||||
socks-proxy-agent "^7.0.0"
|
||||
ssri "^9.0.0"
|
||||
|
||||
markdown-it@^14.0.0:
|
||||
version "14.1.0"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
|
||||
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
entities "^4.4.0"
|
||||
linkify-it "^5.0.0"
|
||||
mdurl "^2.0.0"
|
||||
punycode.js "^2.3.1"
|
||||
uc.micro "^2.1.0"
|
||||
|
||||
matcher@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
|
||||
@@ -6844,6 +7101,11 @@ maybe-combine-errors@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz#e9592832e61fc47643a92cff3c1f33e27211e5be"
|
||||
integrity sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A==
|
||||
|
||||
mdurl@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
|
||||
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
|
||||
|
||||
meow@^12.0.1:
|
||||
version "12.1.1"
|
||||
resolved "https://registry.yarnpkg.com/meow/-/meow-12.1.1.tgz#e558dddbab12477b69b2e9a2728c327f191bace6"
|
||||
@@ -7264,6 +7526,11 @@ ora@^5.1.0:
|
||||
strip-ansi "^6.0.0"
|
||||
wcwidth "^1.0.1"
|
||||
|
||||
orderedmap@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
|
||||
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
|
||||
|
||||
own-keys@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358"
|
||||
@@ -7499,6 +7766,160 @@ property-expr@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8"
|
||||
integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==
|
||||
|
||||
prosemirror-changeset@^2.3.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz#eee3299cfabc7a027694e9abdc4e85505e9dd5e7"
|
||||
integrity sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==
|
||||
dependencies:
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-collab@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
|
||||
integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz#d101fef85618b1be53d5b99ea17bee5600781b38"
|
||||
integrity sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.10.2"
|
||||
|
||||
prosemirror-dropcursor@^1.8.1:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz#2ed30c4796109ddeb1cf7282372b3850528b7228"
|
||||
integrity sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.1.0"
|
||||
prosemirror-view "^1.1.0"
|
||||
|
||||
prosemirror-gapcursor@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
|
||||
integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
|
||||
dependencies:
|
||||
prosemirror-keymap "^1.0.0"
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-view "^1.0.0"
|
||||
|
||||
prosemirror-history@^1.0.0, prosemirror-history@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.4.1.tgz#cc370a46fb629e83a33946a0e12612e934ab8b98"
|
||||
integrity sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==
|
||||
dependencies:
|
||||
prosemirror-state "^1.2.2"
|
||||
prosemirror-transform "^1.0.0"
|
||||
prosemirror-view "^1.31.0"
|
||||
rope-sequence "^1.3.0"
|
||||
|
||||
prosemirror-inputrules@^1.4.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz#e22bfaf1d6ea4fe240ad447c184af3d520d43c37"
|
||||
integrity sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz#c0f6ab95f75c0b82c97e44eb6aaf29cbfc150472"
|
||||
integrity sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
w3c-keyname "^2.2.0"
|
||||
|
||||
prosemirror-markdown@^1.13.1:
|
||||
version "1.13.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz#863eb3fd5f57a444e4378174622b562735b1c503"
|
||||
integrity sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==
|
||||
dependencies:
|
||||
"@types/markdown-it" "^14.0.0"
|
||||
markdown-it "^14.0.0"
|
||||
prosemirror-model "^1.25.0"
|
||||
|
||||
prosemirror-menu@^1.2.4:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz#dea00e7b623cea89f4d76963bee22d2ac2343250"
|
||||
integrity sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==
|
||||
dependencies:
|
||||
crelt "^1.0.0"
|
||||
prosemirror-commands "^1.0.0"
|
||||
prosemirror-history "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0:
|
||||
version "1.25.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.3.tgz#c657c60a361cb1e9c9f683d19118c0af50a6f7a9"
|
||||
integrity sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-schema-basic@^1.2.3:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz#389ce1ec09b8a30ea9bbb92c58569cb690c2d695"
|
||||
integrity sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==
|
||||
dependencies:
|
||||
prosemirror-model "^1.25.0"
|
||||
|
||||
prosemirror-schema-list@^1.5.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz#5869c8f749e8745c394548bb11820b0feb1e32f5"
|
||||
integrity sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.7.3"
|
||||
|
||||
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
|
||||
integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
prosemirror-view "^1.27.0"
|
||||
|
||||
prosemirror-tables@^1.6.4:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.8.1.tgz#896a234e3e18240b629b747a871369dae78c8a9a"
|
||||
integrity sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==
|
||||
dependencies:
|
||||
prosemirror-keymap "^1.2.2"
|
||||
prosemirror-model "^1.25.0"
|
||||
prosemirror-state "^1.4.3"
|
||||
prosemirror-transform "^1.10.3"
|
||||
prosemirror-view "^1.39.1"
|
||||
|
||||
prosemirror-trailing-node@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04"
|
||||
integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==
|
||||
dependencies:
|
||||
"@remirror/core-constants" "3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
|
||||
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.7.3:
|
||||
version "1.10.4"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz#56419eac14f9f56612c806ae46f9238648f3f02e"
|
||||
integrity sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==
|
||||
dependencies:
|
||||
prosemirror-model "^1.21.0"
|
||||
|
||||
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.38.1, prosemirror-view@^1.39.1:
|
||||
version "1.41.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.2.tgz#e69ad3883bfd3c9f3c9cf6da5cee940210df0b6f"
|
||||
integrity sha512-PGS/jETmh+Qjmre/6vcG7SNHAKiGc4vKOJmHMPRmvcUl7ISuVtrtHmH06UDUwaim4NDJfZfVMl7U7JkMMETa6g==
|
||||
dependencies:
|
||||
prosemirror-model "^1.20.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.1.0"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
@@ -7517,6 +7938,11 @@ pump@^3.0.0:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
punycode.js@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
|
||||
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
|
||||
|
||||
punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
@@ -7901,6 +8327,11 @@ rollup@^4.20.0:
|
||||
"@rollup/rollup-win32-x64-msvc" "4.23.0"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rope-sequence@^1.3.0:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
|
||||
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
|
||||
|
||||
rrweb-cssom@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b"
|
||||
@@ -8902,6 +9333,11 @@ typescript@^5.3.3, typescript@^5.4.3:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
|
||||
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
|
||||
|
||||
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
|
||||
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
|
||||
|
||||
uint8-util@^2.2.2, uint8-util@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/uint8-util/-/uint8-util-2.2.5.tgz#f1a8ff800e4e10a3ac1c82ee3667c99245123896"
|
||||
@@ -9021,6 +9457,11 @@ use-sync-external-store@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
|
||||
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
|
||||
|
||||
use-sync-external-store@^1.4.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
|
||||
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
||||
|
||||
user-agents@^1.1.387:
|
||||
version "1.1.387"
|
||||
resolved "https://registry.yarnpkg.com/user-agents/-/user-agents-1.1.387.tgz#afc69da00b50eee7ffa17724890e755a6672b99f"
|
||||
@@ -9087,6 +9528,11 @@ void-elements@3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
|
||||
|
||||
w3c-keyname@^2.2.0:
|
||||
version "2.2.8"
|
||||
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
|
||||
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
|
||||
|
||||
w3c-xmlserializer@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
|
||||
|
||||
Reference in New Issue
Block a user