diff --git a/application/client/babel.config.js b/application/client/babel.config.js index c3c574591a..f51f26c2fc 100644 --- a/application/client/babel.config.js +++ b/application/client/babel.config.js @@ -4,16 +4,14 @@ module.exports = { [ "@babel/preset-env", { - targets: "ie 11", - corejs: "3", - modules: "commonjs", + targets: "defaults, not ie 11", + modules: false, useBuiltIns: false, }, ], [ "@babel/preset-react", { - development: true, runtime: "automatic", }, ], diff --git a/application/client/package.json b/application/client/package.json index 9f8e80a6a8..e2885c7fa8 100644 --- a/application/client/package.json +++ b/application/client/package.json @@ -5,13 +5,10 @@ "license": "MPL-2.0", "author": "CyberAgent, Inc.", "scripts": { - "build": "NODE_ENV=development webpack", + "build": "NODE_ENV=production webpack", "typecheck": "tsc" }, "dependencies": { - "@ffmpeg/core": "0.12.10", - "@ffmpeg/ffmpeg": "0.12.15", - "@imagemagick/magick-wasm": "0.0.37", "@mlc-ai/web-llm": "0.2.80", "@web-speed-hackathon-2026/client": "workspace:*", "bayesian-bm25": "0.4.0", @@ -20,10 +17,9 @@ "classnames": "2.5.1", "common-tags": "1.8.2", "core-js": "3.45.1", + "dayjs": "1.11.20", "encoding-japanese": "2.2.0", "fast-average-color": "9.5.0", - "gifler": "github:themadcreator/gifler#v0.3.0", - "image-size": "2.0.2", "jquery": "3.7.1", "jquery-binarytransport": "1.0.0", "json-repair-js": "1.0.0", @@ -31,12 +27,9 @@ "kuromoji": "0.1.2", "langs": "2.0.0", "lodash": "4.17.21", - "moment": "2.30.1", "negaposi-analyzer-ja": "1.0.1", "normalize.css": "8.0.1", - "omggif": "1.0.10", "pako": "2.1.0", - "piexifjs": "1.0.6", "react": "19.2.0", "react-dom": "19.2.0", "react-helmet": "npm:@dr.pogodin/react-helmet@3.0.4", @@ -57,6 +50,8 @@ "@babel/preset-env": "7.28.3", "@babel/preset-react": "7.27.1", "@babel/preset-typescript": "7.27.1", + "@tailwindcss/postcss": "^4.2.2", + "@tailwindcss/vite": "^4.2.2", "@tsconfig/strictest": "2.0.8", "@types/bluebird": "3.5.42", "@types/common-tags": "1.8.4", @@ -74,6 +69,7 @@ "@types/react-syntax-highlighter": "15.5.13", "@types/redux-form": "^8.3.11", "babel-loader": "10.0.0", + "compression-webpack-plugin": "12.0.0", "copy-webpack-plugin": "13.0.1", "css-loader": "7.1.2", "html-webpack-plugin": "5.6.4", @@ -83,6 +79,7 @@ "postcss-loader": "8.2.0", "postcss-preset-env": "10.4.0", "react-markdown": "10.1.0", + "tailwindcss": "^4.2.2", "typescript": "5.9.3", "webpack": "5.102.1", "webpack-cli": "6.0.1", diff --git a/application/client/postcss.config.js b/application/client/postcss.config.js index d7ee920b94..bcb66acf3f 100644 --- a/application/client/postcss.config.js +++ b/application/client/postcss.config.js @@ -1,9 +1,11 @@ +const tailwindcss = require("@tailwindcss/postcss"); const postcssImport = require("postcss-import"); const postcssPresetEnv = require("postcss-preset-env"); module.exports = { plugins: [ postcssImport(), + tailwindcss(), postcssPresetEnv({ stage: 3, }), diff --git a/application/client/src/components/application/SearchPage.tsx b/application/client/src/components/application/SearchPage.tsx index e99045de45..4893776c6c 100644 --- a/application/client/src/components/application/SearchPage.tsx +++ b/application/client/src/components/application/SearchPage.tsx @@ -1,14 +1,11 @@ -import { useEffect, useMemo, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useMemo, useState, useCallback } from "react"; import { useNavigate } from "react-router"; -import { Field, InjectedFormProps, reduxForm, WrappedFieldProps } from "redux-form"; import { Timeline } from "@web-speed-hackathon-2026/client/src/components/timeline/Timeline"; import { parseSearchQuery, sanitizeSearchText, } from "@web-speed-hackathon-2026/client/src/search/services"; -import { SearchFormData } from "@web-speed-hackathon-2026/client/src/search/types"; -import { validate } from "@web-speed-hackathon-2026/client/src/search/validation"; import { analyzeSentiment } from "@web-speed-hackathon-2026/client/src/utils/negaposi_analyzer"; import { Button } from "../foundation/Button"; @@ -18,33 +15,17 @@ interface Props { results: Models.Post[]; } -const SearchInput = ({ input, meta }: WrappedFieldProps) => ( -
- - {meta.touched && meta.error && ( - {meta.error} - )} -
-); - -const SearchPageComponent = ({ - query, - results, - handleSubmit, -}: Props & InjectedFormProps) => { +export const SearchPage = ({ query, results }: Props) => { const navigate = useNavigate(); const [isNegative, setIsNegative] = useState(false); + const [searchText, setSearchText] = useState(query); - const parsed = parseSearchQuery(query); + // URLのクエリが変わったら入力欄を同期 + useEffect(() => { + setSearchText(query); + }, [query]); + + const parsed = useMemo(() => parseSearchQuery(query), [query]); useEffect(() => { if (!parsed.keywords) { @@ -53,48 +34,56 @@ const SearchPageComponent = ({ } let isMounted = true; - analyzeSentiment(parsed.keywords) - .then((result) => { - if (isMounted) { - setIsNegative(result.label === "negative"); - } - }) - .catch(() => { - if (isMounted) { - setIsNegative(false); - } - }); + const timer = setTimeout(() => { + analyzeSentiment(parsed.keywords) + .then((score) => { + if (isMounted) { + setIsNegative(score < -0.1); + } + }) + .catch(() => { + if (isMounted) setIsNegative(false); + }); + }, 100); return () => { isMounted = false; + clearTimeout(timer); }; }, [parsed.keywords]); const searchConditionText = useMemo(() => { const parts: string[] = []; - if (parsed.keywords) { - parts.push(`「${parsed.keywords}」`); - } - if (parsed.sinceDate) { - parts.push(`${parsed.sinceDate} 以降`); - } - if (parsed.untilDate) { - parts.push(`${parsed.untilDate} 以前`); - } + if (parsed.keywords) parts.push(`「${parsed.keywords}」`); + if (parsed.sinceDate) parts.push(`${parsed.sinceDate} 以降`); + if (parsed.untilDate) parts.push(`${parsed.untilDate} 以前`); return parts.join(" "); }, [parsed]); - const onSubmit = (values: SearchFormData) => { - const sanitizedText = sanitizeSearchText(values.searchText.trim()); - navigate(`/search?q=${encodeURIComponent(sanitizedText)}`); + const handleSubmit = useCallback((e: FormEvent) => { + e.preventDefault(); + const sanitizedText = sanitizeSearchText(searchText.trim()); + if (sanitizedText) { + navigate(`/search?q=${encodeURIComponent(sanitizedText)}`); + } + }, [navigate, searchText]); + + const handleInputChange = (e: ChangeEvent) => { + setSearchText(e.target.value); }; return (
-
+
- + @@ -134,9 +123,3 @@ const SearchPageComponent = ({
); }; - -export const SearchPage = reduxForm({ - form: "search", - enableReinitialize: true, - validate, -})(SearchPageComponent); diff --git a/application/client/src/components/crok/ChatInput.tsx b/application/client/src/components/crok/ChatInput.tsx index 6f8c17796b..acecfc35df 100644 --- a/application/client/src/components/crok/ChatInput.tsx +++ b/application/client/src/components/crok/ChatInput.tsx @@ -1,5 +1,4 @@ import Bluebird from "bluebird"; -import kuromoji, { type Tokenizer, type IpadicFeatures } from "kuromoji"; import { useEffect, useLayoutEffect, @@ -22,64 +21,36 @@ interface Props { onSendMessage: (message: string) => void; } -// トークン単位でハイライト -function highlightMatchByTokens(text: string, queryTokens: string[]): React.ReactNode { - if (queryTokens.length === 0) return text; - - const lowerText = text.toLowerCase(); - - // テキスト内でクエリトークンにマッチする範囲を収集 - const ranges: { start: number; end: number }[] = []; - for (const token of queryTokens) { - let pos = 0; - while (pos < lowerText.length) { - const index = lowerText.indexOf(token, pos); - if (index === -1) break; - ranges.push({ start: index, end: index + token.length }); - pos = index + 1; - } - } - - if (ranges.length === 0) return text; - - // 範囲をソートしてマージ - ranges.sort((a, b) => a.start - b.start); - const merged: { start: number; end: number }[] = [ranges[0]!]; - for (let i = 1; i < ranges.length; i++) { - const prev = merged[merged.length - 1]!; - const curr = ranges[i]!; - if (curr.start <= prev.end) { - prev.end = Math.max(prev.end, curr.end); - } else { - merged.push(curr); - } - } - - const parts: React.ReactNode[] = []; - let lastEnd = 0; - for (let i = 0; i < merged.length; i++) { - const range = merged[i]!; - if (range.start > lastEnd) { - parts.push(text.slice(lastEnd, range.start)); - } - parts.push( - - {text.slice(range.start, range.end)} - , - ); - lastEnd = range.end; - } - if (lastEnd < text.length) { - parts.push(text.slice(lastEnd)); - } - - return <>{parts}; -} +/** + * サジェスト内のトークンに一致する部分をハイライトします + */ +const highlightMatchByTokens = (text: string, tokens: string[]) => { + if (tokens.length === 0) return text; + + // トークンを長い順にソートして、正規表現でのマッチングを確実にする + const sortedTokens = [...tokens].sort((a, b) => b.length - a.length); + const pattern = new RegExp(`(${sortedTokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`, 'gi'); + + const parts = text.split(pattern); + return ( + <> + {parts.map((part, i) => ( + pattern.test(part) ? ( + + {part} + + ) : ( + {part} + ) + ))} + + ); +}; export const ChatInput = ({ isStreaming, onSendMessage }: Props) => { const textareaRef = useRef(null); const suggestionsRef = useRef(null); - const [tokenizer, setTokenizer] = useState | null>(null); + const [tokenizer, setTokenizer] = useState(null); const [inputValue, setInputValue] = useState(""); const [suggestions, setSuggestions] = useState([]); const [queryTokens, setQueryTokens] = useState([]); @@ -97,8 +68,9 @@ export const ChatInput = ({ isStreaming, onSendMessage }: Props) => { let mounted = true; const init = async () => { + const { default: kuromoji } = await import("kuromoji"); const builder = Bluebird.promisifyAll(kuromoji.builder({ dicPath: "/dicts" })); - const nextTokenizer = await builder.buildAsync(); + const nextTokenizer = await (builder as any).buildAsync(); if (mounted) { setTokenizer(nextTokenizer); } @@ -110,26 +82,36 @@ export const ChatInput = ({ isStreaming, onSendMessage }: Props) => { }; }, []); + const [candidates, setCandidates] = useState([]); + + // 初回に candidates を取得 + useEffect(() => { + const fetchCandidates = async () => { + const { suggestions: data } = await fetchJSON<{ suggestions: string[] }>( + "/api/v1/crok/suggestions", + ); + setCandidates(data); + }; + fetchCandidates(); + }, []); + useEffect(() => { let cancelled = false; const updateSuggestions = async () => { - if (!tokenizer || !inputValue.trim()) { + if (!tokenizer || !inputValue.trim() || candidates.length === 0) { setSuggestions([]); setQueryTokens([]); setShowSuggestions(false); return; } - const { suggestions: candidates } = await fetchJSON<{ suggestions: string[] }>( - "/api/v1/crok/suggestions", - ); - if (cancelled) { - return; - } + // デバウンス的な待ちを入れる + await new Promise((resolve) => setTimeout(resolve, 150)); + if (cancelled) return; const tokens = extractTokens(tokenizer.tokenize(inputValue)); - const results = filterSuggestionsBM25(tokenizer, candidates, tokens); + const results = await filterSuggestionsBM25(tokenizer, candidates, tokens); if (cancelled) { return; @@ -139,13 +121,12 @@ export const ChatInput = ({ isStreaming, onSendMessage }: Props) => { setSuggestions(results); setShowSuggestions(results.length > 0); }; - void updateSuggestions(); return () => { cancelled = true; }; - }, [inputValue, tokenizer]); + }, [inputValue, tokenizer, candidates]); const adjustTextareaHeight = () => { const textarea = textareaRef.current; @@ -174,8 +155,7 @@ export const ChatInput = ({ isStreaming, onSendMessage }: Props) => { setShowSuggestions(false); }; - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); + const sendCurrentMessage = () => { if (inputValue.trim() && !isStreaming) { onSendMessage(inputValue.trim()); setInputValue(""); @@ -186,10 +166,15 @@ export const ChatInput = ({ isStreaming, onSendMessage }: Props) => { } }; + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + sendCurrentMessage(); + }; + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); - handleSubmit(e); + sendCurrentMessage(); } }; diff --git a/application/client/src/components/direct_message/DirectMessageListPage.tsx b/application/client/src/components/direct_message/DirectMessageListPage.tsx index 5a373e918e..0ba12a4e1e 100644 --- a/application/client/src/components/direct_message/DirectMessageListPage.tsx +++ b/application/client/src/components/direct_message/DirectMessageListPage.tsx @@ -1,4 +1,4 @@ -import moment from "moment"; +import dayjs from "dayjs"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@web-speed-hackathon-2026/client/src/components/foundation/Button"; @@ -87,6 +87,7 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => { {peer.profileImage.alt}
@@ -100,7 +101,7 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => { className="text-cax-text-subtle text-xs" dateTime={lastMessage.createdAt} > - {moment(lastMessage.createdAt).locale("ja").fromNow()} + {dayjs(lastMessage.createdAt).fromNow()} )}
diff --git a/application/client/src/components/direct_message/DirectMessagePage.tsx b/application/client/src/components/direct_message/DirectMessagePage.tsx index 098c7d2894..9af27de3fd 100644 --- a/application/client/src/components/direct_message/DirectMessagePage.tsx +++ b/application/client/src/components/direct_message/DirectMessagePage.tsx @@ -1,5 +1,5 @@ import classNames from "classnames"; -import moment from "moment"; +import dayjs from "dayjs"; import { ChangeEvent, useCallback, @@ -21,8 +21,11 @@ interface Props { activeUser: Models.User; isPeerTyping: boolean; isSubmitting: boolean; + hasMore: boolean; + isLoadingMore: boolean; onTyping: () => void; onSubmit: (params: DirectMessageFormData) => Promise; + onLoadMore: () => void; } export const DirectMessagePage = ({ @@ -31,8 +34,11 @@ export const DirectMessagePage = ({ activeUser, isPeerTyping, isSubmitting, + hasMore, + isLoadingMore, onTyping, onSubmit, + onLoadMore, }: Props) => { const formRef = useRef(null); const textAreaId = useId(); @@ -43,7 +49,8 @@ export const DirectMessagePage = ({ const [text, setText] = useState(""); const textAreaRows = Math.min((text || "").split("\n").length, 5); const isInvalid = text.trim().length === 0; - const scrollHeightRef = useRef(0); + + const prevMessageCountRef = useRef(conversation.messages.length); const handleChange = useCallback( (event: ChangeEvent) => { @@ -74,16 +81,15 @@ export const DirectMessagePage = ({ ); useEffect(() => { - const id = setInterval(() => { - const height = Number(window.getComputedStyle(document.body).height.replace("px", "")); - if (height !== scrollHeightRef.current) { - scrollHeightRef.current = height; - window.scrollTo(0, height); + // 新しいメッセージが末尾に追加された場合のみスクロールする + if (conversation.messages.length > prevMessageCountRef.current) { + const lastMessage = conversation.messages.at(-1); + if (lastMessage && (lastMessage.sender.id === activeUser.id || lastMessage.id.startsWith("temp-"))) { + window.scrollTo(0, document.body.scrollHeight); } - }, 1); - - return () => clearInterval(id); - }, []); + } + prevMessageCountRef.current = conversation.messages.length; + }, [conversation.messages.length, activeUser.id]); if (conversationError != null) { return ( @@ -99,6 +105,7 @@ export const DirectMessagePage = ({ {peer.profileImage.alt}
@@ -112,6 +119,18 @@ export const DirectMessagePage = ({
+ {hasMore && ( +
+ +
+ )} {conversation.messages.length === 0 && (

まだメッセージはありません。最初のメッセージを送信してみましょう。 @@ -124,6 +143,7 @@ export const DirectMessagePage = ({ return (

  • {isActiveUserSend && message.isRead && ( 既読 @@ -176,12 +196,11 @@ export const DirectMessagePage = ({ onChange={handleChange} onKeyDown={handleKeyDown} rows={textAreaRows} - disabled={isSubmitting} />
    + {alt && ( + + )}
    diff --git a/application/client/src/components/foundation/InfiniteScroll.tsx b/application/client/src/components/foundation/InfiniteScroll.tsx index 408f24c107..4523ede65b 100644 --- a/application/client/src/components/foundation/InfiniteScroll.tsx +++ b/application/client/src/components/foundation/InfiniteScroll.tsx @@ -7,43 +7,42 @@ interface Props { } export const InfiniteScroll = ({ children, fetchMore, items }: Props) => { + const observerTarget = useRef(null); const latestItem = items[items.length - 1]; + const fetchMoreRef = useRef(fetchMore); - const prevReachedRef = useRef(false); + // fetchMore が変わっても observer を再作成しないように ref に保持 + useEffect(() => { + fetchMoreRef.current = fetchMore; + }, [fetchMore]); useEffect(() => { - const handler = () => { - // 念の為 2の18乗 回、最下部かどうかを確認する - const hasReached = Array.from(Array(2 ** 18), () => { - return window.innerHeight + Math.ceil(window.scrollY) >= document.body.offsetHeight; - }).every(Boolean); - - // 画面最下部にスクロールしたタイミングで、登録したハンドラを呼び出す - if (hasReached && !prevReachedRef.current) { - // アイテムがないときは追加で読み込まない - if (latestItem !== undefined) { - fetchMore(); + const target = observerTarget.current; + if (!target) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && latestItem !== undefined) { + fetchMoreRef.current(); } + }, + { + rootMargin: "1000px", // 1000px 手前で読み込みを開始する } + ); - prevReachedRef.current = hasReached; - }; + observer.observe(target); - // 最初は実行されないので手動で呼び出す - prevReachedRef.current = false; - handler(); - - document.addEventListener("wheel", handler, { passive: false }); - document.addEventListener("touchmove", handler, { passive: false }); - document.addEventListener("resize", handler, { passive: false }); - document.addEventListener("scroll", handler, { passive: false }); return () => { - document.removeEventListener("wheel", handler); - document.removeEventListener("touchmove", handler); - document.removeEventListener("resize", handler); - document.removeEventListener("scroll", handler); + observer.unobserve(target); }; - }, [latestItem, fetchMore]); - - return <>{children}; + }, [latestItem]); // latestItem が変わる(=アイテムが追加される)たびに判定をリセット + + return ( + <> + {children} + {/* この透明な div が画面に入ると fetchMore が呼ばれる */} +
    + + ); }; diff --git a/application/client/src/components/foundation/Link.tsx b/application/client/src/components/foundation/Link.tsx index 35f70c0497..9496b29522 100644 --- a/application/client/src/components/foundation/Link.tsx +++ b/application/client/src/components/foundation/Link.tsx @@ -1,13 +1,8 @@ -import { AnchorHTMLAttributes, forwardRef } from "react"; -import { To, useHref } from "react-router"; +import { forwardRef } from "react"; +import { Link as RouterLink, LinkProps } from "react-router"; -type Props = Omit, "href"> & { - to: To; -}; - -export const Link = forwardRef(({ to, ...props }, ref) => { - const href = useHref(to); - return ; +export const Link = forwardRef(({ ...props }, ref) => { + return ; }); Link.displayName = "Link"; diff --git a/application/client/src/components/foundation/PausableMovie.tsx b/application/client/src/components/foundation/PausableMovie.tsx index 98b0df55b0..9be8b20246 100644 --- a/application/client/src/components/foundation/PausableMovie.tsx +++ b/application/client/src/components/foundation/PausableMovie.tsx @@ -1,90 +1,74 @@ import classNames from "classnames"; -import { Animator, Decoder } from "gifler"; -import { GifReader } from "omggif"; -import { RefCallback, useCallback, useRef, useState } from "react"; - -import { AspectRatioBox } from "@web-speed-hackathon-2026/client/src/components/foundation/AspectRatioBox"; -import { FontAwesomeIcon } from "@web-speed-hackathon-2026/client/src/components/foundation/FontAwesomeIcon"; -import { useFetch } from "@web-speed-hackathon-2026/client/src/hooks/use_fetch"; -import { fetchBinary } from "@web-speed-hackathon-2026/client/src/utils/fetchers"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { getMoviePath, getMoviePreviewPath } from "@web-speed-hackathon-2026/client/src/utils/get_path"; interface Props { - src: string; + movieId: string; + isFull?: boolean; } /** - * クリックすると再生・一時停止を切り替えます。 + * 動画を MP4 最適化して表示します。 + * 画面内に入った時だけ読み込みと再生を開始する(Lazy Loading)ことで、 + * 詳細ページ等で画面外の動画が帯域を消費するのを防ぎます。 */ -export const PausableMovie = ({ src }: Props) => { - const { data, isLoading } = useFetch(src, fetchBinary); - - const animatorRef = useRef(null); - const canvasCallbackRef = useCallback>( - (el) => { - animatorRef.current?.stop(); - - if (el === null || data === null) { - return; - } - - // GIF を解析する - const reader = new GifReader(new Uint8Array(data)); - const frames = Decoder.decodeFramesSync(reader); - const animator = new Animator(reader, frames); +export const PausableMovie = ({ movieId, isFull = false }: Props) => { + const [isLoaded, setIsLoaded] = useState(false); + const [isInView, setIsInView] = useState(false); + + const containerRef = useRef(null); + const src = isFull ? getMoviePath(movieId) : getMoviePreviewPath(movieId); - animator.animateInCanvas(el); - animator.onFrame(frames[0]!); + // 画面内に入ったかどうかを監視 + useEffect(() => { + const target = containerRef.current; + if (!target) return; - // 視覚効果 off のとき GIF を自動再生しない - if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - setIsPlaying(false); - animator.stop(); - } else { - setIsPlaying(true); - animator.start(); - } + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsInView(true); + // 一度画面に入ったら読み込みを開始するので、監視を終了して良い + observer.unobserve(target); + } + }, + { rootMargin: "200px" } // 画面に入る少し前から準備開始 + ); - animatorRef.current = animator; - }, - [data], - ); - - const [isPlaying, setIsPlaying] = useState(true); - const handleClick = useCallback(() => { - setIsPlaying((isPlaying) => { - if (isPlaying) { - animatorRef.current?.stop(); - } else { - animatorRef.current?.start(); - } - return !isPlaying; - }); + observer.observe(target); + return () => observer.disconnect(); }, []); - if (isLoading || data === null) { - return null; - } + const handleLoad = useCallback(() => { + setIsLoaded(true); + }, []); return ( - - - +
    + {!isLoaded && ( +
    + )} + + {/* 画面内に入った時だけ video タグを生成する */} + {isInView && ( +
    ); }; diff --git a/application/client/src/components/foundation/SoundWaveSVG.tsx b/application/client/src/components/foundation/SoundWaveSVG.tsx index d95e63164c..9d351ef9ad 100644 --- a/application/client/src/components/foundation/SoundWaveSVG.tsx +++ b/application/client/src/components/foundation/SoundWaveSVG.tsx @@ -1,31 +1,56 @@ -import _ from "lodash"; import { useEffect, useRef, useState } from "react"; +import { AudioContext } from "standardized-audio-context"; interface ParsedData { max: number; peaks: number[]; } +// AudioContext は再利用する +let sharedAudioCtx: AudioContext | null = null; +function getAudioCtx() { + if (!sharedAudioCtx) { + sharedAudioCtx = new AudioContext(); + } + return sharedAudioCtx; +} + async function calculate(data: ArrayBuffer): Promise { - const audioCtx = new AudioContext(); + const audioCtx = getAudioCtx(); - // 音声をデコードする + // 音声をデコードする(ここはどうしても時間がかかるが、ブラウザ内部で非同期に行われる) const buffer = await audioCtx.decodeAudioData(data.slice(0)); - // 左の音声データの絶対値を取る - const leftData = _.map(buffer.getChannelData(0), Math.abs); - // 右の音声データの絶対値を取る - const rightData = _.map(buffer.getChannelData(1), Math.abs); + + const left = buffer.getChannelData(0); + const right = buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : left; + const length = buffer.length; + + const numChunks = 100; + const chunkSize = Math.floor(length / numChunks); + const peaks: number[] = new Array(numChunks); + let globalMax = 0; - // 左右の音声データの平均を取る - const normalized = _.map(_.zip(leftData, rightData), _.mean); - // 100 個の chunk に分ける - const chunks = _.chunk(normalized, Math.ceil(normalized.length / 100)); - // chunk ごとに平均を取る - const peaks = _.map(chunks, _.mean); - // chunk の平均の中から最大値を取る - const max = _.max(peaks) ?? 0; + // 巨大な配列を lodash で回すのではなく、必要な箇所だけサンプリングして計算する + // これによりメインスレッドの占有時間を劇的に短縮(TBT対策) + for (let i = 0; i < numChunks; i++) { + let sum = 0; + const start = i * chunkSize; + const end = start + chunkSize; + + // 各チャンク内をさらにサンプリングして高速化 + const step = Math.max(1, Math.floor(chunkSize / 100)); + let count = 0; + for (let j = start; j < end; j += step) { + const val = (Math.abs(left[j]) + Math.abs(right[j])) / 2; + sum += val; + count++; + } + const avg = sum / count; + peaks[i] = avg; + if (avg > globalMax) globalMax = avg; + } - return { max, peaks }; + return { max: globalMax || 1, peaks }; } interface Props { @@ -33,33 +58,37 @@ interface Props { } export const SoundWaveSVG = ({ soundData }: Props) => { - const uniqueIdRef = useRef(Math.random().toString(16)); const [{ max, peaks }, setPeaks] = useState({ max: 0, peaks: [], }); useEffect(() => { - calculate(soundData).then(({ max, peaks }) => { - setPeaks({ max, peaks }); + let isMounted = true; + calculate(soundData).then((result) => { + if (isMounted) setPeaks(result); }); + return () => { isMounted = false; }; }, [soundData]); + // 100個の rect を作る代わりに、1つの path で描画して DOM 負荷を削減 + const pathData = peaks.map((peak, i) => { + const ratio = peak / max; + const height = Math.max(0.05, ratio); // 最低限の高さを持たせる + return `M ${i} ${1 - height} L ${i} 1 L ${i+0.8} 1 L ${i+0.8} ${1 - height} Z`; + }).join(" "); + return ( - - {peaks.map((peak, idx) => { - const ratio = peak / max; - return ( - - ); - })} + + ); }; diff --git a/application/client/src/components/new_post_modal/NewPostModalPage.tsx b/application/client/src/components/new_post_modal/NewPostModalPage.tsx index e337c46b74..436db46d0a 100644 --- a/application/client/src/components/new_post_modal/NewPostModalPage.tsx +++ b/application/client/src/components/new_post_modal/NewPostModalPage.tsx @@ -1,13 +1,9 @@ -import { MagickFormat } from "@imagemagick/magick-wasm"; import { ChangeEventHandler, FormEventHandler, useCallback, useState } from "react"; import { FontAwesomeIcon } from "@web-speed-hackathon-2026/client/src/components/foundation/FontAwesomeIcon"; import { ModalErrorMessage } from "@web-speed-hackathon-2026/client/src/components/modal/ModalErrorMessage"; import { ModalSubmitButton } from "@web-speed-hackathon-2026/client/src/components/modal/ModalSubmitButton"; import { AttachFileInputButton } from "@web-speed-hackathon-2026/client/src/components/new_post_modal/AttachFileInputButton"; -import { convertImage } from "@web-speed-hackathon-2026/client/src/utils/convert_image"; -import { convertMovie } from "@web-speed-hackathon-2026/client/src/utils/convert_movie"; -import { convertSound } from "@web-speed-hackathon-2026/client/src/utils/convert_sound"; const MAX_UPLOAD_BYTES_LIMIT = 10 * 1024 * 1024; @@ -35,7 +31,6 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm }); const [hasFileError, setHasFileError] = useState(false); - const [isConverting, setIsConverting] = useState(false); const handleChangeText = useCallback>((ev) => { const value = ev.currentTarget.value; @@ -51,26 +46,12 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm setHasFileError(isValid !== true); if (isValid) { - setIsConverting(true); - - Promise.all( - files.map((file) => - convertImage(file, { extension: MagickFormat.Jpg }).then( - (blob) => new File([blob], "converted.jpg", { type: "image/jpeg" }), - ), - ), - ) - .then((convertedFiles) => { - setParams((params) => ({ - ...params, - images: convertedFiles, - movie: undefined, - sound: undefined, - })); - - setIsConverting(false); - }) - .catch(console.error); + setParams((params) => ({ + ...params, + images: files, + movie: undefined, + sound: undefined, + })); } }, []); @@ -80,18 +61,12 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm setHasFileError(isValid !== true); if (isValid) { - setIsConverting(true); - - convertSound(file, { extension: "mp3" }).then((converted) => { - setParams((params) => ({ - ...params, - images: [], - movie: undefined, - sound: new File([converted], "converted.mp3", { type: "audio/mpeg" }), - })); - - setIsConverting(false); - }); + setParams((params) => ({ + ...params, + images: [], + movie: undefined, + sound: file, + })); } }, []); @@ -101,22 +76,12 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm setHasFileError(isValid !== true); if (isValid) { - setIsConverting(true); - - convertMovie(file, { extension: "gif", size: undefined }) - .then((converted) => { - setParams((params) => ({ - ...params, - images: [], - movie: new File([converted], "converted.gif", { - type: "image/gif", - }), - sound: undefined, - })); - - setIsConverting(false); - }) - .catch(console.error); + setParams((params) => ({ + ...params, + images: [], + movie: file, + sound: undefined, + })); } }, []); @@ -167,10 +132,10 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm
    - {isConverting || isLoading ? "変換中" : "投稿する"} + {isLoading ? "投稿中..." : "投稿する"} diff --git a/application/client/src/components/post/CommentItem.tsx b/application/client/src/components/post/CommentItem.tsx index cb5bd38bda..19e377f885 100644 --- a/application/client/src/components/post/CommentItem.tsx +++ b/application/client/src/components/post/CommentItem.tsx @@ -1,4 +1,4 @@ -import moment from "moment"; +import dayjs from "dayjs"; import { Link } from "@web-speed-hackathon-2026/client/src/components/foundation/Link"; import { TranslatableText } from "@web-speed-hackathon-2026/client/src/components/post/TranslatableText"; @@ -19,6 +19,7 @@ export const CommentItem = ({ comment }: Props) => { > {comment.user.profileImage.alt} @@ -42,8 +43,8 @@ export const CommentItem = ({ comment }: Props) => {

    -

    diff --git a/application/client/src/components/post/ImageArea.tsx b/application/client/src/components/post/ImageArea.tsx index 27fe9c018c..ae5a88b157 100644 --- a/application/client/src/components/post/ImageArea.tsx +++ b/application/client/src/components/post/ImageArea.tsx @@ -6,9 +6,11 @@ import { getImagePath } from "@web-speed-hackathon-2026/client/src/utils/get_pat interface Props { images: Models.Image[]; + isLcpElement?: boolean; + isFull?: boolean; } -export const ImageArea = ({ images }: Props) => { +export const ImageArea = ({ images, isLcpElement = false, isFull = false }: Props) => { return (
    @@ -24,7 +26,12 @@ export const ImageArea = ({ images }: Props) => { "row-span-2": images.length <= 2 || (images.length === 3 && idx === 0), })} > - +
    ); })} diff --git a/application/client/src/components/post/MovieArea.tsx b/application/client/src/components/post/MovieArea.tsx index f9fc54907c..b22ecd4add 100644 --- a/application/client/src/components/post/MovieArea.tsx +++ b/application/client/src/components/post/MovieArea.tsx @@ -3,15 +3,16 @@ import { getMoviePath } from "@web-speed-hackathon-2026/client/src/utils/get_pat interface Props { movie: Models.Movie; + isFull?: boolean; } -export const MovieArea = ({ movie }: Props) => { +export const MovieArea = ({ movie, isFull = false }: Props) => { return (
    - +
    ); }; diff --git a/application/client/src/components/post/PostItem.tsx b/application/client/src/components/post/PostItem.tsx index 5fa904c91a..d61e03ea68 100644 --- a/application/client/src/components/post/PostItem.tsx +++ b/application/client/src/components/post/PostItem.tsx @@ -1,4 +1,4 @@ -import moment from "moment"; +import dayjs from "dayjs"; import { Link } from "@web-speed-hackathon-2026/client/src/components/foundation/Link"; import { ImageArea } from "@web-speed-hackathon-2026/client/src/components/post/ImageArea"; @@ -23,6 +23,7 @@ export const PostItem = ({ post }: Props) => { > {post.user.profileImage.alt} @@ -52,12 +53,12 @@ export const PostItem = ({ post }: Props) => {
  • {post.images?.length > 0 ? (
    - +
    ) : null} {post.movie ? (
    - +
    ) : null} {post.sound ? ( @@ -67,8 +68,8 @@ export const PostItem = ({ post }: Props) => { ) : null}

    -

    diff --git a/application/client/src/components/timeline/Timeline.tsx b/application/client/src/components/timeline/Timeline.tsx index 752a4d973b..57b7cd6fc9 100644 --- a/application/client/src/components/timeline/Timeline.tsx +++ b/application/client/src/components/timeline/Timeline.tsx @@ -7,8 +7,8 @@ interface Props { export const Timeline = ({ timeline }: Props) => { return (
    - {timeline.map((post) => { - return ; + {timeline.map((post, index) => { + return ; })}
    ); diff --git a/application/client/src/components/timeline/TimelineItem.tsx b/application/client/src/components/timeline/TimelineItem.tsx index 21b88980f8..240eea7d40 100644 --- a/application/client/src/components/timeline/TimelineItem.tsx +++ b/application/client/src/components/timeline/TimelineItem.tsx @@ -1,5 +1,6 @@ -import moment from "moment"; +import dayjs from "dayjs"; import { MouseEventHandler, useCallback } from "react"; + import { Link, useNavigate } from "react-router"; import { ImageArea } from "@web-speed-hackathon-2026/client/src/components/post/ImageArea"; @@ -28,10 +29,12 @@ const isClickedAnchorOrButton = (target: EventTarget | null, currentTarget: Elem */ interface Props { post: Models.Post; + index?: number; } -export const TimelineItem = ({ post }: Props) => { +export const TimelineItem = ({ post, index = 0 }: Props) => { const navigate = useNavigate(); + const isLcpElement = index < 3; // 最初の3件は LCP 対策で即時ロード /** * ボタンやリンク以外の箇所をクリックしたとき かつ 文字が選択されてないとき、投稿詳細ページに遷移する @@ -56,6 +59,7 @@ export const TimelineItem = ({ post }: Props) => { > {post.user.profileImage.alt} @@ -76,8 +80,8 @@ export const TimelineItem = ({ post }: Props) => { - -
    {post.images?.length > 0 ? (
    - +
    ) : null} {post.movie ? ( diff --git a/application/client/src/components/user_profile/UserProfileHeader.tsx b/application/client/src/components/user_profile/UserProfileHeader.tsx index c1c3355e19..4c9ed55b42 100644 --- a/application/client/src/components/user_profile/UserProfileHeader.tsx +++ b/application/client/src/components/user_profile/UserProfileHeader.tsx @@ -1,5 +1,5 @@ import { FastAverageColor } from "fast-average-color"; -import moment from "moment"; +import dayjs from "dayjs"; import { ReactEventHandler, useCallback, useState } from "react"; import { FontAwesomeIcon } from "@web-speed-hackathon-2026/client/src/components/foundation/FontAwesomeIcon"; @@ -30,6 +30,7 @@ export const UserProfileHeader = ({ user }: Props) => { @@ -43,8 +44,8 @@ export const UserProfileHeader = ({ user }: Props) => { - diff --git a/application/client/src/containers/AppContainer.tsx b/application/client/src/containers/AppContainer.tsx index d66858a949..42f47aed70 100644 --- a/application/client/src/containers/AppContainer.tsx +++ b/application/client/src/containers/AppContainer.tsx @@ -1,21 +1,66 @@ -import { useCallback, useEffect, useId, useState } from "react"; +import { Suspense, lazy, useCallback, useEffect, useId, useState } from "react"; import { Helmet, HelmetProvider } from "react-helmet"; import { Route, Routes, useLocation, useNavigate } from "react-router"; import { AppPage } from "@web-speed-hackathon-2026/client/src/components/application/AppPage"; -import { AuthModalContainer } from "@web-speed-hackathon-2026/client/src/containers/AuthModalContainer"; -import { CrokContainer } from "@web-speed-hackathon-2026/client/src/containers/CrokContainer"; -import { DirectMessageContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageContainer"; -import { DirectMessageListContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageListContainer"; -import { NewPostModalContainer } from "@web-speed-hackathon-2026/client/src/containers/NewPostModalContainer"; -import { NotFoundContainer } from "@web-speed-hackathon-2026/client/src/containers/NotFoundContainer"; -import { PostContainer } from "@web-speed-hackathon-2026/client/src/containers/PostContainer"; -import { SearchContainer } from "@web-speed-hackathon-2026/client/src/containers/SearchContainer"; -import { TermContainer } from "@web-speed-hackathon-2026/client/src/containers/TermContainer"; -import { TimelineContainer } from "@web-speed-hackathon-2026/client/src/containers/TimelineContainer"; -import { UserProfileContainer } from "@web-speed-hackathon-2026/client/src/containers/UserProfileContainer"; import { fetchJSON, sendJSON } from "@web-speed-hackathon-2026/client/src/utils/fetchers"; +const AuthModalContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/AuthModalContainer").then((m) => ({ + default: m.AuthModalContainer, + })), +); +const CrokContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/CrokContainer").then((m) => ({ + default: m.CrokContainer, + })), +); +const DirectMessageContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/DirectMessageContainer").then((m) => ({ + default: m.DirectMessageContainer, + })), +); +const DirectMessageListContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/DirectMessageListContainer").then((m) => ({ + default: m.DirectMessageListContainer, + })), +); +const NewPostModalContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/NewPostModalContainer").then((m) => ({ + default: m.NewPostModalContainer, + })), +); +const NotFoundContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/NotFoundContainer").then((m) => ({ + default: m.NotFoundContainer, + })), +); +const PostContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/PostContainer").then((m) => ({ + default: m.PostContainer, + })), +); +const SearchContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/SearchContainer").then((m) => ({ + default: m.SearchContainer, + })), +); +const TermContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/TermContainer").then((m) => ({ + default: m.TermContainer, + })), +); +const TimelineContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/TimelineContainer").then((m) => ({ + default: m.TimelineContainer, + })), +); +const UserProfileContainer = lazy(() => + import("@web-speed-hackathon-2026/client/src/containers/UserProfileContainer").then((m) => ({ + default: m.UserProfileContainer, + })), +); + export const AppContainer = () => { const { pathname } = useLocation(); const navigate = useNavigate(); @@ -43,16 +88,8 @@ export const AppContainer = () => { const authModalId = useId(); const newPostModalId = useId(); - if (isLoadingActiveUser) { - return ( - - - 読込中 - CaX - - - ); - } - + // isLoadingActiveUser によるブロックを廃止 + // ユーザー情報はバックグラウンドで読み込み、完了次第UIに反映される return ( { newPostModalId={newPostModalId} onLogout={handleLogout} > - - } path="/" /> - - } - path="/dm" - /> - } - path="/dm/:conversationId" - /> - } path="/search" /> - } path="/users/:username" /> - } path="/posts/:postId" /> - } path="/terms" /> - } - path="/crok" - /> - } path="*" /> - + + + } path="/" /> + + } + path="/dm" + /> + } + path="/dm/:conversationId" + /> + } path="/search" /> + } path="/users/:username" /> + } path="/posts/:postId" /> + } path="/terms" /> + } + path="/crok" + /> + } path="*" /> + + - - + + + + ); }; diff --git a/application/client/src/containers/DirectMessageContainer.tsx b/application/client/src/containers/DirectMessageContainer.tsx index 245deac8a6..3cebfc820e 100644 --- a/application/client/src/containers/DirectMessageContainer.tsx +++ b/application/client/src/containers/DirectMessageContainer.tsx @@ -19,6 +19,7 @@ interface DmTypingEvent { } const TYPING_INDICATOR_DURATION_MS = 10 * 1000; +const MESSAGES_PER_PAGE = 50; interface Props { activeUser: Models.User | null; @@ -31,27 +32,53 @@ export const DirectMessageContainer = ({ activeUser, authModalId }: Props) => { const [conversation, setConversation] = useState(null); const [conversationError, setConversationError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const [hasMore, setHasMore] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); const [isPeerTyping, setIsPeerTyping] = useState(false); const peerTypingTimeoutRef = useRef | null>(null); - const loadConversation = useCallback(async () => { + const loadConversation = useCallback(async (offset = 0) => { if (activeUser == null) { return; } try { const data = await fetchJSON( - `/api/v1/dm/${conversationId}`, + `/api/v1/dm/${conversationId}?limit=${MESSAGES_PER_PAGE}&offset=${offset}`, ); - setConversation(data); + + if (offset === 0) { + setConversation(data); + } else { + setConversation((prev) => { + if (prev == null) return data; + return { + ...prev, + messages: [...data.messages, ...prev.messages], + }; + }); + } + + // もし取得件数がページ上限と同じなら、さらに古いメッセージがある可能性がある + setHasMore(data.messages.length === MESSAGES_PER_PAGE); setConversationError(null); } catch (error) { - setConversation(null); - setConversationError(error as Error); + if (offset === 0) { + setConversation(null); + setConversationError(error as Error); + } } }, [activeUser, conversationId]); + const loadMore = useCallback(async () => { + if (conversation == null || isLoadingMore || !hasMore) return; + + setIsLoadingMore(true); + await loadConversation(conversation.messages.length); + setIsLoadingMore(false); + }, [conversation, isLoadingMore, hasMore, loadConversation]); + const sendRead = useCallback(async () => { await sendJSON(`/api/v1/dm/${conversationId}/read`, {}); }, [conversationId]); @@ -63,17 +90,53 @@ export const DirectMessageContainer = ({ activeUser, authModalId }: Props) => { const handleSubmit = useCallback( async (params: DirectMessageFormData) => { + if (activeUser == null) return; + + const tempId = `temp-${Date.now()}`; + const optimisticMessage: Models.DirectMessage = { + id: tempId, + body: params.body, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + isRead: false, + sender: activeUser, + }; + + setConversation((prev) => { + if (prev == null) return null; + return { + ...prev, + messages: [...prev.messages, optimisticMessage], + }; + }); + setIsSubmitting(true); try { - await sendJSON(`/api/v1/dm/${conversationId}/messages`, { + const newMessage = await sendJSON(`/api/v1/dm/${conversationId}/messages`, { body: params.body, }); - loadConversation(); + + setConversation((prev) => { + if (prev == null) return null; + return { + ...prev, + messages: prev.messages.map((m) => (m.id === tempId ? newMessage : m)), + }; + }); + } catch (error) { + setConversation((prev) => { + if (prev == null) return null; + return { + ...prev, + messages: prev.messages.filter((m) => m.id !== tempId), + }; + }); + alert("メッセージの送信に失敗しました"); } finally { setIsSubmitting(false); } }, - [conversationId, loadConversation], + [conversationId, activeUser], ); const handleTyping = useCallback(async () => { @@ -82,16 +145,24 @@ export const DirectMessageContainer = ({ activeUser, authModalId }: Props) => { useWs(`/api/v1/dm/${conversationId}`, (event: DmUpdateEvent | DmTypingEvent) => { if (event.type === "dm:conversation:message") { - void loadConversation().then(() => { - if (event.payload.sender.id !== activeUser?.id) { - setIsPeerTyping(false); - if (peerTypingTimeoutRef.current !== null) { - clearTimeout(peerTypingTimeoutRef.current); - } - peerTypingTimeoutRef.current = null; - } + const newMessage = event.payload; + setConversation((prev) => { + if (prev == null) return null; + if (prev.messages.some((m) => m.id === newMessage.id)) return prev; + return { + ...prev, + messages: [...prev.messages, newMessage], + }; }); - void sendRead(); + + if (newMessage.sender.id !== activeUser?.id) { + setIsPeerTyping(false); + if (peerTypingTimeoutRef.current !== null) { + clearTimeout(peerTypingTimeoutRef.current); + } + peerTypingTimeoutRef.current = null; + void sendRead(); + } } else if (event.type === "dm:conversation:typing") { setIsPeerTyping(true); if (peerTypingTimeoutRef.current !== null) { @@ -135,6 +206,9 @@ export const DirectMessageContainer = ({ activeUser, authModalId }: Props) => { isPeerTyping={isPeerTyping} isSubmitting={isSubmitting} onSubmit={handleSubmit} + hasMore={hasMore} + isLoadingMore={isLoadingMore} + onLoadMore={loadMore} /> ); diff --git a/application/client/src/containers/SearchContainer.tsx b/application/client/src/containers/SearchContainer.tsx index f5cdd4148f..c33cc887a0 100644 --- a/application/client/src/containers/SearchContainer.tsx +++ b/application/client/src/containers/SearchContainer.tsx @@ -20,7 +20,7 @@ export const SearchContainer = () => { 検索 - CaX - + ); }; diff --git a/application/client/src/hooks/use_infinite_fetch.ts b/application/client/src/hooks/use_infinite_fetch.ts index 394fccd9ea..b779518b0f 100644 --- a/application/client/src/hooks/use_infinite_fetch.ts +++ b/application/client/src/hooks/use_infinite_fetch.ts @@ -2,28 +2,42 @@ import { useCallback, useEffect, useRef, useState } from "react"; const LIMIT = 30; +// クライアント側でのメモリキャッシュ +const globalCache = new Map(); + interface ReturnValues { data: Array; error: Error | null; isLoading: boolean; fetchMore: () => void; + hasMore: boolean; } export function useInfiniteFetch( apiPath: string, fetcher: (apiPath: string) => Promise, ): ReturnValues { - const internalRef = useRef({ isLoading: false, offset: 0 }); + // キャッシュがあればそれを初期値にする + const cachedData = apiPath ? globalCache.get(apiPath) || [] : []; + + const internalRef = useRef({ + isLoading: false, + offset: cachedData.length, + hasMore: true + }); const [result, setResult] = useState, "fetchMore">>({ - data: [], + data: cachedData, error: null, - isLoading: true, + isLoading: cachedData.length === 0, // キャッシュがあればロード中ではない + hasMore: true, }); - const fetchMore = useCallback(() => { - const { isLoading, offset } = internalRef.current; - if (isLoading) { + const fetchMore = useCallback((isInitial = false) => { + const { isLoading, offset, hasMore } = internalRef.current; + + // 初回フェッチ以外で、ロード中またはこれ以上データがない場合はスキップ + if (!isInitial && (isLoading || !hasMore)) { return; } @@ -31,21 +45,34 @@ export function useInfiniteFetch( ...cur, isLoading: true, })); - internalRef.current = { - isLoading: true, - offset, - }; + internalRef.current.isLoading = true; + + const currentOffset = isInitial ? 0 : offset; + const separator = apiPath.includes("?") ? "&" : "?"; + const paginatedPath = `${apiPath}${separator}limit=${LIMIT}&offset=${currentOffset}`; + + void fetcher(paginatedPath).then( + (newData) => { + const nextHasMore = newData.length === LIMIT; + + setResult((cur) => { + const combinedData = isInitial ? newData : [...cur.data, ...newData]; + // キャッシュを更新 + if (apiPath) { + globalCache.set(apiPath, combinedData); + } + return { + ...cur, + data: combinedData, + isLoading: false, + hasMore: nextHasMore, + }; + }); - void fetcher(apiPath).then( - (allData) => { - setResult((cur) => ({ - ...cur, - data: [...cur.data, ...allData.slice(offset, offset + LIMIT)], - isLoading: false, - })); internalRef.current = { isLoading: false, - offset: offset + LIMIT, + offset: currentOffset + LIMIT, + hasMore: nextHasMore, }; }, (error) => { @@ -54,26 +81,20 @@ export function useInfiniteFetch( error, isLoading: false, })); - internalRef.current = { - isLoading: false, - offset, - }; + internalRef.current.isLoading = false; }, ); }, [apiPath, fetcher]); useEffect(() => { - setResult(() => ({ - data: [], - error: null, - isLoading: true, - })); + // 既にデータがある場合(キャッシュヒット時)でも、最新を確認するために一度フェッチする + // ただし、画面がチラつかないように裏側で実行される internalRef.current = { isLoading: false, offset: 0, + hasMore: true, }; - - fetchMore(); + fetchMore(true); }, [fetchMore]); return { @@ -81,3 +102,4 @@ export function useInfiniteFetch( fetchMore, }; } + diff --git a/application/client/src/hooks/use_sse.ts b/application/client/src/hooks/use_sse.ts index 24532a9c5a..ccad0cea2d 100644 --- a/application/client/src/hooks/use_sse.ts +++ b/application/client/src/hooks/use_sse.ts @@ -60,8 +60,13 @@ export function useSSE(options: SSEOptions): ReturnValues { }; eventSource.onerror = (error) => { - console.error("SSE Error:", error); - stop(); + // 接続が完了した後の終了(done)をエラーと誤認しないようにチェック + if (eventSource.readyState === EventSource.CLOSED) { + stop(); + } else { + console.error("SSE Error:", error); + stop(); + } }; }, [options, stop], diff --git a/application/client/src/index.css b/application/client/src/index.css index 8612ebcdd2..84a93e34ab 100644 --- a/application/client/src/index.css +++ b/application/client/src/index.css @@ -1,25 +1,174 @@ @layer normalize, theme, base, components, utilities; @import "normalize.css" layer(normalize); - -@font-face { - /* Source Han Serif JP Regular の Y 軸を 1/1.43 に縮小した改変フォント */ - font-family: "Rei no Are Mincho"; - font-display: block; - src: url(/fonts/ReiNoAreMincho-Regular.otf) format("opentype"); - font-weight: normal; -} - -@font-face { - /* Source Han Serif JP Heavy の Y 軸を 1/1.43 に縮小した改変フォント */ - font-family: "Rei no Are Mincho"; - font-display: block; - src: url(/fonts/ReiNoAreMincho-Heavy.otf) format("opentype"); - font-weight: bold; -} +@import "tailwindcss" layer(theme); .font-awesome { height: 1em; vertical-align: -0.125em; width: 1em; } + +@layer base { + button:not(:disabled), + [role="button"]:not(:disabled) { + cursor: pointer; + } +} + +@theme { + --color-cax-canvas: var(--color-stone-100); + --color-cax-surface: var(--color-white); + --color-cax-surface-raised: var(--color-white); + --color-cax-surface-subtle: var(--color-stone-50); + --color-cax-overlay: var(--color-slate-950); + --color-cax-border: var(--color-stone-300); + --color-cax-border-strong: var(--color-stone-400); + --color-cax-text: var(--color-teal-950); + --color-cax-text-muted: var(--color-teal-700); + --color-cax-text-subtle: var(--color-slate-500); + --color-cax-brand: var(--color-teal-700); + --color-cax-brand-strong: var(--color-teal-800); + --color-cax-brand-soft: var(--color-teal-100); + --color-cax-accent: var(--color-orange-700); + --color-cax-accent-soft: var(--color-orange-100); + --color-cax-danger: var(--color-red-600); + --color-cax-danger-soft: var(--color-red-100); + --color-cax-highlight: var(--color-amber-200); + --color-cax-highlight-ink: var(--color-amber-950); +} + +@utility markdown { + @apply text-sm wrap-anywhere; + line-break: strict; + + /* インライン要素 */ + :where(a) { + @apply text-cax-accent decoration-cax-accent underline underline-offset-2; + } + :where(strong) { + @apply font-bold; + } + :where(em) { + @apply italic; + } + :where(code):not(:where(pre > code)) { + @apply bg-cax-surface-subtle text-cax-text rounded px-1 py-0.5 font-mono; + } + :where(del) { + @apply decoration-cax-text-subtle line-through; + } + + /* ブロック要素 */ + :where(p) { + @apply text-cax-text my-6; + } + :where(blockquote) { + @apply border-cax-border text-cax-text-muted my-6 border-l-4 pl-4; + } + :where(hr) { + @apply border-cax-border my-10 border-t; + } + + /* リスト */ + :where(ol) { + @apply my-6 list-decimal pl-6; + } + :where(ul) { + @apply my-6 list-disc pl-6; + } + :where(li) { + @apply my-2; + } + :where(ol > li, ul > li)::marker { + @apply text-cax-text-muted; + } + :where(ol ol, ul ul, ol ul, ul ol) { + @apply my-2; + } + + /* テーブル */ + :where(table) { + @apply text-cax-text my-6 w-full table-auto text-sm; + } + :where(thead) { + @apply border-cax-border border-b; + } + :where(thead th) { + @apply px-2 pb-1.5 font-bold; + } + :where(tbody tr) { + @apply border-cax-border border-b; + } + :where(tbody tr:last-child) { + @apply border-b-0; + } + :where(tbody td) { + @apply align-baseline; + } + :where(tfoot) { + @apply border-cax-border border-t; + } + :where(tfoot td) { + @apply align-top; + } + :where(th, td) { + @apply text-left; + } + :where(tbody td, tfoot td) { + @apply px-2 py-1.5; + } + :where(tbody tr:last-child td, tfoot tr:last-child td) { + @apply pb-0; + } + + /* 見出し */ + :where(h1, h2, h3, h4, h5, h6) { + @apply text-cax-text text-pretty; + } + :where(h1, h2, h3, h4) { + @apply font-bold; + } + :where(h1, h2) { + @apply mt-10 mb-8 text-2xl; + } + :where(h3) { + @apply mt-8 mb-6 text-xl; + } + :where(h4) { + @apply mt-6 mb-4 text-lg; + } + :where(h5) { + @apply border-cax-border mt-6 mb-4 border-b py-0.5 pl-2 font-bold; + } + :where(h6) { + @apply border-cax-border mt-6 mb-4 border-b py-0.5 pl-2; + } + :where(h1 + *, h2 + *, h3 + *, h4 + *, h5 + *, h6 + *) { + @apply mt-0; + } + + /* 注釈 */ + :where(.footnotes) { + @apply border-cax-border mt-8 border-t; + } + :where(.footnotes h2) { + @apply sr-only; + } + :where(.footnotes ol) { + @apply mt-8 mb-0 text-sm; + } + :where(.footnotes ol li p) { + @apply my-0; + } + + /* 最初の要素はマージンを0にする */ + & > *:first-child { + @apply mt-0; + } + + /* 最後の要素はマージンを0にする */ + & > *:last-child { + @apply mb-0; + } +} diff --git a/application/client/src/index.html b/application/client/src/index.html index 3d949e7473..87d31d84f6 100644 --- a/application/client/src/index.html +++ b/application/client/src/index.html @@ -3,175 +3,9 @@ + + CaX - - - -
    diff --git a/application/client/src/index.tsx b/application/client/src/index.tsx index b1833b0af3..e609ae3464 100644 --- a/application/client/src/index.tsx +++ b/application/client/src/index.tsx @@ -1,16 +1,35 @@ import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; import { BrowserRouter } from "react-router"; +import dayjs from "dayjs"; +import "dayjs/locale/ja"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import relativeTime from "dayjs/plugin/relativeTime"; import { AppContainer } from "@web-speed-hackathon-2026/client/src/containers/AppContainer"; import { store } from "@web-speed-hackathon-2026/client/src/store"; -window.addEventListener("load", () => { - createRoot(document.getElementById("app")!).render( +// dayjs のグローバル設定 +dayjs.extend(localizedFormat); +dayjs.extend(relativeTime); +dayjs.locale("ja"); + +const container = document.getElementById("app"); +if (container) { + // Service Worker の登録 + if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js").catch((error) => { + console.error("SW registration failed: ", error); + }); + }); + } + + createRoot(container).render( , ); -}); +} diff --git a/application/client/src/sw.js b/application/client/src/sw.js new file mode 100644 index 0000000000..ca794738be --- /dev/null +++ b/application/client/src/sw.js @@ -0,0 +1,59 @@ +const CACHE_NAME = 'cax-cache-v1'; +const ASSETS_TO_CACHE = [ + '/', + '/index.html', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(ASSETS_TO_CACHE); + }) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + // GET リクエストのみキャッシュ対象とする + if (event.request.method !== 'GET') return; + + // API リクエストなどはキャッシュしない(または別途戦略を立てる) + const url = new URL(event.request.url); + if (url.pathname.startsWith('/api/')) return; + + event.respondWith( + caches.match(event.request).then((response) => { + // キャッシュがあればそれを返し、なければネットワークから取得 + return response || fetch(event.request).then((fetchResponse) => { + // 静的ファイル(scripts, styles, images など)は取得時にキャッシュに追加する + if (fetchResponse.ok && ( + url.pathname.startsWith('/scripts/') || + url.pathname.startsWith('/styles/') || + url.pathname.startsWith('/images/') || + url.pathname.startsWith('/fonts/') + )) { + const responseToCache = fetchResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + } + return fetchResponse; + }); + }) + ); +}); diff --git a/application/client/src/utils/bm25_search.ts b/application/client/src/utils/bm25_search.ts index c590d12c09..9cb865a146 100644 --- a/application/client/src/utils/bm25_search.ts +++ b/application/client/src/utils/bm25_search.ts @@ -1,5 +1,4 @@ -import { BM25 } from "bayesian-bm25"; -import type { Tokenizer, IpadicFeatures } from "kuromoji"; +import Bluebird from "bluebird"; import _ from "lodash"; const STOP_POS = new Set(["助詞", "助動詞", "記号"]); @@ -7,7 +6,7 @@ const STOP_POS = new Set(["助詞", "助動詞", "記号"]); /** * 形態素解析で内容語トークン(名詞、動詞、形容詞など)を抽出 */ -export function extractTokens(tokens: IpadicFeatures[]): string[] { +export function extractTokens(tokens: any[]): string[] { return tokens .filter((t) => t.surface_form !== "" && t.pos !== "" && !STOP_POS.has(t.pos)) .map((t) => t.surface_form.toLowerCase()); @@ -16,13 +15,15 @@ export function extractTokens(tokens: IpadicFeatures[]): string[] { /** * BM25で候補をスコアリングして、クエリと類似度の高い上位10件を返す */ -export function filterSuggestionsBM25( - tokenizer: Tokenizer, +export async function filterSuggestionsBM25( + tokenizer: any, candidates: string[], queryTokens: string[], -): string[] { +): Promise { if (queryTokens.length === 0) return []; + // Dynamic import to avoid bundling bayesian-bm25 into the main bundle + const { BM25 } = await import("bayesian-bm25"); const bm25 = new BM25({ k1: 1.2, b: 0.75 }); const tokenizedCandidates = candidates.map((c) => extractTokens(tokenizer.tokenize(c))); @@ -40,3 +41,29 @@ export function filterSuggestionsBM25( .map((s) => s.text) .value(); } + +export async function searchBM25( + corpus: string[], + query: string, + options: { k1: number; b: number } = { k1: 1.2, b: 0.75 }, +): Promise { + // Dynamic import to avoid bundling kuromoji and bayesian-bm25 into the main bundle + const { default: kuromoji } = await import("kuromoji"); + const { BM25 } = await import("bayesian-bm25"); + + const builder = Bluebird.promisifyAll(kuromoji.builder({ dicPath: "/dicts" })); + + const tokenizer = await (builder as any).buildAsync(); + + const documents = corpus.map((text) => + tokenizer.tokenize(text).map((token: any) => token.surface_form), + ); + const queryTokens = tokenizer.tokenize(query).map((token: any) => token.surface_form); + + const bm25 = new BM25(documents, { + k1: options.k1, + b: options.b, + }); + + return bm25.search(queryTokens); +} diff --git a/application/client/src/utils/convert_image.ts b/application/client/src/utils/convert_image.ts deleted file mode 100644 index 9fce086d9c..0000000000 --- a/application/client/src/utils/convert_image.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { initializeImageMagick, ImageMagick, MagickFormat } from "@imagemagick/magick-wasm"; -import magickWasm from "@imagemagick/magick-wasm/magick.wasm?binary"; -import { dump, insert, ImageIFD } from "piexifjs"; - -interface Options { - extension: MagickFormat; -} - -export async function convertImage(file: File, options: Options): Promise { - await initializeImageMagick(magickWasm); - - const byteArray = new Uint8Array(await file.arrayBuffer()); - - return new Promise((resolve) => { - ImageMagick.read(byteArray, (img) => { - img.format = options.extension; - - const comment = img.comment; - - img.write((output) => { - if (comment == null) { - resolve(new Blob([output as Uint8Array])); - return; - } - - // ImageMagick では EXIF の ImageDescription フィールドに保存されているデータが - // 非標準の Comment フィールドに移されてしまうため - // piexifjs を使って ImageDescription フィールドに書き込む - const binary = Array.from(output as Uint8Array) - .map((b) => String.fromCharCode(b)) - .join(""); - const descriptionBinary = Array.from(new TextEncoder().encode(comment)) - .map((b) => String.fromCharCode(b)) - .join(""); - const exifStr = dump({ "0th": { [ImageIFD.ImageDescription]: descriptionBinary } }); - const outputWithExif = insert(exifStr, binary); - const bytes = Uint8Array.from(outputWithExif.split("").map((c) => c.charCodeAt(0))); - resolve(new Blob([bytes])); - }); - }); - }); -} diff --git a/application/client/src/utils/convert_movie.ts b/application/client/src/utils/convert_movie.ts deleted file mode 100644 index fa08b4a003..0000000000 --- a/application/client/src/utils/convert_movie.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { loadFFmpeg } from "@web-speed-hackathon-2026/client/src/utils/load_ffmpeg"; - -interface Options { - extension: string; - size?: number | undefined; -} - -/** - * 先頭 5 秒のみ、正方形にくり抜かれた無音動画を作成します - */ -export async function convertMovie(file: File, options: Options): Promise { - const ffmpeg = await loadFFmpeg(); - - const cropOptions = [ - "'min(iw,ih)':'min(iw,ih)'", - options.size ? `scale=${options.size}:${options.size}` : undefined, - ] - .filter(Boolean) - .join(","); - const exportFile = `export.${options.extension}`; - - await ffmpeg.writeFile("file", new Uint8Array(await file.arrayBuffer())); - - await ffmpeg.exec([ - "-i", - "file", - "-t", - "5", - "-r", - "10", - "-vf", - `crop=${cropOptions}`, - "-an", - exportFile, - ]); - - const output = (await ffmpeg.readFile(exportFile)) as Uint8Array; - - ffmpeg.terminate(); - - const blob = new Blob([output]); - return blob; -} diff --git a/application/client/src/utils/convert_sound.ts b/application/client/src/utils/convert_sound.ts deleted file mode 100644 index 79cc37ab2d..0000000000 --- a/application/client/src/utils/convert_sound.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { extractMetadataFromSound } from "@web-speed-hackathon-2026/client/src/utils/extract_metadata_from_sound"; -import { loadFFmpeg } from "@web-speed-hackathon-2026/client/src/utils/load_ffmpeg"; - -interface Options { - extension: string; -} - -export async function convertSound(file: File, options: Options): Promise { - const ffmpeg = await loadFFmpeg(); - - const exportFile = `export.${options.extension}`; - - await ffmpeg.writeFile("file", new Uint8Array(await file.arrayBuffer())); - - // 文字化けを防ぐためにメタデータを抽出して付与し直す - const metadata = await extractMetadataFromSound(file); - - await ffmpeg.exec([ - "-i", - "file", - "-metadata", - `artist=${metadata.artist}`, - "-metadata", - `title=${metadata.title}`, - "-vn", - exportFile, - ]); - - const output = (await ffmpeg.readFile(exportFile)) as Uint8Array; - - ffmpeg.terminate(); - - const blob = new Blob([output]); - return blob; -} diff --git a/application/client/src/utils/create_translator.ts b/application/client/src/utils/create_translator.ts index ad1dabad22..2e294d1620 100644 --- a/application/client/src/utils/create_translator.ts +++ b/application/client/src/utils/create_translator.ts @@ -1,4 +1,3 @@ -import { CreateMLCEngine } from "@mlc-ai/web-llm"; import { stripIndents } from "common-tags"; import * as JSONRepairJS from "json-repair-js"; import langs from "langs"; @@ -21,6 +20,8 @@ export async function createTranslator(params: Params): Promise { const targetLang = langs.where("1", params.targetLanguage); invariant(targetLang, `Unsupported target language code: ${params.targetLanguage}`); + // Dynamic import to avoid bundling web-llm into the main bundle + const { CreateMLCEngine } = await import("@mlc-ai/web-llm"); const engine = await CreateMLCEngine("gemma-2-2b-jpn-it-q4f16_1-MLC"); return { diff --git a/application/client/src/utils/extract_metadata_from_sound.ts b/application/client/src/utils/extract_metadata_from_sound.ts deleted file mode 100644 index 5e3ee41fe1..0000000000 --- a/application/client/src/utils/extract_metadata_from_sound.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Encoding from "encoding-japanese"; - -import { loadFFmpeg } from "@web-speed-hackathon-2026/client/src/utils/load_ffmpeg"; - -interface SoundMetadata { - artist: string; - title: string; - [key: string]: string; -} - -const UNKNOWN_ARTIST = "Unknown Artist"; -const UNKNOWN_TITLE = "Unknown Title"; - -export async function extractMetadataFromSound(data: File): Promise { - try { - const ffmpeg = await loadFFmpeg(); - - const exportFile = "meta.txt"; - - await ffmpeg.writeFile("file", new Uint8Array(await data.arrayBuffer())); - - await ffmpeg.exec(["-i", "file", "-f", "ffmetadata", exportFile]); - - const output = (await ffmpeg.readFile(exportFile)) as Uint8Array; - - ffmpeg.terminate(); - - const outputUtf8 = Encoding.convert(output, { - to: "UNICODE", - from: "AUTO", - type: "string", - }); - - const meta = parseFFmetadata(outputUtf8); - - return { - artist: meta.artist ?? UNKNOWN_ARTIST, - title: meta.title ?? UNKNOWN_TITLE, - }; - } catch { - return { - artist: UNKNOWN_ARTIST, - title: UNKNOWN_TITLE, - }; - } -} - -function parseFFmetadata(ffmetadata: string): Partial { - return Object.fromEntries( - ffmetadata - .split("\n") - .filter((line) => !line.startsWith(";") && line.includes("=")) - .map((line) => line.split("=")) - .map(([key, value]) => [key!.trim(), value!.trim()]), - ) as Partial; -} diff --git a/application/client/src/utils/fetchers.ts b/application/client/src/utils/fetchers.ts index 92a14f408f..28168dda89 100644 --- a/application/client/src/utils/fetchers.ts +++ b/application/client/src/utils/fetchers.ts @@ -1,40 +1,28 @@ -import $ from "jquery"; import { gzip } from "pako"; export async function fetchBinary(url: string): Promise { - const result = await $.ajax({ - async: false, - dataType: "binary", - method: "GET", - responseType: "arraybuffer", - url, - }); - return result; + const response = await fetch(url, { credentials: "same-origin" }); + if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); + return await response.arrayBuffer(); } export async function fetchJSON(url: string): Promise { - const result = await $.ajax({ - async: false, - dataType: "json", - method: "GET", - url, - }); - return result; + const response = await fetch(url, { credentials: "same-origin" }); + if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); + return await response.json(); } export async function sendFile(url: string, file: File): Promise { - const result = await $.ajax({ - async: false, - data: file, - dataType: "json", + const response = await fetch(url, { + body: file, + credentials: "same-origin", headers: { "Content-Type": "application/octet-stream", }, method: "POST", - processData: false, - url, }); - return result; + if (!response.ok) throw new Error(`Post failed: ${response.status}`); + return await response.json(); } export async function sendJSON(url: string, data: object): Promise { @@ -42,17 +30,15 @@ export async function sendJSON(url: string, data: object): Promise { const uint8Array = new TextEncoder().encode(jsonString); const compressed = gzip(uint8Array); - const result = await $.ajax({ - async: false, - data: compressed, - dataType: "json", + const response = await fetch(url, { + body: compressed, + credentials: "same-origin", headers: { "Content-Encoding": "gzip", "Content-Type": "application/json", }, method: "POST", - processData: false, - url, }); - return result; + if (!response.ok) throw new Error(`Post failed: ${response.status}`); + return await response.json(); } diff --git a/application/client/src/utils/get_path.ts b/application/client/src/utils/get_path.ts index 0e3497f56c..a4fd49f6ef 100644 --- a/application/client/src/utils/get_path.ts +++ b/application/client/src/utils/get_path.ts @@ -1,9 +1,17 @@ export function getImagePath(imageId: string): string { - return `/images/${imageId}.jpg`; + return `/images/${imageId}.webp`; +} + +export function getImageThumbnailPath(imageId: string): string { + return `/api/v1/images/${imageId}/thumbnail`; } export function getMoviePath(movieId: string): string { - return `/movies/${movieId}.gif`; + return `/movies/${movieId}.mp4`; +} + +export function getMoviePreviewPath(movieId: string): string { + return `/api/v1/movies/${movieId}/preview`; } export function getSoundPath(soundId: string): string { @@ -11,5 +19,5 @@ export function getSoundPath(soundId: string): string { } export function getProfileImagePath(profileImageId: string): string { - return `/images/profiles/${profileImageId}.jpg`; + return `/images/profiles/${profileImageId}.webp`; } diff --git a/application/client/src/utils/load_ffmpeg.ts b/application/client/src/utils/load_ffmpeg.ts deleted file mode 100644 index f923a3d5a4..0000000000 --- a/application/client/src/utils/load_ffmpeg.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FFmpeg } from "@ffmpeg/ffmpeg"; - -export async function loadFFmpeg(): Promise { - const ffmpeg = new FFmpeg(); - - await ffmpeg.load({ - coreURL: await import("@ffmpeg/core?binary").then(({ default: b }) => { - return URL.createObjectURL(new Blob([b], { type: "text/javascript" })); - }), - wasmURL: await import("@ffmpeg/core/wasm?binary").then(({ default: b }) => { - return URL.createObjectURL(new Blob([b], { type: "application/wasm" })); - }), - }); - - return ffmpeg; -} diff --git a/application/client/src/utils/negaposi_analyzer.ts b/application/client/src/utils/negaposi_analyzer.ts index f81ed5f4ea..4441d14dac 100644 --- a/application/client/src/utils/negaposi_analyzer.ts +++ b/application/client/src/utils/negaposi_analyzer.ts @@ -1,31 +1,25 @@ import Bluebird from "bluebird"; -import kuromoji, { type Tokenizer, type IpadicFeatures } from "kuromoji"; -import analyze from "negaposi-analyzer-ja"; -async function getTokenizer(): Promise> { - const builder = Bluebird.promisifyAll(kuromoji.builder({ dicPath: "/dicts" })); - return await builder.buildAsync(); +let tokenizerPromise: Promise | null = null; + +async function getTokenizer() { + if (tokenizerPromise) return tokenizerPromise; + + tokenizerPromise = (async () => { + const { default: kuromoji } = await import("kuromoji"); + const builder = Bluebird.promisifyAll(kuromoji.builder({ dicPath: "/dicts" })); + return await (builder as any).buildAsync(); + })(); + + return tokenizerPromise; } -type SentimentResult = { - score: number; - label: "positive" | "negative" | "neutral"; -}; +export async function analyzeSentiment(text: string): Promise { + // Dynamic import to avoid bundling kuromoji and negaposi-analyzer into the main bundle + const { default: analyze } = await import("negaposi-analyzer-ja"); -export async function analyzeSentiment(text: string): Promise { const tokenizer = await getTokenizer(); const tokens = tokenizer.tokenize(text); - const score = analyze(tokens); - - let label: SentimentResult["label"]; - if (score > 0.1) { - label = "positive"; - } else if (score < -0.1) { - label = "negative"; - } else { - label = "neutral"; - } - - return { score, label }; + return analyze(tokens); } diff --git a/application/client/webpack.config.js b/application/client/webpack.config.js index 9fae72647f..1ab3ac8230 100644 --- a/application/client/webpack.config.js +++ b/application/client/webpack.config.js @@ -4,6 +4,7 @@ const path = require("path"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const CompressionPlugin = require("compression-webpack-plugin"); const webpack = require("webpack"); const SRC_PATH = path.resolve(__dirname, "./src"); @@ -25,18 +26,15 @@ const config = { ], static: [PUBLIC_PATH, UPLOAD_PATH], }, - devtool: "inline-source-map", + devtool: process.env.NODE_ENV === "production" ? "source-map" : "eval-source-map", entry: { main: [ - "core-js", - "regenerator-runtime/runtime", - "jquery-binarytransport", path.resolve(SRC_PATH, "./index.css"), path.resolve(SRC_PATH, "./buildinfo.ts"), path.resolve(SRC_PATH, "./index.tsx"), ], }, - mode: "none", + mode: process.env.NODE_ENV === "production" ? "production" : "development", module: { rules: [ { @@ -54,24 +52,24 @@ const config = { }, { resourceQuery: /binary/, - type: "asset/bytes", + type: "asset/resource", + generator: { + filename: "binaries/[name].[contenthash][ext]", + }, }, ], }, output: { chunkFilename: "scripts/chunk-[contenthash].js", - chunkFormat: false, - filename: "scripts/[name].js", + chunkFormat: "array-push", + filename: "scripts/[name].[contenthash].js", path: DIST_PATH, - publicPath: "auto", + publicPath: "/", clean: true, }, plugins: [ new webpack.ProvidePlugin({ - $: "jquery", - AudioContext: ["standardized-audio-context", "AudioContext"], Buffer: ["buffer", "Buffer"], - "window.jQuery": "jquery", }), new webpack.EnvironmentPlugin({ BUILD_DATE: new Date().toISOString(), @@ -80,7 +78,7 @@ const config = { NODE_ENV: "development", }), new MiniCssExtractPlugin({ - filename: "styles/[name].css", + filename: "styles/[name].[contenthash].css", }), new CopyWebpackPlugin({ patterns: [ @@ -88,12 +86,21 @@ const config = { from: path.resolve(__dirname, "node_modules/katex/dist/fonts"), to: path.resolve(DIST_PATH, "styles/fonts"), }, + { + from: path.resolve(SRC_PATH, "sw.js"), + to: DIST_PATH, + }, ], }), new HtmlWebpackPlugin({ - inject: false, + inject: true, template: path.resolve(SRC_PATH, "./index.html"), }), + new CompressionPlugin({ + test: /\.(js|css|html|svg|ttf|woff|woff2)$/, + threshold: 10240, + minRatio: 0.8, + }), ], resolve: { extensions: [".tsx", ".ts", ".mjs", ".cjs", ".jsx", ".js"], @@ -128,14 +135,38 @@ const config = { }, }, optimization: { - minimize: false, - splitChunks: false, - concatenateModules: false, - usedExports: false, - providedExports: false, - sideEffects: false, + minimize: process.env.NODE_ENV === "production", + splitChunks: { + chunks: "async", + minSize: 30000, + cacheGroups: { + // 巨大なライブラリを個別に切り出す + kuromoji: { + test: /[\\/]node_modules[\\/]kuromoji[\\/]/, + name: "lib-kuromoji", + priority: 20, + }, + negaposi: { + test: /[\\/]node_modules[\\/]negaposi-analyzer-ja[\\/]/, + name: "lib-negaposi", + priority: 20, + }, + katex: { + test: /[\\/]node_modules[\\/]katex[\\/]/, + name: "lib-katex", + priority: 20, + }, + // その他のベンダー + defaultVendors: { + test: /[\\/]node_modules[\\/]/, + priority: -10, + reuseExistingChunk: true, + name: "vendors-misc", + }, + }, + }, }, - cache: false, + cache: true, ignoreWarnings: [ { module: /@ffmpeg/, diff --git a/application/e2e/test-dm-debug.js b/application/e2e/test-dm-debug.js new file mode 100644 index 0000000000..c5fce79613 --- /dev/null +++ b/application/e2e/test-dm-debug.js @@ -0,0 +1,11 @@ +const { chromium } = require('playwright'); +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage({ baseURL: 'http://localhost:3000' }); + page.on('console', msg => console.log('BROWSER CONSOLE:', msg.text())); + page.on('pageerror', error => console.log('BROWSER ERROR:', error)); + + await page.goto('/signin'); + // wait for it + await page.waitForTimeout(1000); +})(); diff --git a/application/pnpm-lock.yaml b/application/pnpm-lock.yaml index 510570f5c9..ea0394424e 100644 --- a/application/pnpm-lock.yaml +++ b/application/pnpm-lock.yaml @@ -21,15 +21,6 @@ importers: client: dependencies: - '@ffmpeg/core': - specifier: 0.12.10 - version: 0.12.10 - '@ffmpeg/ffmpeg': - specifier: 0.12.15 - version: 0.12.15 - '@imagemagick/magick-wasm': - specifier: 0.0.37 - version: 0.0.37 '@mlc-ai/web-llm': specifier: 0.2.80 version: 0.2.80 @@ -54,18 +45,15 @@ importers: core-js: specifier: 3.45.1 version: 3.45.1 + dayjs: + specifier: 1.11.20 + version: 1.11.20 encoding-japanese: specifier: 2.2.0 version: 2.2.0 fast-average-color: specifier: 9.5.0 version: 9.5.0 - gifler: - specifier: github:themadcreator/gifler#v0.3.0 - version: https://codeload.github.com/themadcreator/gifler/tar.gz/c3259b071c7782f85d4928a5f03d0b378ed003b5 - image-size: - specifier: 2.0.2 - version: 2.0.2 jquery: specifier: 3.7.1 version: 3.7.1 @@ -87,24 +75,15 @@ importers: lodash: specifier: 4.17.21 version: 4.17.21 - moment: - specifier: 2.30.1 - version: 2.30.1 negaposi-analyzer-ja: specifier: 1.0.1 version: 1.0.1 normalize.css: specifier: 8.0.1 version: 8.0.1 - omggif: - specifier: 1.0.10 - version: 1.0.10 pako: specifier: 2.1.0 version: 2.1.0 - piexifjs: - specifier: 1.0.6 - version: 1.0.6 react: specifier: 19.2.0 version: 19.2.0 @@ -160,6 +139,12 @@ importers: '@babel/preset-typescript': specifier: 7.27.1 version: 7.27.1(@babel/core@7.28.4) + '@tailwindcss/postcss': + specifier: ^4.2.2 + version: 4.2.2 + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.1(@types/node@22.18.8)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)) '@tsconfig/strictest': specifier: 2.0.8 version: 2.0.8 @@ -211,6 +196,9 @@ importers: babel-loader: specifier: 10.0.0 version: 10.0.0(@babel/core@7.28.4)(webpack@5.102.1) + compression-webpack-plugin: + specifier: 12.0.0 + version: 12.0.0(webpack@5.102.1) copy-webpack-plugin: specifier: 13.0.1 version: 13.0.1(webpack@5.102.1) @@ -238,6 +226,9 @@ importers: react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.2)(react@19.2.0) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 typescript: specifier: 5.9.3 version: 5.9.3 @@ -277,6 +268,9 @@ importers: body-parser: specifier: 2.2.0 version: 2.2.0 + compression: + specifier: 1.8.1 + version: 1.8.1 connect-history-api-fallback: specifier: 2.0.0 version: 2.0.0 @@ -286,9 +280,15 @@ importers: express-session: specifier: 1.18.2 version: 1.18.2 + ffmpeg-static: + specifier: 5.3.0 + version: 5.3.0 file-type: specifier: 21.1.1 version: 21.1.1 + fluent-ffmpeg: + specifier: 2.1.3 + version: 2.1.3 http-errors: specifier: 2.0.0 version: 2.0.0 @@ -301,6 +301,9 @@ importers: serve-static: specifier: 2.2.0 version: 2.2.0 + sharp: + specifier: 0.34.5 + version: 0.34.5 sqlite3: specifier: 5.1.7 version: 5.1.7 @@ -323,6 +326,9 @@ importers: '@types/body-parser': specifier: 1.19.6 version: 1.19.6 + '@types/compression': + specifier: 1.8.1 + version: 1.8.1 '@types/connect-history-api-fallback': specifier: 1.5.4 version: 1.5.4 @@ -332,6 +338,9 @@ importers: '@types/express-session': specifier: 1.18.2 version: 1.18.2 + '@types/fluent-ffmpeg': + specifier: 2.1.28 + version: 2.1.28 '@types/http-errors': specifier: 2.0.5 version: 2.0.5 @@ -350,6 +359,10 @@ importers: packages: + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1181,6 +1194,10 @@ packages: peerDependencies: postcss: ^8.4 + '@derhuerst/http-basic@8.2.4': + resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==} + engines: {node: '>=6.0.0'} + '@discoveryjs/json-ext@0.6.3': resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} engines: {node: '>=14.17.0'} @@ -1190,6 +1207,15 @@ packages: peerDependencies: react: '19' + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@esbuild/aix-ppc64@0.25.11': resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} @@ -1350,23 +1376,161 @@ packages: resolution: {integrity: sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} - '@ffmpeg/core@0.12.10': - resolution: {integrity: sha512-dzNplnn2Nxle2c2i2rrDhqcB19q9cglCkWnoMTDN9Q9l3PvdjZWd1HfSPjCNWc/p8Q3CT+Es9fWOR0UhAeYQZA==} - engines: {node: '>=16.x'} + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@ffmpeg/ffmpeg@0.12.15': - resolution: {integrity: sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==} - engines: {node: '>=18.x'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} - '@ffmpeg/types@0.12.4': - resolution: {integrity: sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==} - engines: {node: '>=16.x'} + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] - '@gar/promisify@1.1.3': - resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] - '@imagemagick/magick-wasm@0.0.37': - resolution: {integrity: sha512-tVs9hcWu9u7I3Jz/XvUYVvCEniuxAR+JjZEzI+yKtQmYAtNsLF1WjoH1HZGCKPumaB9jAHZlcf2RGT9+1l3nxQ==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1429,6 +1593,9 @@ packages: '@mlc-ai/web-llm@0.2.80': resolution: {integrity: sha512-Hwy1OCsK5cOU4nKr2wIJ2qA1g595PENtO5f2d9Wd/GgFsj5X04uxfaaJfqED8eFAJOpQpn/DirogdEY/yp5jQg==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@npmcli/fs@1.1.1': resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} @@ -1437,6 +1604,9 @@ packages: engines: {node: '>=10'} deprecated: This functionality has been moved to @npmcli/fs + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + '@oxfmt/binding-android-arm-eabi@0.36.0': resolution: {integrity: sha512-Z4yVHJWx/swHHjtr0dXrBZb6LxS+qNz1qdza222mWwPTUK4L790+5i3LTgjx3KYGBzcYpjaiZBw4vOx94dH7MQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1608,6 +1778,201 @@ packages: engines: {node: '>=18'} hasBin: true + '@rolldown/binding-android-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.10': + resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.2': + resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -1622,6 +1987,9 @@ packages: '@tsconfig/strictest@2.0.8': resolution: {integrity: sha512-XnQ7vNz5HRN0r88GYf1J9JJjqtZPiHt2woGJOo2dYqyHGGcd6OLGqSlBB6p1j9mpzja6Oe5BoPqWmeDx6X9rLw==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/bcrypt@6.0.0': resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} @@ -1637,6 +2005,9 @@ packages: '@types/common-tags@1.8.4': resolution: {integrity: sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg==} + '@types/compression@1.8.1': + resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==} + '@types/connect-history-api-fallback@1.5.4': resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} @@ -1679,6 +2050,9 @@ packages: '@types/express@5.0.3': resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/fluent-ffmpeg@2.1.28': + resolution: {integrity: sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -1721,6 +2095,9 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + '@types/node@10.17.60': + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + '@types/node@22.18.8': resolution: {integrity: sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==} @@ -1948,6 +2325,9 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} @@ -2085,6 +2465,9 @@ packages: caniuse-lite@1.0.30001749: resolution: {integrity: sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==} + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2159,6 +2542,12 @@ packages: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} + compression-webpack-plugin@12.0.0: + resolution: {integrity: sha512-LR4mS19Jqq41XfA3xVMLrtzVNzqJbUHdzPeLRfQoLiAS9s87f0021fDuU89xxVQFcB6d20ufBkv4j1rQ4OowHw==} + engines: {node: '>= 20.9.0'} + peerDependencies: + webpack: ^5.1.0 + compression@1.8.1: resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} @@ -2170,6 +2559,10 @@ packages: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} engines: {'0': node >= 0.8} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} @@ -2293,6 +2686,9 @@ packages: csv-parse@1.3.3: resolution: {integrity: sha512-byxnDBxM1AVF3YfmsK7Smop9/usNz7gAZYSo9eYp61TGcNXraJby1rAiLyJSt1/8Iho2qaxZOtZCOvQMXogPtg==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2426,6 +2822,10 @@ packages: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} @@ -2565,6 +2965,10 @@ packages: picomatch: optional: true + ffmpeg-static@5.3.0: + resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==} + engines: {node: '>=16'} + file-type@21.1.1: resolution: {integrity: sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==} engines: {node: '>=20'} @@ -2596,6 +3000,11 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -2667,10 +3076,6 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - gifler@https://codeload.github.com/themadcreator/gifler/tar.gz/c3259b071c7782f85d4928a5f03d0b378ed003b5: - resolution: {tarball: https://codeload.github.com/themadcreator/gifler/tar.gz/c3259b071c7782f85d4928a5f03d0b378ed003b5} - version: 0.3.0 - github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -2823,6 +3228,9 @@ packages: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} + http-response-object@3.0.2: + resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -2855,11 +3263,6 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - image-size@2.0.2: - resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} - engines: {node: '>=16.x'} - hasBin: true - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3075,6 +3478,80 @@ packages: launch-editor@2.11.1: resolution: {integrity: sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3120,6 +3597,9 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-fetch-happen@9.1.0: resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} engines: {node: '>= 10'} @@ -3497,9 +3977,6 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - omggif@1.0.10: - resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3568,6 +4045,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-cache-control@1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -3620,9 +4100,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - piexifjs@1.0.6: - resolution: {integrity: sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==} - pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -3848,6 +4325,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -3863,6 +4344,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -4093,6 +4578,11 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rolldown@1.0.0-rc.10: + resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -4181,6 +4671,10 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-javascript@7.0.4: + resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==} + engines: {node: '>=20.0.0'} + serve-index@1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} engines: {node: '>= 0.8.0'} @@ -4209,6 +4703,10 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4343,6 +4841,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -4565,6 +5066,49 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.0.1: + resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + watchpack@2.4.4: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} @@ -4637,6 +5181,10 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4691,6 +5239,8 @@ packages: snapshots: + '@alloc/quick-lru@5.2.0': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -5710,6 +6260,13 @@ snapshots: dependencies: postcss: 8.5.6 + '@derhuerst/http-basic@8.2.4': + dependencies: + caseless: 0.12.0 + concat-stream: 2.0.0 + http-response-object: 3.0.2 + parse-cache-control: 1.0.1 + '@discoveryjs/json-ext@0.6.3': {} '@dr.pogodin/react-helmet@3.0.4(react@19.2.0)': @@ -5717,6 +6274,22 @@ snapshots: '@babel/runtime': 7.28.4 react: 19.2.0 + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.11': optional: true @@ -5797,18 +6370,104 @@ snapshots: '@faker-js/faker@10.2.0': {} - '@ffmpeg/core@0.12.10': {} + '@gar/promisify@1.1.3': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true - '@ffmpeg/ffmpeg@0.12.15': + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@ffmpeg/types': 0.12.4 + '@emnapi/runtime': 1.9.1 + optional: true - '@ffmpeg/types@0.12.4': {} + '@img/sharp-win32-arm64@0.34.5': + optional: true - '@gar/promisify@1.1.3': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@imagemagick/magick-wasm@0.0.37': {} + '@img/sharp-win32-x64@0.34.5': + optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -5875,6 +6534,13 @@ snapshots: dependencies: loglevel: 1.9.2 + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@npmcli/fs@1.1.1': dependencies: '@gar/promisify': 1.1.3 @@ -5887,6 +6553,8 @@ snapshots: rimraf: 3.0.2 optional: true + '@oxc-project/types@0.120.0': {} + '@oxfmt/binding-android-arm-eabi@0.36.0': optional: true @@ -5972,6 +6640,131 @@ snapshots: dependencies: playwright: 1.50.1 + '@rolldown/binding-android-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.10': {} + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/postcss@4.2.2': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + postcss: 8.5.6 + tailwindcss: 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@8.0.1(@types/node@22.18.8)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.1(@types/node@22.18.8)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6) + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -5986,6 +6779,11 @@ snapshots: '@tsconfig/strictest@2.0.8': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/bcrypt@6.0.0': dependencies: '@types/node': 22.18.8 @@ -6003,6 +6801,11 @@ snapshots: '@types/common-tags@1.8.4': {} + '@types/compression@1.8.1': + dependencies: + '@types/express': 5.0.3 + '@types/node': 22.18.8 + '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.1.0 @@ -6067,6 +6870,10 @@ snapshots: '@types/express-serve-static-core': 5.1.0 '@types/serve-static': 1.15.9 + '@types/fluent-ffmpeg@2.1.28': + dependencies: + '@types/node': 22.18.8 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -6107,6 +6914,8 @@ snapshots: dependencies: '@types/node': 22.18.8 + '@types/node@10.17.60': {} + '@types/node@22.18.8': dependencies: undici-types: 6.21.0 @@ -6302,7 +7111,6 @@ snapshots: debug: 4.4.3 transitivePeerDependencies: - supports-color - optional: true agentkeepalive@4.6.0: dependencies: @@ -6353,6 +7161,8 @@ snapshots: array-flatten@1.1.1: {} + async@0.2.10: {} + async@2.6.4: dependencies: lodash: 4.17.21 @@ -6551,6 +7361,8 @@ snapshots: caniuse-lite@1.0.30001749: {} + caseless@0.12.0: {} + ccount@2.0.1: {} character-entities-html4@2.1.0: {} @@ -6613,6 +7425,12 @@ snapshots: dependencies: mime-db: 1.54.0 + compression-webpack-plugin@12.0.0(webpack@5.102.1): + dependencies: + schema-utils: 4.3.3 + serialize-javascript: 7.0.4 + webpack: 5.102.1(webpack-cli@6.0.1) + compression@1.8.1: dependencies: bytes: 3.1.2 @@ -6635,6 +7453,13 @@ snapshots: readable-stream: 2.3.8 typedarray: 0.0.6 + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + connect-history-api-fallback@2.0.0: {} console-control-strings@1.1.0: @@ -6743,6 +7568,8 @@ snapshots: csv-parse@1.3.3: {} + dayjs@1.11.20: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -6856,6 +7683,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + entities@2.2.0: {} entities@6.0.1: {} @@ -7046,6 +7878,15 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + ffmpeg-static@5.3.0: + dependencies: + '@derhuerst/http-basic': 8.2.4 + env-paths: 2.2.1 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + transitivePeerDependencies: + - supports-color + file-type@21.1.1: dependencies: '@tokenizer/inflate': 0.4.1 @@ -7096,6 +7937,11 @@ snapshots: flat@5.0.2: {} + fluent-ffmpeg@2.1.3: + dependencies: + async: 0.2.10 + which: 1.3.1 + follow-redirects@1.15.11: {} format@0.2.2: {} @@ -7161,11 +8007,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - gifler@https://codeload.github.com/themadcreator/gifler/tar.gz/c3259b071c7782f85d4928a5f03d0b378ed003b5: - dependencies: - bluebird: 3.7.2 - omggif: 1.0.10 - github-from-package@0.0.0: {} glob-parent@5.1.2: @@ -7386,13 +8227,16 @@ snapshots: transitivePeerDependencies: - debug + http-response-object@3.0.2: + dependencies: + '@types/node': 10.17.60 + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 debug: 4.4.3 transitivePeerDependencies: - supports-color - optional: true humanize-ms@1.2.1: dependencies: @@ -7419,8 +8263,6 @@ snapshots: ieee754@1.2.1: {} - image-size@2.0.2: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7593,6 +8435,55 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.3 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lines-and-columns@1.2.4: {} loader-runner@4.3.1: {} @@ -7635,6 +8526,10 @@ snapshots: yallist: 4.0.0 optional: true + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-fetch-happen@9.1.0: dependencies: agentkeepalive: 4.6.0 @@ -8258,8 +9153,6 @@ snapshots: obuf@1.1.2: {} - omggif@1.0.10: {} - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -8352,6 +9245,8 @@ snapshots: dependencies: callsites: 3.1.0 + parse-cache-control@1.0.1: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -8401,8 +9296,6 @@ snapshots: picomatch@4.0.3: {} - piexifjs@1.0.6: {} - pify@2.3.0: {} pkg-dir@4.2.0: @@ -8685,6 +9578,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -8709,6 +9608,8 @@ snapshots: process-nextick-args@2.0.1: {} + progress@2.0.3: {} + promise-inflight@1.0.1: optional: true @@ -9000,6 +9901,27 @@ snapshots: glob: 7.2.3 optional: true + rolldown@1.0.0-rc.10: + dependencies: + '@oxc-project/types': 0.120.0 + '@rolldown/pluginutils': 1.0.0-rc.10 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-x64': 1.0.0-rc.10 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 + router@2.2.0: dependencies: debug: 4.4.3 @@ -9101,6 +10023,8 @@ snapshots: dependencies: randombytes: 2.1.0 + serialize-javascript@7.0.4: {} + serve-index@1.9.1: dependencies: accepts: 1.3.8 @@ -9144,6 +10068,37 @@ snapshots: dependencies: kind-of: 6.0.3 + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -9320,6 +10275,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwindcss@4.2.2: {} + tapable@2.3.0: {} tar-fs@2.1.4: @@ -9542,6 +10499,20 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite@8.0.1(@types/node@22.18.8)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.3 + postcss: 8.5.8 + rolldown: 1.0.0-rc.10 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.18.8 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.44.0 + tsx: 4.20.6 + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 @@ -9672,6 +10643,10 @@ snapshots: websocket-extensions@0.1.4: {} + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/application/public/images/029b4b75-bbcc-4aa5-8bd7-e4bb12a33cd3.jpg b/application/public/images/029b4b75-bbcc-4aa5-8bd7-e4bb12a33cd3.jpg deleted file mode 100644 index 8ce999154f..0000000000 Binary files a/application/public/images/029b4b75-bbcc-4aa5-8bd7-e4bb12a33cd3.jpg and /dev/null differ diff --git a/application/public/images/029b4b75-bbcc-4aa5-8bd7-e4bb12a33cd3.webp b/application/public/images/029b4b75-bbcc-4aa5-8bd7-e4bb12a33cd3.webp new file mode 100644 index 0000000000..ef5ec4d18d Binary files /dev/null and b/application/public/images/029b4b75-bbcc-4aa5-8bd7-e4bb12a33cd3.webp differ diff --git a/application/public/images/078c4d42-12e3-4c1d-823c-9ba552f6b066.jpg b/application/public/images/078c4d42-12e3-4c1d-823c-9ba552f6b066.jpg deleted file mode 100644 index c2fa15e15d..0000000000 Binary files a/application/public/images/078c4d42-12e3-4c1d-823c-9ba552f6b066.jpg and /dev/null differ diff --git a/application/public/images/078c4d42-12e3-4c1d-823c-9ba552f6b066.webp b/application/public/images/078c4d42-12e3-4c1d-823c-9ba552f6b066.webp new file mode 100644 index 0000000000..c83532eab0 Binary files /dev/null and b/application/public/images/078c4d42-12e3-4c1d-823c-9ba552f6b066.webp differ diff --git a/application/public/images/083258be-3e8c-4537-ac9c-fd5fd9cd943b.jpg b/application/public/images/083258be-3e8c-4537-ac9c-fd5fd9cd943b.jpg deleted file mode 100644 index c0b4aab6e3..0000000000 Binary files a/application/public/images/083258be-3e8c-4537-ac9c-fd5fd9cd943b.jpg and /dev/null differ diff --git a/application/public/images/083258be-3e8c-4537-ac9c-fd5fd9cd943b.webp b/application/public/images/083258be-3e8c-4537-ac9c-fd5fd9cd943b.webp new file mode 100644 index 0000000000..821995a1f1 Binary files /dev/null and b/application/public/images/083258be-3e8c-4537-ac9c-fd5fd9cd943b.webp differ diff --git a/application/public/images/18358ca6-0aa7-4592-9926-1ec522b9aacb.jpg b/application/public/images/18358ca6-0aa7-4592-9926-1ec522b9aacb.jpg deleted file mode 100644 index 252ec44ae1..0000000000 Binary files a/application/public/images/18358ca6-0aa7-4592-9926-1ec522b9aacb.jpg and /dev/null differ diff --git a/application/public/images/18358ca6-0aa7-4592-9926-1ec522b9aacb.webp b/application/public/images/18358ca6-0aa7-4592-9926-1ec522b9aacb.webp new file mode 100644 index 0000000000..ca79d85669 Binary files /dev/null and b/application/public/images/18358ca6-0aa7-4592-9926-1ec522b9aacb.webp differ diff --git a/application/public/images/19b3516f-ccfc-4d76-a45c-fc2aade43afe.jpg b/application/public/images/19b3516f-ccfc-4d76-a45c-fc2aade43afe.jpg deleted file mode 100644 index deb08a9733..0000000000 Binary files a/application/public/images/19b3516f-ccfc-4d76-a45c-fc2aade43afe.jpg and /dev/null differ diff --git a/application/public/images/19b3516f-ccfc-4d76-a45c-fc2aade43afe.webp b/application/public/images/19b3516f-ccfc-4d76-a45c-fc2aade43afe.webp new file mode 100644 index 0000000000..155c152dba Binary files /dev/null and b/application/public/images/19b3516f-ccfc-4d76-a45c-fc2aade43afe.webp differ diff --git a/application/public/images/26117ade-f330-46a2-8c48-767b6f472613.jpg b/application/public/images/26117ade-f330-46a2-8c48-767b6f472613.jpg deleted file mode 100644 index 2a858b13ef..0000000000 Binary files a/application/public/images/26117ade-f330-46a2-8c48-767b6f472613.jpg and /dev/null differ diff --git a/application/public/images/26117ade-f330-46a2-8c48-767b6f472613.webp b/application/public/images/26117ade-f330-46a2-8c48-767b6f472613.webp new file mode 100644 index 0000000000..389ba25ebd Binary files /dev/null and b/application/public/images/26117ade-f330-46a2-8c48-767b6f472613.webp differ diff --git a/application/public/images/3a5915dc-6ef0-4c66-ad4b-bba9c724cfbc.jpg b/application/public/images/3a5915dc-6ef0-4c66-ad4b-bba9c724cfbc.jpg deleted file mode 100644 index 69f567ba06..0000000000 Binary files a/application/public/images/3a5915dc-6ef0-4c66-ad4b-bba9c724cfbc.jpg and /dev/null differ diff --git a/application/public/images/3a5915dc-6ef0-4c66-ad4b-bba9c724cfbc.webp b/application/public/images/3a5915dc-6ef0-4c66-ad4b-bba9c724cfbc.webp new file mode 100644 index 0000000000..cd3a0ea1a3 Binary files /dev/null and b/application/public/images/3a5915dc-6ef0-4c66-ad4b-bba9c724cfbc.webp differ diff --git a/application/public/images/4685b32a-43d2-4478-bb79-2cdb56f8ecf0.jpg b/application/public/images/4685b32a-43d2-4478-bb79-2cdb56f8ecf0.jpg deleted file mode 100644 index fc5345e622..0000000000 Binary files a/application/public/images/4685b32a-43d2-4478-bb79-2cdb56f8ecf0.jpg and /dev/null differ diff --git a/application/public/images/4685b32a-43d2-4478-bb79-2cdb56f8ecf0.webp b/application/public/images/4685b32a-43d2-4478-bb79-2cdb56f8ecf0.webp new file mode 100644 index 0000000000..5092f474f0 Binary files /dev/null and b/application/public/images/4685b32a-43d2-4478-bb79-2cdb56f8ecf0.webp differ diff --git a/application/public/images/49b8af97-9536-4a23-86f6-21526ff2715b.jpg b/application/public/images/49b8af97-9536-4a23-86f6-21526ff2715b.jpg deleted file mode 100644 index 19cf6b1607..0000000000 Binary files a/application/public/images/49b8af97-9536-4a23-86f6-21526ff2715b.jpg and /dev/null differ diff --git a/application/public/images/49b8af97-9536-4a23-86f6-21526ff2715b.webp b/application/public/images/49b8af97-9536-4a23-86f6-21526ff2715b.webp new file mode 100644 index 0000000000..761ad1e3eb Binary files /dev/null and b/application/public/images/49b8af97-9536-4a23-86f6-21526ff2715b.webp differ diff --git a/application/public/images/5be3fce7-0365-4aa3-a1b6-cdeb553e8dfb.jpg b/application/public/images/5be3fce7-0365-4aa3-a1b6-cdeb553e8dfb.jpg deleted file mode 100644 index 9b2d6c4e1c..0000000000 Binary files a/application/public/images/5be3fce7-0365-4aa3-a1b6-cdeb553e8dfb.jpg and /dev/null differ diff --git a/application/public/images/5be3fce7-0365-4aa3-a1b6-cdeb553e8dfb.webp b/application/public/images/5be3fce7-0365-4aa3-a1b6-cdeb553e8dfb.webp new file mode 100644 index 0000000000..35de7f3b50 Binary files /dev/null and b/application/public/images/5be3fce7-0365-4aa3-a1b6-cdeb553e8dfb.webp differ diff --git a/application/public/images/5e7212da-6b4c-4eb2-b828-b0bc35bfbc1c.jpg b/application/public/images/5e7212da-6b4c-4eb2-b828-b0bc35bfbc1c.jpg deleted file mode 100644 index 4d6a13d7e0..0000000000 Binary files a/application/public/images/5e7212da-6b4c-4eb2-b828-b0bc35bfbc1c.jpg and /dev/null differ diff --git a/application/public/images/5e7212da-6b4c-4eb2-b828-b0bc35bfbc1c.webp b/application/public/images/5e7212da-6b4c-4eb2-b828-b0bc35bfbc1c.webp new file mode 100644 index 0000000000..79e52ae272 Binary files /dev/null and b/application/public/images/5e7212da-6b4c-4eb2-b828-b0bc35bfbc1c.webp differ diff --git a/application/public/images/6d532fa5-daff-4876-a26f-b5c8669d1176.jpg b/application/public/images/6d532fa5-daff-4876-a26f-b5c8669d1176.jpg deleted file mode 100644 index 6db63536c4..0000000000 Binary files a/application/public/images/6d532fa5-daff-4876-a26f-b5c8669d1176.jpg and /dev/null differ diff --git a/application/public/images/6d532fa5-daff-4876-a26f-b5c8669d1176.webp b/application/public/images/6d532fa5-daff-4876-a26f-b5c8669d1176.webp new file mode 100644 index 0000000000..20be20b04b Binary files /dev/null and b/application/public/images/6d532fa5-daff-4876-a26f-b5c8669d1176.webp differ diff --git a/application/public/images/737f764e-f495-4104-b6d6-8434681718d5.jpg b/application/public/images/737f764e-f495-4104-b6d6-8434681718d5.jpg deleted file mode 100644 index 12e5a0742c..0000000000 Binary files a/application/public/images/737f764e-f495-4104-b6d6-8434681718d5.jpg and /dev/null differ diff --git a/application/public/images/737f764e-f495-4104-b6d6-8434681718d5.webp b/application/public/images/737f764e-f495-4104-b6d6-8434681718d5.webp new file mode 100644 index 0000000000..f6813049e4 Binary files /dev/null and b/application/public/images/737f764e-f495-4104-b6d6-8434681718d5.webp differ diff --git a/application/public/images/77284ba9-06c0-4c66-92a9-4d2513336e24.jpg b/application/public/images/77284ba9-06c0-4c66-92a9-4d2513336e24.jpg deleted file mode 100644 index 9182e213f2..0000000000 Binary files a/application/public/images/77284ba9-06c0-4c66-92a9-4d2513336e24.jpg and /dev/null differ diff --git a/application/public/images/77284ba9-06c0-4c66-92a9-4d2513336e24.webp b/application/public/images/77284ba9-06c0-4c66-92a9-4d2513336e24.webp new file mode 100644 index 0000000000..ab8253d6cb Binary files /dev/null and b/application/public/images/77284ba9-06c0-4c66-92a9-4d2513336e24.webp differ diff --git a/application/public/images/824ddc65-8afc-4cd5-8176-1a8053758e72.jpg b/application/public/images/824ddc65-8afc-4cd5-8176-1a8053758e72.jpg deleted file mode 100644 index 9dee0afd36..0000000000 Binary files a/application/public/images/824ddc65-8afc-4cd5-8176-1a8053758e72.jpg and /dev/null differ diff --git a/application/public/images/824ddc65-8afc-4cd5-8176-1a8053758e72.webp b/application/public/images/824ddc65-8afc-4cd5-8176-1a8053758e72.webp new file mode 100644 index 0000000000..534b34c731 Binary files /dev/null and b/application/public/images/824ddc65-8afc-4cd5-8176-1a8053758e72.webp differ diff --git a/application/public/images/85946f86-c0bd-4d6b-83b7-94eb32dcbcf4.jpg b/application/public/images/85946f86-c0bd-4d6b-83b7-94eb32dcbcf4.jpg deleted file mode 100644 index 74e42ea800..0000000000 Binary files a/application/public/images/85946f86-c0bd-4d6b-83b7-94eb32dcbcf4.jpg and /dev/null differ diff --git a/application/public/images/85946f86-c0bd-4d6b-83b7-94eb32dcbcf4.webp b/application/public/images/85946f86-c0bd-4d6b-83b7-94eb32dcbcf4.webp new file mode 100644 index 0000000000..b739d733dc Binary files /dev/null and b/application/public/images/85946f86-c0bd-4d6b-83b7-94eb32dcbcf4.webp differ diff --git a/application/public/images/9bb2f5c0-0f7c-4b9d-8e6a-aa87ebe7efc5.jpg b/application/public/images/9bb2f5c0-0f7c-4b9d-8e6a-aa87ebe7efc5.jpg deleted file mode 100644 index 7ffc62b872..0000000000 Binary files a/application/public/images/9bb2f5c0-0f7c-4b9d-8e6a-aa87ebe7efc5.jpg and /dev/null differ diff --git a/application/public/images/9bb2f5c0-0f7c-4b9d-8e6a-aa87ebe7efc5.webp b/application/public/images/9bb2f5c0-0f7c-4b9d-8e6a-aa87ebe7efc5.webp new file mode 100644 index 0000000000..f2761b34e4 Binary files /dev/null and b/application/public/images/9bb2f5c0-0f7c-4b9d-8e6a-aa87ebe7efc5.webp differ diff --git a/application/public/images/9c8c5258-f659-4890-8b7f-0485097d957b.jpg b/application/public/images/9c8c5258-f659-4890-8b7f-0485097d957b.jpg deleted file mode 100644 index 476900f697..0000000000 Binary files a/application/public/images/9c8c5258-f659-4890-8b7f-0485097d957b.jpg and /dev/null differ diff --git a/application/public/images/9c8c5258-f659-4890-8b7f-0485097d957b.webp b/application/public/images/9c8c5258-f659-4890-8b7f-0485097d957b.webp new file mode 100644 index 0000000000..37c3be263f Binary files /dev/null and b/application/public/images/9c8c5258-f659-4890-8b7f-0485097d957b.webp differ diff --git a/application/public/images/a21c9b2c-9fc7-4d3c-8488-a465150f7b1c.jpg b/application/public/images/a21c9b2c-9fc7-4d3c-8488-a465150f7b1c.jpg deleted file mode 100644 index 607c1dcaad..0000000000 Binary files a/application/public/images/a21c9b2c-9fc7-4d3c-8488-a465150f7b1c.jpg and /dev/null differ diff --git a/application/public/images/a21c9b2c-9fc7-4d3c-8488-a465150f7b1c.webp b/application/public/images/a21c9b2c-9fc7-4d3c-8488-a465150f7b1c.webp new file mode 100644 index 0000000000..9c10f16481 Binary files /dev/null and b/application/public/images/a21c9b2c-9fc7-4d3c-8488-a465150f7b1c.webp differ diff --git a/application/public/images/af15685e-2e43-4453-bc8f-55e386bd5963.jpg b/application/public/images/af15685e-2e43-4453-bc8f-55e386bd5963.jpg deleted file mode 100644 index 9d733cf531..0000000000 Binary files a/application/public/images/af15685e-2e43-4453-bc8f-55e386bd5963.jpg and /dev/null differ diff --git a/application/public/images/af15685e-2e43-4453-bc8f-55e386bd5963.webp b/application/public/images/af15685e-2e43-4453-bc8f-55e386bd5963.webp new file mode 100644 index 0000000000..3379d7758c Binary files /dev/null and b/application/public/images/af15685e-2e43-4453-bc8f-55e386bd5963.webp differ diff --git a/application/public/images/af15f1d0-8350-46f4-9652-e02eb31469da.jpg b/application/public/images/af15f1d0-8350-46f4-9652-e02eb31469da.jpg deleted file mode 100644 index 2afefff833..0000000000 Binary files a/application/public/images/af15f1d0-8350-46f4-9652-e02eb31469da.jpg and /dev/null differ diff --git a/application/public/images/af15f1d0-8350-46f4-9652-e02eb31469da.webp b/application/public/images/af15f1d0-8350-46f4-9652-e02eb31469da.webp new file mode 100644 index 0000000000..6edf144b3c Binary files /dev/null and b/application/public/images/af15f1d0-8350-46f4-9652-e02eb31469da.webp differ diff --git a/application/public/images/c095fdc4-eb78-4ae1-9efa-4b8e360177ce.jpg b/application/public/images/c095fdc4-eb78-4ae1-9efa-4b8e360177ce.jpg deleted file mode 100644 index dd24051c18..0000000000 Binary files a/application/public/images/c095fdc4-eb78-4ae1-9efa-4b8e360177ce.jpg and /dev/null differ diff --git a/application/public/images/c095fdc4-eb78-4ae1-9efa-4b8e360177ce.webp b/application/public/images/c095fdc4-eb78-4ae1-9efa-4b8e360177ce.webp new file mode 100644 index 0000000000..fde4964ce0 Binary files /dev/null and b/application/public/images/c095fdc4-eb78-4ae1-9efa-4b8e360177ce.webp differ diff --git a/application/public/images/da2bfcde-14fd-473c-ae79-572d95152b61.jpg b/application/public/images/da2bfcde-14fd-473c-ae79-572d95152b61.jpg deleted file mode 100644 index 7e5fcd034d..0000000000 Binary files a/application/public/images/da2bfcde-14fd-473c-ae79-572d95152b61.jpg and /dev/null differ diff --git a/application/public/images/da2bfcde-14fd-473c-ae79-572d95152b61.webp b/application/public/images/da2bfcde-14fd-473c-ae79-572d95152b61.webp new file mode 100644 index 0000000000..f660ff6ba0 Binary files /dev/null and b/application/public/images/da2bfcde-14fd-473c-ae79-572d95152b61.webp differ diff --git a/application/public/images/ddc7053e-0f2f-49b1-9c07-e1060e2fa4aa.jpg b/application/public/images/ddc7053e-0f2f-49b1-9c07-e1060e2fa4aa.jpg deleted file mode 100644 index 09b3277c02..0000000000 Binary files a/application/public/images/ddc7053e-0f2f-49b1-9c07-e1060e2fa4aa.jpg and /dev/null differ diff --git a/application/public/images/ddc7053e-0f2f-49b1-9c07-e1060e2fa4aa.webp b/application/public/images/ddc7053e-0f2f-49b1-9c07-e1060e2fa4aa.webp new file mode 100644 index 0000000000..2805d87e15 Binary files /dev/null and b/application/public/images/ddc7053e-0f2f-49b1-9c07-e1060e2fa4aa.webp differ diff --git a/application/public/images/e40ff559-d0d3-4eb0-8792-21cb171b815c.jpg b/application/public/images/e40ff559-d0d3-4eb0-8792-21cb171b815c.jpg deleted file mode 100644 index e2eb1f9420..0000000000 Binary files a/application/public/images/e40ff559-d0d3-4eb0-8792-21cb171b815c.jpg and /dev/null differ diff --git a/application/public/images/e40ff559-d0d3-4eb0-8792-21cb171b815c.webp b/application/public/images/e40ff559-d0d3-4eb0-8792-21cb171b815c.webp new file mode 100644 index 0000000000..1580d324a3 Binary files /dev/null and b/application/public/images/e40ff559-d0d3-4eb0-8792-21cb171b815c.webp differ diff --git a/application/public/images/eb487309-79ed-40d0-9fee-382ed8486b70.jpg b/application/public/images/eb487309-79ed-40d0-9fee-382ed8486b70.jpg deleted file mode 100644 index 4527761130..0000000000 Binary files a/application/public/images/eb487309-79ed-40d0-9fee-382ed8486b70.jpg and /dev/null differ diff --git a/application/public/images/eb487309-79ed-40d0-9fee-382ed8486b70.webp b/application/public/images/eb487309-79ed-40d0-9fee-382ed8486b70.webp new file mode 100644 index 0000000000..dfb91a5db0 Binary files /dev/null and b/application/public/images/eb487309-79ed-40d0-9fee-382ed8486b70.webp differ diff --git a/application/public/images/ec098438-5fac-44a8-bd5a-84c575a32790.jpg b/application/public/images/ec098438-5fac-44a8-bd5a-84c575a32790.jpg deleted file mode 100644 index 7885818d74..0000000000 Binary files a/application/public/images/ec098438-5fac-44a8-bd5a-84c575a32790.jpg and /dev/null differ diff --git a/application/public/images/ec098438-5fac-44a8-bd5a-84c575a32790.webp b/application/public/images/ec098438-5fac-44a8-bd5a-84c575a32790.webp new file mode 100644 index 0000000000..758401e207 Binary files /dev/null and b/application/public/images/ec098438-5fac-44a8-bd5a-84c575a32790.webp differ diff --git a/application/public/images/ee6d7cb7-3c05-4bde-92e5-aebef3785904.jpg b/application/public/images/ee6d7cb7-3c05-4bde-92e5-aebef3785904.jpg deleted file mode 100644 index 0db48838ad..0000000000 Binary files a/application/public/images/ee6d7cb7-3c05-4bde-92e5-aebef3785904.jpg and /dev/null differ diff --git a/application/public/images/ee6d7cb7-3c05-4bde-92e5-aebef3785904.webp b/application/public/images/ee6d7cb7-3c05-4bde-92e5-aebef3785904.webp new file mode 100644 index 0000000000..6d8372e4e4 Binary files /dev/null and b/application/public/images/ee6d7cb7-3c05-4bde-92e5-aebef3785904.webp differ diff --git a/application/public/images/f046441d-b837-4dc7-b0ae-5cf2604eab4c.jpg b/application/public/images/f046441d-b837-4dc7-b0ae-5cf2604eab4c.jpg deleted file mode 100644 index b6677c524f..0000000000 Binary files a/application/public/images/f046441d-b837-4dc7-b0ae-5cf2604eab4c.jpg and /dev/null differ diff --git a/application/public/images/f046441d-b837-4dc7-b0ae-5cf2604eab4c.webp b/application/public/images/f046441d-b837-4dc7-b0ae-5cf2604eab4c.webp new file mode 100644 index 0000000000..707c9911e1 Binary files /dev/null and b/application/public/images/f046441d-b837-4dc7-b0ae-5cf2604eab4c.webp differ diff --git a/application/public/images/f478a152-02f8-46a3-91ce-d1d7944d303a.jpg b/application/public/images/f478a152-02f8-46a3-91ce-d1d7944d303a.jpg deleted file mode 100644 index a2aa172d31..0000000000 Binary files a/application/public/images/f478a152-02f8-46a3-91ce-d1d7944d303a.jpg and /dev/null differ diff --git a/application/public/images/f478a152-02f8-46a3-91ce-d1d7944d303a.webp b/application/public/images/f478a152-02f8-46a3-91ce-d1d7944d303a.webp new file mode 100644 index 0000000000..404b5723b5 Binary files /dev/null and b/application/public/images/f478a152-02f8-46a3-91ce-d1d7944d303a.webp differ diff --git a/application/public/images/profiles/09d52cbb-28a2-4413-b220-1f8c9e80a440.jpg b/application/public/images/profiles/09d52cbb-28a2-4413-b220-1f8c9e80a440.jpg deleted file mode 100644 index 233fecd827..0000000000 Binary files a/application/public/images/profiles/09d52cbb-28a2-4413-b220-1f8c9e80a440.jpg and /dev/null differ diff --git a/application/public/images/profiles/09d52cbb-28a2-4413-b220-1f8c9e80a440.webp b/application/public/images/profiles/09d52cbb-28a2-4413-b220-1f8c9e80a440.webp new file mode 100644 index 0000000000..bdc70cc974 Binary files /dev/null and b/application/public/images/profiles/09d52cbb-28a2-4413-b220-1f8c9e80a440.webp differ diff --git a/application/public/images/profiles/0aba06a6-1b56-4ebd-8218-951aaba173af.jpg b/application/public/images/profiles/0aba06a6-1b56-4ebd-8218-951aaba173af.jpg deleted file mode 100644 index 83534c3edf..0000000000 Binary files a/application/public/images/profiles/0aba06a6-1b56-4ebd-8218-951aaba173af.jpg and /dev/null differ diff --git a/application/public/images/profiles/0aba06a6-1b56-4ebd-8218-951aaba173af.webp b/application/public/images/profiles/0aba06a6-1b56-4ebd-8218-951aaba173af.webp new file mode 100644 index 0000000000..769eafd51b Binary files /dev/null and b/application/public/images/profiles/0aba06a6-1b56-4ebd-8218-951aaba173af.webp differ diff --git a/application/public/images/profiles/0ccabdd2-4601-4c2f-88f5-1848b06ef035.jpg b/application/public/images/profiles/0ccabdd2-4601-4c2f-88f5-1848b06ef035.jpg deleted file mode 100644 index cd0ad8af5d..0000000000 Binary files a/application/public/images/profiles/0ccabdd2-4601-4c2f-88f5-1848b06ef035.jpg and /dev/null differ diff --git a/application/public/images/profiles/0ccabdd2-4601-4c2f-88f5-1848b06ef035.webp b/application/public/images/profiles/0ccabdd2-4601-4c2f-88f5-1848b06ef035.webp new file mode 100644 index 0000000000..1cc4606282 Binary files /dev/null and b/application/public/images/profiles/0ccabdd2-4601-4c2f-88f5-1848b06ef035.webp differ diff --git a/application/public/images/profiles/25dde9ae-1dd3-4d23-bfd3-90a94b59816c.jpg b/application/public/images/profiles/25dde9ae-1dd3-4d23-bfd3-90a94b59816c.jpg deleted file mode 100644 index ac262dc1a2..0000000000 Binary files a/application/public/images/profiles/25dde9ae-1dd3-4d23-bfd3-90a94b59816c.jpg and /dev/null differ diff --git a/application/public/images/profiles/25dde9ae-1dd3-4d23-bfd3-90a94b59816c.webp b/application/public/images/profiles/25dde9ae-1dd3-4d23-bfd3-90a94b59816c.webp new file mode 100644 index 0000000000..acb8e28534 Binary files /dev/null and b/application/public/images/profiles/25dde9ae-1dd3-4d23-bfd3-90a94b59816c.webp differ diff --git a/application/public/images/profiles/2d5ef610-a9e5-426c-9eeb-916a9b753d55.jpg b/application/public/images/profiles/2d5ef610-a9e5-426c-9eeb-916a9b753d55.jpg deleted file mode 100644 index 2790b99200..0000000000 Binary files a/application/public/images/profiles/2d5ef610-a9e5-426c-9eeb-916a9b753d55.jpg and /dev/null differ diff --git a/application/public/images/profiles/2d5ef610-a9e5-426c-9eeb-916a9b753d55.webp b/application/public/images/profiles/2d5ef610-a9e5-426c-9eeb-916a9b753d55.webp new file mode 100644 index 0000000000..95def96912 Binary files /dev/null and b/application/public/images/profiles/2d5ef610-a9e5-426c-9eeb-916a9b753d55.webp differ diff --git a/application/public/images/profiles/36079dc7-dd73-4073-aceb-7d5c1f0dab4e.jpg b/application/public/images/profiles/36079dc7-dd73-4073-aceb-7d5c1f0dab4e.jpg deleted file mode 100644 index 46b554eb6f..0000000000 Binary files a/application/public/images/profiles/36079dc7-dd73-4073-aceb-7d5c1f0dab4e.jpg and /dev/null differ diff --git a/application/public/images/profiles/36079dc7-dd73-4073-aceb-7d5c1f0dab4e.webp b/application/public/images/profiles/36079dc7-dd73-4073-aceb-7d5c1f0dab4e.webp new file mode 100644 index 0000000000..9c52823080 Binary files /dev/null and b/application/public/images/profiles/36079dc7-dd73-4073-aceb-7d5c1f0dab4e.webp differ diff --git a/application/public/images/profiles/37812068-9ef8-4429-b219-8d9c9b91c89c.jpg b/application/public/images/profiles/37812068-9ef8-4429-b219-8d9c9b91c89c.jpg deleted file mode 100644 index 5b48c21675..0000000000 Binary files a/application/public/images/profiles/37812068-9ef8-4429-b219-8d9c9b91c89c.jpg and /dev/null differ diff --git a/application/public/images/profiles/37812068-9ef8-4429-b219-8d9c9b91c89c.webp b/application/public/images/profiles/37812068-9ef8-4429-b219-8d9c9b91c89c.webp new file mode 100644 index 0000000000..a7fc9e858c Binary files /dev/null and b/application/public/images/profiles/37812068-9ef8-4429-b219-8d9c9b91c89c.webp differ diff --git a/application/public/images/profiles/396fe4ce-aa36-4d96-b54e-6db40bae2eed.jpg b/application/public/images/profiles/396fe4ce-aa36-4d96-b54e-6db40bae2eed.jpg deleted file mode 100644 index 3183b7c1d5..0000000000 Binary files a/application/public/images/profiles/396fe4ce-aa36-4d96-b54e-6db40bae2eed.jpg and /dev/null differ diff --git a/application/public/images/profiles/396fe4ce-aa36-4d96-b54e-6db40bae2eed.webp b/application/public/images/profiles/396fe4ce-aa36-4d96-b54e-6db40bae2eed.webp new file mode 100644 index 0000000000..2def2876f3 Binary files /dev/null and b/application/public/images/profiles/396fe4ce-aa36-4d96-b54e-6db40bae2eed.webp differ diff --git a/application/public/images/profiles/3d43c4e2-6eaf-4bb9-bff3-cb955440c891.jpg b/application/public/images/profiles/3d43c4e2-6eaf-4bb9-bff3-cb955440c891.jpg deleted file mode 100644 index 56fbbddfa2..0000000000 Binary files a/application/public/images/profiles/3d43c4e2-6eaf-4bb9-bff3-cb955440c891.jpg and /dev/null differ diff --git a/application/public/images/profiles/3d43c4e2-6eaf-4bb9-bff3-cb955440c891.webp b/application/public/images/profiles/3d43c4e2-6eaf-4bb9-bff3-cb955440c891.webp new file mode 100644 index 0000000000..fa0e3e6794 Binary files /dev/null and b/application/public/images/profiles/3d43c4e2-6eaf-4bb9-bff3-cb955440c891.webp differ diff --git a/application/public/images/profiles/3dd3640a-5f9e-40d0-8daf-bfdb473b129e.jpg b/application/public/images/profiles/3dd3640a-5f9e-40d0-8daf-bfdb473b129e.jpg deleted file mode 100644 index 16b5ea9144..0000000000 Binary files a/application/public/images/profiles/3dd3640a-5f9e-40d0-8daf-bfdb473b129e.jpg and /dev/null differ diff --git a/application/public/images/profiles/3dd3640a-5f9e-40d0-8daf-bfdb473b129e.webp b/application/public/images/profiles/3dd3640a-5f9e-40d0-8daf-bfdb473b129e.webp new file mode 100644 index 0000000000..a99efde92d Binary files /dev/null and b/application/public/images/profiles/3dd3640a-5f9e-40d0-8daf-bfdb473b129e.webp differ diff --git a/application/public/images/profiles/51874337-0b42-4b03-8e3d-fbd4960a9947.jpg b/application/public/images/profiles/51874337-0b42-4b03-8e3d-fbd4960a9947.jpg deleted file mode 100644 index 0c993f38e7..0000000000 Binary files a/application/public/images/profiles/51874337-0b42-4b03-8e3d-fbd4960a9947.jpg and /dev/null differ diff --git a/application/public/images/profiles/51874337-0b42-4b03-8e3d-fbd4960a9947.webp b/application/public/images/profiles/51874337-0b42-4b03-8e3d-fbd4960a9947.webp new file mode 100644 index 0000000000..dc80e909f7 Binary files /dev/null and b/application/public/images/profiles/51874337-0b42-4b03-8e3d-fbd4960a9947.webp differ diff --git a/application/public/images/profiles/52c82d1c-b455-4572-aef1-0dd61b50b1d2.jpg b/application/public/images/profiles/52c82d1c-b455-4572-aef1-0dd61b50b1d2.jpg deleted file mode 100644 index a66fe8cd43..0000000000 Binary files a/application/public/images/profiles/52c82d1c-b455-4572-aef1-0dd61b50b1d2.jpg and /dev/null differ diff --git a/application/public/images/profiles/52c82d1c-b455-4572-aef1-0dd61b50b1d2.webp b/application/public/images/profiles/52c82d1c-b455-4572-aef1-0dd61b50b1d2.webp new file mode 100644 index 0000000000..f78592a977 Binary files /dev/null and b/application/public/images/profiles/52c82d1c-b455-4572-aef1-0dd61b50b1d2.webp differ diff --git a/application/public/images/profiles/538dbca6-85d6-434e-a1f4-b370d03dbb85.jpg b/application/public/images/profiles/538dbca6-85d6-434e-a1f4-b370d03dbb85.jpg deleted file mode 100644 index 2cc2dec460..0000000000 Binary files a/application/public/images/profiles/538dbca6-85d6-434e-a1f4-b370d03dbb85.jpg and /dev/null differ diff --git a/application/public/images/profiles/538dbca6-85d6-434e-a1f4-b370d03dbb85.webp b/application/public/images/profiles/538dbca6-85d6-434e-a1f4-b370d03dbb85.webp new file mode 100644 index 0000000000..3abcddd606 Binary files /dev/null and b/application/public/images/profiles/538dbca6-85d6-434e-a1f4-b370d03dbb85.webp differ diff --git a/application/public/images/profiles/5506d25e-f03b-497a-a883-6434aa160d0f.jpg b/application/public/images/profiles/5506d25e-f03b-497a-a883-6434aa160d0f.jpg deleted file mode 100644 index ce19226b90..0000000000 Binary files a/application/public/images/profiles/5506d25e-f03b-497a-a883-6434aa160d0f.jpg and /dev/null differ diff --git a/application/public/images/profiles/5506d25e-f03b-497a-a883-6434aa160d0f.webp b/application/public/images/profiles/5506d25e-f03b-497a-a883-6434aa160d0f.webp new file mode 100644 index 0000000000..b35abdc0bd Binary files /dev/null and b/application/public/images/profiles/5506d25e-f03b-497a-a883-6434aa160d0f.webp differ diff --git a/application/public/images/profiles/5e071af0-e9a1-4c5c-859f-464c18bb7da9.jpg b/application/public/images/profiles/5e071af0-e9a1-4c5c-859f-464c18bb7da9.jpg deleted file mode 100644 index 9a3ac1e9bb..0000000000 Binary files a/application/public/images/profiles/5e071af0-e9a1-4c5c-859f-464c18bb7da9.jpg and /dev/null differ diff --git a/application/public/images/profiles/5e071af0-e9a1-4c5c-859f-464c18bb7da9.webp b/application/public/images/profiles/5e071af0-e9a1-4c5c-859f-464c18bb7da9.webp new file mode 100644 index 0000000000..ca27bd6816 Binary files /dev/null and b/application/public/images/profiles/5e071af0-e9a1-4c5c-859f-464c18bb7da9.webp differ diff --git a/application/public/images/profiles/6931b54d-f07b-405d-80dc-17c09acebfa9.jpg b/application/public/images/profiles/6931b54d-f07b-405d-80dc-17c09acebfa9.jpg deleted file mode 100644 index 30dfd53a3f..0000000000 Binary files a/application/public/images/profiles/6931b54d-f07b-405d-80dc-17c09acebfa9.jpg and /dev/null differ diff --git a/application/public/images/profiles/6931b54d-f07b-405d-80dc-17c09acebfa9.webp b/application/public/images/profiles/6931b54d-f07b-405d-80dc-17c09acebfa9.webp new file mode 100644 index 0000000000..79804ebdda Binary files /dev/null and b/application/public/images/profiles/6931b54d-f07b-405d-80dc-17c09acebfa9.webp differ diff --git a/application/public/images/profiles/7d7bf516-e05e-4a4f-95fa-0c73e7bd3f93.jpg b/application/public/images/profiles/7d7bf516-e05e-4a4f-95fa-0c73e7bd3f93.jpg deleted file mode 100644 index 887c28b398..0000000000 Binary files a/application/public/images/profiles/7d7bf516-e05e-4a4f-95fa-0c73e7bd3f93.jpg and /dev/null differ diff --git a/application/public/images/profiles/7d7bf516-e05e-4a4f-95fa-0c73e7bd3f93.webp b/application/public/images/profiles/7d7bf516-e05e-4a4f-95fa-0c73e7bd3f93.webp new file mode 100644 index 0000000000..6762b8c215 Binary files /dev/null and b/application/public/images/profiles/7d7bf516-e05e-4a4f-95fa-0c73e7bd3f93.webp differ diff --git a/application/public/images/profiles/84ba6fee-d167-43c4-8b10-d94caa923f48.jpg b/application/public/images/profiles/84ba6fee-d167-43c4-8b10-d94caa923f48.jpg deleted file mode 100644 index 36f1107d67..0000000000 Binary files a/application/public/images/profiles/84ba6fee-d167-43c4-8b10-d94caa923f48.jpg and /dev/null differ diff --git a/application/public/images/profiles/84ba6fee-d167-43c4-8b10-d94caa923f48.webp b/application/public/images/profiles/84ba6fee-d167-43c4-8b10-d94caa923f48.webp new file mode 100644 index 0000000000..8139d11eda Binary files /dev/null and b/application/public/images/profiles/84ba6fee-d167-43c4-8b10-d94caa923f48.webp differ diff --git a/application/public/images/profiles/a99e1112-f0a0-46a3-8e23-5d34c27898c0.jpg b/application/public/images/profiles/a99e1112-f0a0-46a3-8e23-5d34c27898c0.jpg deleted file mode 100644 index 16115798f4..0000000000 Binary files a/application/public/images/profiles/a99e1112-f0a0-46a3-8e23-5d34c27898c0.jpg and /dev/null differ diff --git a/application/public/images/profiles/a99e1112-f0a0-46a3-8e23-5d34c27898c0.webp b/application/public/images/profiles/a99e1112-f0a0-46a3-8e23-5d34c27898c0.webp new file mode 100644 index 0000000000..41af4da510 Binary files /dev/null and b/application/public/images/profiles/a99e1112-f0a0-46a3-8e23-5d34c27898c0.webp differ diff --git a/application/public/images/profiles/af98cd5f-b1a6-408c-a455-0970b3247e4c.jpg b/application/public/images/profiles/af98cd5f-b1a6-408c-a455-0970b3247e4c.jpg deleted file mode 100644 index 65b3887c6d..0000000000 Binary files a/application/public/images/profiles/af98cd5f-b1a6-408c-a455-0970b3247e4c.jpg and /dev/null differ diff --git a/application/public/images/profiles/af98cd5f-b1a6-408c-a455-0970b3247e4c.webp b/application/public/images/profiles/af98cd5f-b1a6-408c-a455-0970b3247e4c.webp new file mode 100644 index 0000000000..d2f34b7a8f Binary files /dev/null and b/application/public/images/profiles/af98cd5f-b1a6-408c-a455-0970b3247e4c.webp differ diff --git a/application/public/images/profiles/b2c256a3-296f-49e0-ba8b-101b55146956.jpg b/application/public/images/profiles/b2c256a3-296f-49e0-ba8b-101b55146956.jpg deleted file mode 100644 index 8224e454c6..0000000000 Binary files a/application/public/images/profiles/b2c256a3-296f-49e0-ba8b-101b55146956.jpg and /dev/null differ diff --git a/application/public/images/profiles/b2c256a3-296f-49e0-ba8b-101b55146956.webp b/application/public/images/profiles/b2c256a3-296f-49e0-ba8b-101b55146956.webp new file mode 100644 index 0000000000..6211b43f45 Binary files /dev/null and b/application/public/images/profiles/b2c256a3-296f-49e0-ba8b-101b55146956.webp differ diff --git a/application/public/images/profiles/c8939885-5dca-4132-b234-64a12c1861a5.jpg b/application/public/images/profiles/c8939885-5dca-4132-b234-64a12c1861a5.jpg deleted file mode 100644 index 42a6b2d415..0000000000 Binary files a/application/public/images/profiles/c8939885-5dca-4132-b234-64a12c1861a5.jpg and /dev/null differ diff --git a/application/public/images/profiles/c8939885-5dca-4132-b234-64a12c1861a5.webp b/application/public/images/profiles/c8939885-5dca-4132-b234-64a12c1861a5.webp new file mode 100644 index 0000000000..e130377cfe Binary files /dev/null and b/application/public/images/profiles/c8939885-5dca-4132-b234-64a12c1861a5.webp differ diff --git a/application/public/images/profiles/ca81e02a-11aa-4218-971d-c8bd8d9e67cf.jpg b/application/public/images/profiles/ca81e02a-11aa-4218-971d-c8bd8d9e67cf.jpg deleted file mode 100644 index 069ef7f263..0000000000 Binary files a/application/public/images/profiles/ca81e02a-11aa-4218-971d-c8bd8d9e67cf.jpg and /dev/null differ diff --git a/application/public/images/profiles/ca81e02a-11aa-4218-971d-c8bd8d9e67cf.webp b/application/public/images/profiles/ca81e02a-11aa-4218-971d-c8bd8d9e67cf.webp new file mode 100644 index 0000000000..2c818367ef Binary files /dev/null and b/application/public/images/profiles/ca81e02a-11aa-4218-971d-c8bd8d9e67cf.webp differ diff --git a/application/public/images/profiles/cd5b31e5-0fb4-4b40-830d-3a22058b30cc.jpg b/application/public/images/profiles/cd5b31e5-0fb4-4b40-830d-3a22058b30cc.jpg deleted file mode 100644 index 53e1fada1a..0000000000 Binary files a/application/public/images/profiles/cd5b31e5-0fb4-4b40-830d-3a22058b30cc.jpg and /dev/null differ diff --git a/application/public/images/profiles/cd5b31e5-0fb4-4b40-830d-3a22058b30cc.webp b/application/public/images/profiles/cd5b31e5-0fb4-4b40-830d-3a22058b30cc.webp new file mode 100644 index 0000000000..8ce5bf1b4a Binary files /dev/null and b/application/public/images/profiles/cd5b31e5-0fb4-4b40-830d-3a22058b30cc.webp differ diff --git a/application/public/images/profiles/cf145991-b2ff-4ef5-aeb5-dbc9d9eb51a0.jpg b/application/public/images/profiles/cf145991-b2ff-4ef5-aeb5-dbc9d9eb51a0.jpg deleted file mode 100644 index 3a0b4f3fbe..0000000000 Binary files a/application/public/images/profiles/cf145991-b2ff-4ef5-aeb5-dbc9d9eb51a0.jpg and /dev/null differ diff --git a/application/public/images/profiles/cf145991-b2ff-4ef5-aeb5-dbc9d9eb51a0.webp b/application/public/images/profiles/cf145991-b2ff-4ef5-aeb5-dbc9d9eb51a0.webp new file mode 100644 index 0000000000..cb4c841d1a Binary files /dev/null and b/application/public/images/profiles/cf145991-b2ff-4ef5-aeb5-dbc9d9eb51a0.webp differ diff --git a/application/public/images/profiles/dbe9b1f0-9822-4f77-9635-f9fd64e2b4e5.jpg b/application/public/images/profiles/dbe9b1f0-9822-4f77-9635-f9fd64e2b4e5.jpg deleted file mode 100644 index 4753c56a48..0000000000 Binary files a/application/public/images/profiles/dbe9b1f0-9822-4f77-9635-f9fd64e2b4e5.jpg and /dev/null differ diff --git a/application/public/images/profiles/dbe9b1f0-9822-4f77-9635-f9fd64e2b4e5.webp b/application/public/images/profiles/dbe9b1f0-9822-4f77-9635-f9fd64e2b4e5.webp new file mode 100644 index 0000000000..382bc8714d Binary files /dev/null and b/application/public/images/profiles/dbe9b1f0-9822-4f77-9635-f9fd64e2b4e5.webp differ diff --git a/application/public/images/profiles/ed0d327c-2ba5-4b23-8284-3e31f7a51d16.jpg b/application/public/images/profiles/ed0d327c-2ba5-4b23-8284-3e31f7a51d16.jpg deleted file mode 100644 index 82c2cff091..0000000000 Binary files a/application/public/images/profiles/ed0d327c-2ba5-4b23-8284-3e31f7a51d16.jpg and /dev/null differ diff --git a/application/public/images/profiles/ed0d327c-2ba5-4b23-8284-3e31f7a51d16.webp b/application/public/images/profiles/ed0d327c-2ba5-4b23-8284-3e31f7a51d16.webp new file mode 100644 index 0000000000..5036ef93e1 Binary files /dev/null and b/application/public/images/profiles/ed0d327c-2ba5-4b23-8284-3e31f7a51d16.webp differ diff --git a/application/public/images/profiles/f1f4c2c2-bf06-44b5-b43e-02a00d770242.jpg b/application/public/images/profiles/f1f4c2c2-bf06-44b5-b43e-02a00d770242.jpg deleted file mode 100644 index 26f38db2ae..0000000000 Binary files a/application/public/images/profiles/f1f4c2c2-bf06-44b5-b43e-02a00d770242.jpg and /dev/null differ diff --git a/application/public/images/profiles/f1f4c2c2-bf06-44b5-b43e-02a00d770242.webp b/application/public/images/profiles/f1f4c2c2-bf06-44b5-b43e-02a00d770242.webp new file mode 100644 index 0000000000..d5f206c9e7 Binary files /dev/null and b/application/public/images/profiles/f1f4c2c2-bf06-44b5-b43e-02a00d770242.webp differ diff --git a/application/public/images/profiles/f4619909-0f90-45dd-ada0-6c6305453a74.jpg b/application/public/images/profiles/f4619909-0f90-45dd-ada0-6c6305453a74.jpg deleted file mode 100644 index 6e361f141b..0000000000 Binary files a/application/public/images/profiles/f4619909-0f90-45dd-ada0-6c6305453a74.jpg and /dev/null differ diff --git a/application/public/images/profiles/f4619909-0f90-45dd-ada0-6c6305453a74.webp b/application/public/images/profiles/f4619909-0f90-45dd-ada0-6c6305453a74.webp new file mode 100644 index 0000000000..a3d9beed78 Binary files /dev/null and b/application/public/images/profiles/f4619909-0f90-45dd-ada0-6c6305453a74.webp differ diff --git a/application/public/images/profiles/fd571d42-c471-47fd-846f-d3c1325685fd.jpg b/application/public/images/profiles/fd571d42-c471-47fd-846f-d3c1325685fd.jpg deleted file mode 100644 index 8477503ea7..0000000000 Binary files a/application/public/images/profiles/fd571d42-c471-47fd-846f-d3c1325685fd.jpg and /dev/null differ diff --git a/application/public/images/profiles/fd571d42-c471-47fd-846f-d3c1325685fd.webp b/application/public/images/profiles/fd571d42-c471-47fd-846f-d3c1325685fd.webp new file mode 100644 index 0000000000..6a27a222dd Binary files /dev/null and b/application/public/images/profiles/fd571d42-c471-47fd-846f-d3c1325685fd.webp differ diff --git a/application/public/movies/090e7491-5cdb-4a1b-88b1-1e036a45e296.mp4 b/application/public/movies/090e7491-5cdb-4a1b-88b1-1e036a45e296.mp4 new file mode 100644 index 0000000000..3d2f0bc81e Binary files /dev/null and b/application/public/movies/090e7491-5cdb-4a1b-88b1-1e036a45e296.mp4 differ diff --git a/application/public/movies/090e7491-5cdb-4a1b-88b1-1e036a45e296_preview.mp4 b/application/public/movies/090e7491-5cdb-4a1b-88b1-1e036a45e296_preview.mp4 new file mode 100644 index 0000000000..0eed559d0e Binary files /dev/null and b/application/public/movies/090e7491-5cdb-4a1b-88b1-1e036a45e296_preview.mp4 differ diff --git a/application/public/movies/0c4b66bc-091e-4f76-85a3-288567cfdc12.mp4 b/application/public/movies/0c4b66bc-091e-4f76-85a3-288567cfdc12.mp4 new file mode 100644 index 0000000000..a3e82105c7 Binary files /dev/null and b/application/public/movies/0c4b66bc-091e-4f76-85a3-288567cfdc12.mp4 differ diff --git a/application/public/movies/0c4b66bc-091e-4f76-85a3-288567cfdc12_preview.mp4 b/application/public/movies/0c4b66bc-091e-4f76-85a3-288567cfdc12_preview.mp4 new file mode 100644 index 0000000000..630ff56e7d Binary files /dev/null and b/application/public/movies/0c4b66bc-091e-4f76-85a3-288567cfdc12_preview.mp4 differ diff --git a/application/public/movies/1b558288-6ec6-4ece-a9b8-4259379b7489.mp4 b/application/public/movies/1b558288-6ec6-4ece-a9b8-4259379b7489.mp4 new file mode 100644 index 0000000000..17f22776e5 Binary files /dev/null and b/application/public/movies/1b558288-6ec6-4ece-a9b8-4259379b7489.mp4 differ diff --git a/application/public/movies/1b558288-6ec6-4ece-a9b8-4259379b7489_preview.mp4 b/application/public/movies/1b558288-6ec6-4ece-a9b8-4259379b7489_preview.mp4 new file mode 100644 index 0000000000..72056cad3a Binary files /dev/null and b/application/public/movies/1b558288-6ec6-4ece-a9b8-4259379b7489_preview.mp4 differ diff --git a/application/public/movies/241b7993-f7c4-49e5-84f0-bbaf6a144634.mp4 b/application/public/movies/241b7993-f7c4-49e5-84f0-bbaf6a144634.mp4 new file mode 100644 index 0000000000..4e06f94f6b Binary files /dev/null and b/application/public/movies/241b7993-f7c4-49e5-84f0-bbaf6a144634.mp4 differ diff --git a/application/public/movies/241b7993-f7c4-49e5-84f0-bbaf6a144634_preview.mp4 b/application/public/movies/241b7993-f7c4-49e5-84f0-bbaf6a144634_preview.mp4 new file mode 100644 index 0000000000..54b42c2495 Binary files /dev/null and b/application/public/movies/241b7993-f7c4-49e5-84f0-bbaf6a144634_preview.mp4 differ diff --git a/application/public/movies/3cb50e48-535b-4e5f-bbde-455c01def021.mp4 b/application/public/movies/3cb50e48-535b-4e5f-bbde-455c01def021.mp4 new file mode 100644 index 0000000000..fa1993549f Binary files /dev/null and b/application/public/movies/3cb50e48-535b-4e5f-bbde-455c01def021.mp4 differ diff --git a/application/public/movies/3cb50e48-535b-4e5f-bbde-455c01def021_preview.mp4 b/application/public/movies/3cb50e48-535b-4e5f-bbde-455c01def021_preview.mp4 new file mode 100644 index 0000000000..d96de85dbf Binary files /dev/null and b/application/public/movies/3cb50e48-535b-4e5f-bbde-455c01def021_preview.mp4 differ diff --git a/application/public/movies/51a14d70-9dd6-45ad-9f87-64af91ec2779.mp4 b/application/public/movies/51a14d70-9dd6-45ad-9f87-64af91ec2779.mp4 new file mode 100644 index 0000000000..b4608a9ca9 Binary files /dev/null and b/application/public/movies/51a14d70-9dd6-45ad-9f87-64af91ec2779.mp4 differ diff --git a/application/public/movies/51a14d70-9dd6-45ad-9f87-64af91ec2779_preview.mp4 b/application/public/movies/51a14d70-9dd6-45ad-9f87-64af91ec2779_preview.mp4 new file mode 100644 index 0000000000..584c1ac802 Binary files /dev/null and b/application/public/movies/51a14d70-9dd6-45ad-9f87-64af91ec2779_preview.mp4 differ diff --git a/application/public/movies/6ccc437c-253d-4e6f-baa2-2f4f2419f830.mp4 b/application/public/movies/6ccc437c-253d-4e6f-baa2-2f4f2419f830.mp4 new file mode 100644 index 0000000000..0788b3660c Binary files /dev/null and b/application/public/movies/6ccc437c-253d-4e6f-baa2-2f4f2419f830.mp4 differ diff --git a/application/public/movies/6ccc437c-253d-4e6f-baa2-2f4f2419f830_preview.mp4 b/application/public/movies/6ccc437c-253d-4e6f-baa2-2f4f2419f830_preview.mp4 new file mode 100644 index 0000000000..2a0405c1e9 Binary files /dev/null and b/application/public/movies/6ccc437c-253d-4e6f-baa2-2f4f2419f830_preview.mp4 differ diff --git a/application/public/movies/74eb4b82-601d-40ec-9aa5-70c4ac5d9799.mp4 b/application/public/movies/74eb4b82-601d-40ec-9aa5-70c4ac5d9799.mp4 new file mode 100644 index 0000000000..b485796667 Binary files /dev/null and b/application/public/movies/74eb4b82-601d-40ec-9aa5-70c4ac5d9799.mp4 differ diff --git a/application/public/movies/74eb4b82-601d-40ec-9aa5-70c4ac5d9799_preview.mp4 b/application/public/movies/74eb4b82-601d-40ec-9aa5-70c4ac5d9799_preview.mp4 new file mode 100644 index 0000000000..77a6294f4f Binary files /dev/null and b/application/public/movies/74eb4b82-601d-40ec-9aa5-70c4ac5d9799_preview.mp4 differ diff --git a/application/public/movies/7518b1ae-3bc5-4b42-b82b-0013a3a74b16.mp4 b/application/public/movies/7518b1ae-3bc5-4b42-b82b-0013a3a74b16.mp4 new file mode 100644 index 0000000000..baa3df4f34 Binary files /dev/null and b/application/public/movies/7518b1ae-3bc5-4b42-b82b-0013a3a74b16.mp4 differ diff --git a/application/public/movies/7518b1ae-3bc5-4b42-b82b-0013a3a74b16_preview.mp4 b/application/public/movies/7518b1ae-3bc5-4b42-b82b-0013a3a74b16_preview.mp4 new file mode 100644 index 0000000000..24a59f0971 Binary files /dev/null and b/application/public/movies/7518b1ae-3bc5-4b42-b82b-0013a3a74b16_preview.mp4 differ diff --git a/application/public/movies/826f0b4d-0f4b-408c-9560-a82798116255.mp4 b/application/public/movies/826f0b4d-0f4b-408c-9560-a82798116255.mp4 new file mode 100644 index 0000000000..3d70299695 Binary files /dev/null and b/application/public/movies/826f0b4d-0f4b-408c-9560-a82798116255.mp4 differ diff --git a/application/public/movies/826f0b4d-0f4b-408c-9560-a82798116255_preview.mp4 b/application/public/movies/826f0b4d-0f4b-408c-9560-a82798116255_preview.mp4 new file mode 100644 index 0000000000..615d66a790 Binary files /dev/null and b/application/public/movies/826f0b4d-0f4b-408c-9560-a82798116255_preview.mp4 differ diff --git a/application/public/movies/b3998a47-ee87-483e-acf1-8e5b69c8527a.mp4 b/application/public/movies/b3998a47-ee87-483e-acf1-8e5b69c8527a.mp4 new file mode 100644 index 0000000000..8d026fcfec Binary files /dev/null and b/application/public/movies/b3998a47-ee87-483e-acf1-8e5b69c8527a.mp4 differ diff --git a/application/public/movies/b3998a47-ee87-483e-acf1-8e5b69c8527a_preview.mp4 b/application/public/movies/b3998a47-ee87-483e-acf1-8e5b69c8527a_preview.mp4 new file mode 100644 index 0000000000..5e7ec02218 Binary files /dev/null and b/application/public/movies/b3998a47-ee87-483e-acf1-8e5b69c8527a_preview.mp4 differ diff --git a/application/public/movies/b44e6ef6-fb30-4f59-9c86-70fe0f1edf08.mp4 b/application/public/movies/b44e6ef6-fb30-4f59-9c86-70fe0f1edf08.mp4 new file mode 100644 index 0000000000..be9b09cba5 Binary files /dev/null and b/application/public/movies/b44e6ef6-fb30-4f59-9c86-70fe0f1edf08.mp4 differ diff --git a/application/public/movies/b44e6ef6-fb30-4f59-9c86-70fe0f1edf08_preview.mp4 b/application/public/movies/b44e6ef6-fb30-4f59-9c86-70fe0f1edf08_preview.mp4 new file mode 100644 index 0000000000..3cb5c1dc63 Binary files /dev/null and b/application/public/movies/b44e6ef6-fb30-4f59-9c86-70fe0f1edf08_preview.mp4 differ diff --git a/application/public/movies/c8f1d48d-d831-4d69-9477-0112152f95b9.mp4 b/application/public/movies/c8f1d48d-d831-4d69-9477-0112152f95b9.mp4 new file mode 100644 index 0000000000..4e45c1c8fa Binary files /dev/null and b/application/public/movies/c8f1d48d-d831-4d69-9477-0112152f95b9.mp4 differ diff --git a/application/public/movies/c8f1d48d-d831-4d69-9477-0112152f95b9_preview.mp4 b/application/public/movies/c8f1d48d-d831-4d69-9477-0112152f95b9_preview.mp4 new file mode 100644 index 0000000000..cace0aea82 Binary files /dev/null and b/application/public/movies/c8f1d48d-d831-4d69-9477-0112152f95b9_preview.mp4 differ diff --git a/application/public/movies/db504945-d122-4c70-8c2d-fe636282ca00.mp4 b/application/public/movies/db504945-d122-4c70-8c2d-fe636282ca00.mp4 new file mode 100644 index 0000000000..d9ef656b30 Binary files /dev/null and b/application/public/movies/db504945-d122-4c70-8c2d-fe636282ca00.mp4 differ diff --git a/application/public/movies/db504945-d122-4c70-8c2d-fe636282ca00_preview.mp4 b/application/public/movies/db504945-d122-4c70-8c2d-fe636282ca00_preview.mp4 new file mode 100644 index 0000000000..b9f03aa6e7 Binary files /dev/null and b/application/public/movies/db504945-d122-4c70-8c2d-fe636282ca00_preview.mp4 differ diff --git a/application/public/movies/fafa6ec6-1572-4def-aa16-4a9fbf28aa41.mp4 b/application/public/movies/fafa6ec6-1572-4def-aa16-4a9fbf28aa41.mp4 new file mode 100644 index 0000000000..dd57ba5f42 Binary files /dev/null and b/application/public/movies/fafa6ec6-1572-4def-aa16-4a9fbf28aa41.mp4 differ diff --git a/application/public/movies/fafa6ec6-1572-4def-aa16-4a9fbf28aa41_preview.mp4 b/application/public/movies/fafa6ec6-1572-4def-aa16-4a9fbf28aa41_preview.mp4 new file mode 100644 index 0000000000..13e5c3a4ee Binary files /dev/null and b/application/public/movies/fafa6ec6-1572-4def-aa16-4a9fbf28aa41_preview.mp4 differ diff --git a/application/server/package.json b/application/server/package.json index 9482575df7..c0f146dc5f 100644 --- a/application/server/package.json +++ b/application/server/package.json @@ -17,14 +17,18 @@ "@web-speed-hackathon-2026/server": "workspace:*", "bcrypt": "6.0.0", "body-parser": "2.2.0", + "compression": "1.8.1", "connect-history-api-fallback": "2.0.0", "express": "5.1.0", "express-session": "1.18.2", + "ffmpeg-static": "5.3.0", "file-type": "21.1.1", + "fluent-ffmpeg": "2.1.3", "http-errors": "2.0.0", "music-metadata": "11.10.3", "sequelize": "6.37.7", "serve-static": "2.2.0", + "sharp": "0.34.5", "sqlite3": "5.1.7", "tsx": "4.20.6", "uuid": "13.0.0", @@ -34,9 +38,11 @@ "@faker-js/faker": "10.2.0", "@types/bcrypt": "6.0.0", "@types/body-parser": "1.19.6", + "@types/compression": "1.8.1", "@types/connect-history-api-fallback": "1.5.4", "@types/express": "5.0.3", "@types/express-session": "1.18.2", + "@types/fluent-ffmpeg": "2.1.28", "@types/http-errors": "2.0.5", "@types/node": "22.18.8", "@types/serve-static": "1.15.9", diff --git a/application/server/src/app.ts b/application/server/src/app.ts index 671fb424cc..79f7928448 100644 --- a/application/server/src/app.ts +++ b/application/server/src/app.ts @@ -1,4 +1,5 @@ import bodyParser from "body-parser"; +import compression from "compression"; import Express from "express"; import { apiRouter } from "@web-speed-hackathon-2026/server/src/routes/api"; @@ -9,15 +10,23 @@ export const app = Express(); app.set("trust proxy", true); +app.use(compression()); app.use(sessionMiddleware); + +// favicon.ico への 404 エラーを抑制するために 204 (No Content) を返す +app.get("/favicon.ico", (_req, res) => { + res.status(204).end(); +}); + app.use(bodyParser.json()); app.use(bodyParser.raw({ limit: "10mb" })); -app.use((_req, res, next) => { - res.header({ - "Cache-Control": "max-age=0, no-transform", - Connection: "close", - }); +app.use((req, res, next) => { + if (req.path.startsWith("/api/")) { + res.header({ + "Cache-Control": "max-age=0, no-transform", + }); + } return next(); }); diff --git a/application/server/src/index.ts b/application/server/src/index.ts index 8b7858889a..a266f90ac9 100644 --- a/application/server/src/index.ts +++ b/application/server/src/index.ts @@ -1,15 +1,19 @@ -import "@web-speed-hackathon-2026/server/src/utils/express_websocket_support"; -import { app } from "@web-speed-hackathon-2026/server/src/app"; +import "./utils/express_websocket_support"; +import { app } from "./app"; import { initializeSequelize } from "./sequelize"; +import { initializeVideos } from "./utils/video_initializer"; async function main() { await initializeSequelize(); + await initializeVideos(); const server = app.listen(Number(process.env["PORT"] || 3000), "0.0.0.0", () => { const address = server.address(); - if (typeof address === "object") { - console.log(`Listening on ${address?.address}:${address?.port}`); + if (address && typeof address === "object") { + console.log(`Listening on ${address.address}:${address.port}`); + } else if (typeof address === "string") { + console.log(`Listening on ${address}`); } }); } diff --git a/application/server/src/routes/api/crok.ts b/application/server/src/routes/api/crok.ts index cfd6065951..7afb996b98 100644 --- a/application/server/src/routes/api/crok.ts +++ b/application/server/src/routes/api/crok.ts @@ -33,21 +33,24 @@ crokRouter.get("/crok", async (req, res) => { let messageId = 0; - // TTFT (Time to First Token) - await sleep(3000); + const send = (data: any) => { + res.write(`id: ${messageId++}\ndata: ${JSON.stringify(data)}\n\n`); + // compression ミドルウェアを使用している場合、flush() しないとブラウザに届かない + if ((res as any).flush) { + (res as any).flush(); + } + }; + + // TTFT (Time to First Token) を削除して即座に返答開始 for (const char of response) { if (res.closed) break; - - const data = JSON.stringify({ text: char, done: false }); - res.write(`event: message\nid: ${messageId++}\ndata: ${data}\n\n`); - + send({ text: char, done: false }); await sleep(10); } if (!res.closed) { - const data = JSON.stringify({ text: "", done: true }); - res.write(`event: message\nid: ${messageId}\ndata: ${data}\n\n`); + send({ text: "", done: true }); } res.end(); diff --git a/application/server/src/routes/api/direct_message.ts b/application/server/src/routes/api/direct_message.ts index 2993a2d6be..713590d8e8 100644 --- a/application/server/src/routes/api/direct_message.ts +++ b/application/server/src/routes/api/direct_message.ts @@ -100,17 +100,40 @@ directMessageRouter.get("/dm/:conversationId", async (req, res) => { throw new httpErrors.Unauthorized(); } + const limit = req.query["limit"] != null ? Number(req.query["limit"]) : 50; + const offset = req.query["offset"] != null ? Number(req.query["offset"]) : 0; + const conversation = await DirectMessageConversation.findOne({ where: { id: req.params.conversationId, [Op.or]: [{ initiatorId: req.session.userId }, { memberId: req.session.userId }], }, + // デフォルトスコープに含まれるメッセージを上書きするため、個別に include を指定 + include: [ + { association: "initiator", include: [{ association: "profileImage" }] }, + { association: "member", include: [{ association: "profileImage" }] }, + { + association: "messages", + include: [{ association: "sender", include: [{ association: "profileImage" }] }], + limit, + offset, + order: [["createdAt", "DESC"]], + required: false, + }, + ], }); + if (conversation === null) { throw new httpErrors.NotFound(); } - return res.status(200).type("application/json").send(conversation); + // クライアント側で使いやすいようにメッセージの順序を昇順に戻す + const result = conversation.toJSON(); + if (result.messages) { + result.messages.reverse(); + } + + return res.status(200).type("application/json").send(result); }); directMessageRouter.ws("/dm/:conversationId", async (req, _res) => { diff --git a/application/server/src/routes/api/image.ts b/application/server/src/routes/api/image.ts index d5c23e209d..8e2f2e9be5 100644 --- a/application/server/src/routes/api/image.ts +++ b/application/server/src/routes/api/image.ts @@ -4,15 +4,65 @@ import path from "path"; import { Router } from "express"; import { fileTypeFromBuffer } from "file-type"; import httpErrors from "http-errors"; +import sharp from "sharp"; import { v4 as uuidv4 } from "uuid"; -import { UPLOAD_PATH } from "@web-speed-hackathon-2026/server/src/paths"; +import { UPLOAD_PATH, PUBLIC_PATH } from "@web-speed-hackathon-2026/server/src/paths"; -// 変換した画像の拡張子 -const EXTENSION = "jpg"; +// サーバー側で保存・配信する標準の拡張子 +const TARGET_EXTENSION = "webp"; export const imageRouter = Router(); +imageRouter.get("/images/:id/thumbnail", async (req, res) => { + const { id } = req.params; + const thumbFileName = `./thumbnails/${id}.webp`; + const cachePath = path.resolve(UPLOAD_PATH, thumbFileName); + + try { + // 1. キャッシュ確認 + try { + await fs.access(cachePath); + return res.type("image/webp").sendFile(cachePath); + } catch {} + + // 2. オリジナル確認(webp または jpg) + let filePath: string | null = null; + const candidates = [ + path.resolve(UPLOAD_PATH, `./images/${id}.webp`), + path.resolve(PUBLIC_PATH, `./images/${id}.webp`), + path.resolve(PUBLIC_PATH, `./images/${id}.jpg`), // シードデータ用 + path.resolve(PUBLIC_PATH, `./images/profiles/${id}.webp`), + path.resolve(PUBLIC_PATH, `./images/profiles/${id}.jpg`), + ]; + + for (const cand of candidates) { + try { + await fs.access(cand); + filePath = cand; + break; + } catch {} + } + + if (!filePath) throw new Error("Original image not found"); + + // 3. サムネイル生成 + const buffer = await fs.readFile(filePath); + const thumbnail = await sharp(buffer) + .resize({ width: 400, withoutEnlargement: true }) + .webp({ quality: 70 }) + .toBuffer(); + + // 4. キャッシュ保存 + await fs.mkdir(path.dirname(cachePath), { recursive: true }); + await fs.writeFile(cachePath, thumbnail); + + res.type("image/webp").send(thumbnail); + } catch (error) { + throw new httpErrors.NotFound(); + } +}); + imageRouter.post("/images", async (req, res) => { if (req.session.userId === undefined) { throw new httpErrors.Unauthorized(); @@ -22,15 +72,36 @@ imageRouter.post("/images", async (req, res) => { } const type = await fileTypeFromBuffer(req.body); - if (type === undefined || type.ext !== EXTENSION) { - throw new httpErrors.BadRequest("Invalid file type"); + // jpg, png, webp など画像であれば受け入れる + if (type === undefined || !type.mime.startsWith("image/")) { + throw new httpErrors.BadRequest("Invalid file type. Only images are allowed."); } const imageId = uuidv4(); + const imageDir = path.resolve(UPLOAD_PATH, "images"); + const finalPath = path.resolve(imageDir, `./${imageId}.${TARGET_EXTENSION}`); + + await fs.mkdir(imageDir, { recursive: true }); + + // 変換処理を非同期(await しない)で実行 + (async () => { + try { + const resizedBuffer = await sharp(req.body) + .resize({ + fit: "inside", + width: 1080, + withoutEnlargement: true, + }) + .webp({ quality: 80 }) + .withMetadata() + .toBuffer(); - const filePath = path.resolve(UPLOAD_PATH, `./images/${imageId}.${EXTENSION}`); - await fs.mkdir(path.resolve(UPLOAD_PATH, "images"), { recursive: true }); - await fs.writeFile(filePath, req.body); + await fs.writeFile(finalPath, resizedBuffer); + } catch (err) { + console.error(`Background image processing failed for ${imageId}:`, err); + } + })(); + // 処理を待たずに即座に ID を返却 return res.status(200).type("application/json").send({ id: imageId }); }); diff --git a/application/server/src/routes/api/initialize.ts b/application/server/src/routes/api/initialize.ts index de1044cef0..68320f0712 100644 --- a/application/server/src/routes/api/initialize.ts +++ b/application/server/src/routes/api/initialize.ts @@ -9,13 +9,27 @@ import { sessionStore } from "../../session"; export const initializeRouter = Router(); -initializeRouter.post("/initialize", async (_req, res) => { - // DBリセット - await initializeSequelize(); - // sessionStoreをクリア - sessionStore.clear(); - // uploadディレクトリをクリア - await fs.rm(UPLOAD_PATH, { force: true, recursive: true }); +// GET でも疎通確認ができるようにする(本来は POST のみだがデバッグと利便性のため) +const handleInitialize = async (_req: any, res: any) => { + try { + // DBリセット + await initializeSequelize(); + // sessionStoreをクリア + sessionStore.clear(); + // uploadディレクトリをクリア + try { + await fs.rm(UPLOAD_PATH, { force: true, recursive: true }); + } catch (e) { + console.error("Failed to remove upload path:", e); + } + await fs.mkdir(UPLOAD_PATH, { recursive: true }); - return res.status(200).type("application/json").send({}); -}); + return res.status(200).type("application/json").send({}); + } catch (error: any) { + console.error("Initialize failed:", error); + return res.status(500).json({ error: error.message }); + } +}; + +initializeRouter.post("/initialize", handleInitialize); +initializeRouter.get("/initialize", handleInitialize); diff --git a/application/server/src/routes/api/movie.ts b/application/server/src/routes/api/movie.ts index 4c96c207be..378e56a3f5 100644 --- a/application/server/src/routes/api/movie.ts +++ b/application/server/src/routes/api/movie.ts @@ -1,18 +1,48 @@ import { promises as fs } from "fs"; import path from "path"; +import ffmpegPath from "ffmpeg-static"; +import fluentFfmpeg from "fluent-ffmpeg"; import { Router } from "express"; import { fileTypeFromBuffer } from "file-type"; import httpErrors from "http-errors"; import { v4 as uuidv4 } from "uuid"; -import { UPLOAD_PATH } from "@web-speed-hackathon-2026/server/src/paths"; +import { UPLOAD_PATH, PUBLIC_PATH } from "@web-speed-hackathon-2026/server/src/paths"; // 変換した動画の拡張子 const EXTENSION = "gif"; +const VIDEO_EXTENSION = "mp4"; + +if (ffmpegPath) { + fluentFfmpeg.setFfmpegPath(ffmpegPath); +} export const movieRouter = Router(); +movieRouter.get("/movies/:id/preview", async (req, res) => { + const { id } = req.params; + const previewFileName = `./movies/${id}_preview.${VIDEO_EXTENSION}`; + const uploadPath = path.resolve(UPLOAD_PATH, previewFileName); + const publicPath = path.resolve(PUBLIC_PATH, previewFileName); + + try { + let filePath: string; + try { + await fs.access(uploadPath); + filePath = uploadPath; + } catch { + await fs.access(publicPath); + filePath = publicPath; + } + + return res.type("video/mp4").sendFile(filePath); + } catch (error) { + console.error("Preview fetching failed:", error); + throw new httpErrors.NotFound(); + } +}); + movieRouter.post("/movies", async (req, res) => { if (req.session.userId === undefined) { throw new httpErrors.Unauthorized(); @@ -27,10 +57,66 @@ movieRouter.post("/movies", async (req, res) => { } const movieId = uuidv4(); + const movieDir = path.resolve(UPLOAD_PATH, "movies"); + + const tempGifPath = path.resolve(movieDir, `${movieId}.tmp.${EXTENSION}`); + const mp4Path = path.resolve(movieDir, `${movieId}.${VIDEO_EXTENSION}`); + const previewMp4Path = path.resolve(movieDir, `${movieId}_preview.${VIDEO_EXTENSION}`); + const finalGifPath = path.resolve(movieDir, `${movieId}.${EXTENSION}`); + + await fs.mkdir(movieDir, { recursive: true }); + + // 元の GIF を一旦保存 + await fs.writeFile(tempGifPath, req.body); + + // 変換処理を非同期(await しない)で実行 + (async () => { + try { + // 1. フル解像度 MP4 に変換(解像度維持・極限スリム化) + const convertFull = new Promise((resolve, reject) => { + fluentFfmpeg(tempGifPath) + .outputOptions([ + "-c:v libx264", + "-pix_fmt yuv420p", + "-vf scale=trunc(iw/2)*2:trunc(ih/2)*2", // 解像度は維持しつつ偶数化 + "-crf 40", + "-maxrate 800k", + "-bufsize 1600k", + "-preset slower", + "-movflags +faststart" + ]) + .toFormat("mp4") + .on("end", () => resolve()) + .on("error", (err) => reject(err)) + .save(mp4Path); + }); + + // 2. プレビュー用 MP4 (5秒, 低解像度) に変換 + const convertPreview = new Promise((resolve, reject) => { + fluentFfmpeg(tempGifPath) + .outputOptions([ + "-t 5", + "-c:v libx264", + "-pix_fmt yuv420p", + "-vf scale=256:-2", + "-crf 32", + "-preset fast", + "-movflags +faststart" + ]) + .toFormat("mp4") + .on("end", () => resolve()) + .on("error", (err) => reject(err)) + .save(previewMp4Path); + }); - const filePath = path.resolve(UPLOAD_PATH, `./movies/${movieId}.${EXTENSION}`); - await fs.mkdir(path.resolve(UPLOAD_PATH, "movies"), { recursive: true }); - await fs.writeFile(filePath, req.body); + await Promise.all([convertFull, convertPreview]); + await fs.rename(tempGifPath, finalGifPath); + console.log(`Successfully converted movie: ${movieId}`); + } catch (err) { + console.error(`Background conversion failed for ${movieId}:`, err); + } + })(); + // 変換を待たずにレスポンスを返す return res.status(200).type("application/json").send({ id: movieId }); }); diff --git a/application/server/src/routes/api/post.ts b/application/server/src/routes/api/post.ts index cda8654b2b..581ae16db9 100644 --- a/application/server/src/routes/api/post.ts +++ b/application/server/src/routes/api/post.ts @@ -5,35 +5,67 @@ import { Comment, Post } from "@web-speed-hackathon-2026/server/src/models"; export const postRouter = Router(); +// 簡易的なインメモリキャッシュ +const apiCache = new Map(); +const CACHE_TTL_MS = 5000; // 5秒間キャッシュ + +function getCached(key: string) { + const cached = apiCache.get(key); + if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { + return cached.data; + } + return null; +} + +function setCache(key: string, data: any) { + apiCache.set(key, { data, timestamp: Date.now() }); +} + postRouter.get("/posts", async (req, res) => { - const posts = await Post.findAll({ - limit: req.query["limit"] != null ? Number(req.query["limit"]) : undefined, - offset: req.query["offset"] != null ? Number(req.query["offset"]) : undefined, - }); + const limit = req.query["limit"] != null ? Number(req.query["limit"]) : undefined; + const offset = req.query["offset"] != null ? Number(req.query["offset"]) : undefined; + + const cacheKey = `posts:${limit}:${offset}`; + const cachedData = getCached(cacheKey); + if (cachedData) return res.status(200).type("application/json").send(cachedData); + + const posts = await Post.findAll({ limit, offset }); + setCache(cacheKey, posts); return res.status(200).type("application/json").send(posts); }); postRouter.get("/posts/:postId", async (req, res) => { - const post = await Post.findByPk(req.params.postId); + const cacheKey = `post:${req.params.postId}`; + const cachedData = getCached(cacheKey); + if (cachedData) return res.status(200).type("application/json").send(cachedData); + const post = await Post.findByPk(req.params.postId); if (post === null) { throw new httpErrors.NotFound(); } + setCache(cacheKey, post); return res.status(200).type("application/json").send(post); }); postRouter.get("/posts/:postId/comments", async (req, res) => { - const posts = await Comment.findAll({ - limit: req.query["limit"] != null ? Number(req.query["limit"]) : undefined, - offset: req.query["offset"] != null ? Number(req.query["offset"]) : undefined, - where: { - postId: req.params.postId, - }, + const limit = req.query["limit"] != null ? Number(req.query["limit"]) : undefined; + const offset = req.query["offset"] != null ? Number(req.query["offset"]) : undefined; + const { postId } = req.params; + + const cacheKey = `comments:${postId}:${limit}:${offset}`; + const cachedData = getCached(cacheKey); + if (cachedData) return res.status(200).type("application/json").send(cachedData); + + const comments = await Comment.findAll({ + limit, + offset, + where: { postId }, }); + setCache(cacheKey, comments); - return res.status(200).type("application/json").send(posts); + return res.status(200).type("application/json").send(comments); }); postRouter.post("/posts", async (req, res) => { diff --git a/application/server/src/routes/api/search.ts b/application/server/src/routes/api/search.ts index 48e99856b4..3602ea682e 100644 --- a/application/server/src/routes/api/search.ts +++ b/application/server/src/routes/api/search.ts @@ -6,6 +6,10 @@ import { parseSearchQuery } from "@web-speed-hackathon-2026/server/src/utils/par export const searchRouter = Router(); +// 簡易的なキャッシュ +const searchCache = new Map(); +const CACHE_TTL_MS = 5000; + searchRouter.get("/search", async (req, res) => { const query = req.query["q"]; @@ -13,6 +17,15 @@ searchRouter.get("/search", async (req, res) => { return res.status(200).type("application/json").send([]); } + const limit = req.query["limit"] != null ? Number(req.query["limit"]) : undefined; + const offset = req.query["offset"] != null ? Number(req.query["offset"]) : undefined; + + const cacheKey = `search:${query}:${limit}:${offset}`; + const cached = searchCache.get(cacheKey); + if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { + return res.status(200).type("application/json").send(cached.data); + } + const { keywords, sinceDate, untilDate } = parseSearchQuery(query); // キーワードも日付フィルターもない場合は空配列を返す @@ -21,8 +34,6 @@ searchRouter.get("/search", async (req, res) => { } const searchTerm = keywords ? `%${keywords}%` : null; - const limit = req.query["limit"] != null ? Number(req.query["limit"]) : undefined; - const offset = req.query["offset"] != null ? Number(req.query["offset"]) : undefined; // 日付条件を構築 const dateConditions: Record[] = []; @@ -35,58 +46,39 @@ searchRouter.get("/search", async (req, res) => { const dateWhere = dateConditions.length > 0 ? { createdAt: Object.assign({}, ...dateConditions) } : {}; - // テキスト検索条件 - const textWhere = searchTerm ? { text: { [Op.like]: searchTerm } } : {}; - - const postsByText = await Post.findAll({ + const posts = await Post.findAll({ + include: [ + { + association: "user", + include: [{ association: "profileImage" }], + required: true, + where: searchTerm + ? { + [Op.or]: [ + { username: { [Op.like]: searchTerm } }, + { name: { [Op.like]: searchTerm } }, + { "$Post.text$": { [Op.like]: searchTerm } }, + ], + } + : undefined, + }, + { + association: "images", + through: { attributes: [] }, + }, + { association: "movie" }, + { association: "sound" }, + ], limit, offset, + subQuery: false, + order: [["createdAt", "DESC"]], where: { - ...textWhere, ...dateWhere, }, }); - // ユーザー名/名前での検索(キーワードがある場合のみ) - let postsByUser: typeof postsByText = []; - if (searchTerm) { - postsByUser = await Post.findAll({ - include: [ - { - association: "user", - attributes: { exclude: ["profileImageId"] }, - include: [{ association: "profileImage" }], - required: true, - where: { - [Op.or]: [{ username: { [Op.like]: searchTerm } }, { name: { [Op.like]: searchTerm } }], - }, - }, - { - association: "images", - through: { attributes: [] }, - }, - { association: "movie" }, - { association: "sound" }, - ], - limit, - offset, - where: dateWhere, - }); - } - - const postIdSet = new Set(); - const mergedPosts: typeof postsByText = []; - - for (const post of [...postsByText, ...postsByUser]) { - if (!postIdSet.has(post.id)) { - postIdSet.add(post.id); - mergedPosts.push(post); - } - } - - mergedPosts.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); - - const result = mergedPosts.slice(offset || 0, (offset || 0) + (limit || mergedPosts.length)); + searchCache.set(cacheKey, { data: posts, timestamp: Date.now() }); - return res.status(200).type("application/json").send(result); + return res.status(200).type("application/json").send(posts); }); diff --git a/application/server/src/routes/static.ts b/application/server/src/routes/static.ts index b5820c986e..95ccb7e51b 100644 --- a/application/server/src/routes/static.ts +++ b/application/server/src/routes/static.ts @@ -15,21 +15,24 @@ staticRouter.use(history()); staticRouter.use( serveStatic(UPLOAD_PATH, { - etag: false, - lastModified: false, + maxAge: "1d", }), ); staticRouter.use( serveStatic(PUBLIC_PATH, { - etag: false, - lastModified: false, + maxAge: "1d", }), ); staticRouter.use( serveStatic(CLIENT_DIST_PATH, { - etag: false, - lastModified: false, + setHeaders: (res, path) => { + if (path.endsWith(".html")) { + res.setHeader("Cache-Control", "max-age=0, no-transform"); + } else { + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + } + }, }), ); diff --git a/application/server/src/sequelize.ts b/application/server/src/sequelize.ts index 6663f8da54..0518c4e217 100644 --- a/application/server/src/sequelize.ts +++ b/application/server/src/sequelize.ts @@ -12,18 +12,60 @@ let _sequelize: Sequelize | null = null; export async function initializeSequelize() { const prevSequelize = _sequelize; _sequelize = null; - await prevSequelize?.close(); + if (prevSequelize) { + try { + await prevSequelize.close(); + } catch (e) { + console.error("Failed to close previous sequelize:", e); + } + } + + const tmpDir = path.resolve(os.tmpdir(), "./wsh-"); + try { + await fs.mkdir(tmpDir, { recursive: true }); + } catch (e) { + // Ignore if already exists + } const TEMP_PATH = path.resolve( - await fs.mkdtemp(path.resolve(os.tmpdir(), "./wsh-")), + await fs.mkdtemp(path.join(tmpDir, "db-")), "./database.sqlite", ); - await fs.copyFile(DATABASE_PATH, TEMP_PATH); + + try { + await fs.copyFile(DATABASE_PATH, TEMP_PATH); + } catch (e) { + console.error("Failed to copy database file:", e); + throw e; + } _sequelize = new Sequelize({ dialect: "sqlite", logging: false, storage: TEMP_PATH, }); + + try { + // Enable WAL mode for better performance + await _sequelize.query("PRAGMA journal_mode = WAL;"); + await _sequelize.query("PRAGMA synchronous = NORMAL;"); + await _sequelize.query("PRAGMA cache_size = -64000;"); // 64MB cache に増量 + await _sequelize.query("PRAGMA mmap_size = 268435456;"); // 256MB mmap + + // Add indexes + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_posts_id_desc ON Posts(id DESC);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_posts_userId ON Posts(userId);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_posts_createdAt ON Posts(createdAt);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_users_name ON Users(name);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_comments_postId ON Comments(postId);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_dm_conversationId ON DirectMessages(conversationId);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_dm_createdAt ON DirectMessages(createdAt);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_dmc_initiatorId ON DirectMessageConversations(initiatorId);"); + await _sequelize.query("CREATE INDEX IF NOT EXISTS idx_dmc_memberId ON DirectMessageConversations(memberId);"); + } catch (e) { + console.error("Failed to optimize SQLite or create indexes:", e); + // Continue even if optimization fails + } + initModels(_sequelize); } diff --git a/application/server/src/utils/express_websocket_support.ts b/application/server/src/utils/express_websocket_support.ts index 9fb9d7ec58..584859af8a 100644 --- a/application/server/src/utils/express_websocket_support.ts +++ b/application/server/src/utils/express_websocket_support.ts @@ -1,5 +1,4 @@ import http from "node:http"; -import { parse, format } from "node:url"; import Express, { Router } from "express"; import { WebSocketServer } from "ws"; @@ -11,10 +10,9 @@ Router.prototype.ws = Express.application.ws = function ( ...handlers: Express.RequestHandler[] ) { // パスに `/ws` を付与してWebSocket用のルートを作成する - const wsPath = format({ - ...parse(path), - pathname: `${parse(path).pathname}/ws`, - }); + const url = new URL(path, "http://localhost"); + url.pathname = `${url.pathname}/ws`; + const wsPath = url.pathname + url.search; this.get(wsPath, ...handlers); }; @@ -29,10 +27,9 @@ Express.application.listen = function (this: Express.Application, ...args: unkno const req: Express.Request = Object.setPrototypeOf(rawReq, Express.request); // パスに `/ws` を付与して WebSocket 用のルートに変換する - req.url = format({ - ...parse(req.url), - pathname: `${parse(req.url).pathname}/ws`, - }); + const url = new URL(req.url, "http://localhost"); + url.pathname = `${url.pathname}/ws`; + req.url = url.pathname + url.search; const wss = mapping.get(req.path) ?? new WebSocketServer({ noServer: true }); mapping.set(req.path, wss); diff --git a/application/server/src/utils/video_initializer.ts b/application/server/src/utils/video_initializer.ts new file mode 100644 index 0000000000..610844b3c5 --- /dev/null +++ b/application/server/src/utils/video_initializer.ts @@ -0,0 +1,80 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import ffmpegPath from "ffmpeg-static"; +import fluentFfmpeg from "fluent-ffmpeg"; +import { UPLOAD_PATH, PUBLIC_PATH } from "../paths.js"; + +if (ffmpegPath) { + fluentFfmpeg.setFfmpegPath(ffmpegPath); +} + +async function convertGifToMp4(inputPath: string, outputPath: string, options: string[]) { + return new Promise((resolve, reject) => { + fluentFfmpeg(inputPath) + .outputOptions(options) + .toFormat("mp4") + .on("end", () => resolve()) + .on("error", (err) => reject(err)) + .save(outputPath); + }); +} + +async function processDirectory(dir: string) { + const movieDir = path.resolve(dir, "movies"); + try { + await fs.access(movieDir); + } catch { + return; // ディレクトリがなければスキップ + } + + const files = await fs.readdir(movieDir); + for (const file of files) { + if (!file.endsWith(".gif")) continue; + + const id = path.basename(file, ".gif"); + const gifPath = path.join(movieDir, file); + const mp4Path = path.join(movieDir, `${id}.mp4`); + const previewPath = path.join(movieDir, `${id}_preview.mp4`); + + // フル解像度 MP4 がない場合に生成(解像度維持・700KB以下ターゲット設定) + try { + await fs.access(mp4Path); + } catch { + console.log(`Generating full-res ultra-optimized MP4 for ${id}...`); + await convertGifToMp4(gifPath, mp4Path, [ + "-c:v libx264", + "-pix_fmt yuv420p", + "-vf scale=trunc(iw/2)*2:trunc(ih/2)*2", + "-crf 40", + "-maxrate 800k", + "-bufsize 1600k", + "-preset slower", + "-movflags +faststart" + ]).catch(err => console.error(`Failed to generate MP4 for ${id}:`, err)); + } + + + // プレビュー用 MP4 がない場合に生成 + try { + await fs.access(previewPath); + } catch { + console.log(`Generating missing preview for ${id}...`); + await convertGifToMp4(gifPath, previewPath, [ + "-t 5", + "-c:v libx264", + "-pix_fmt yuv420p", + "-vf scale=256:-2", + "-crf 32", + "-preset fast", + "-movflags +faststart" + ]).catch(err => console.error(`Failed to generate preview for ${id}:`, err)); + } + } +} + +export async function initializeVideos() { + console.log("Checking for missing video files..."); + await processDirectory(PUBLIC_PATH); + await processDirectory(UPLOAD_PATH); + console.log("Video initialization complete."); +}