diff --git a/Docs/API/VocaDBTooltips.js b/Docs/API/VocaDBTooltips.js deleted file mode 100644 index 33d0fe95c3..0000000000 --- a/Docs/API/VocaDBTooltips.js +++ /dev/null @@ -1,34 +0,0 @@ -// DEPRECATED. This script is not being maintained and may be a security issue. USE IT AT YOUR OWN RISK. -// Please post a comment at https://github.com/VocaDB/vocadb/issues/892 if you have need for an updated tooltip script. - -// Add qtip tooltip to all VocaDB album and artist links on page -jQuery(document).ready(function () { - jQuery("a[href^='http://vocadb.net/'], a[href^='https://vocadb.net/']").each(function (_, elem) { - var regex = /http(s)?:\/\/vocadb\.net\/((Artist|Album|Song)\/Details|(Ar|Al|E|S|T))\/(\d+)/g; - var href = jQuery(elem).attr("href"); - var match = regex.test(href); - if (match) { - jQuery(elem).qtip({ - content: { - text: 'Loading...', - ajax: { - url: 'https://vocadb.net/Ext/EntryToolTip', - type: 'GET', - dataType: 'jsonp', - data: { url: href }, - success: function (data) { - this.set('content.text', data); - } - } - }, - position: { - container: jQuery('#container') - }, - style: { - classes: "tooltip-wide" - } - }); - } - }); -}); -//# sourceMappingURL=VocaDBTooltips.js.map \ No newline at end of file diff --git a/Docs/API/VocaDBTooltips.js.map b/Docs/API/VocaDBTooltips.js.map deleted file mode 100644 index d42d3de76f..0000000000 --- a/Docs/API/VocaDBTooltips.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"VocaDBTooltips.js","sourceRoot":"","sources":["VocaDBTooltips.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAQhE,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC;IACtB,MAAM,CAAC,+DAA+D,CAAC,CAAC,IAAI,CAAC,UAAC,CAAC,EAAE,IAAiB;QAEjG,IAAI,KAAK,GAAG,gFAAgF,CAAC;QAC7F,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE7B,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YAEX,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;gBACjB,OAAO,EAAE;oBACR,IAAI,EAAE,YAAY;oBAClB,IAAI,EAAE;wBACL,GAAG,EAAE,qCAAqC;wBAC1C,IAAI,EAAE,KAAK;wBACX,QAAQ,EAAE,OAAO;wBACjB,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE;wBACnB,OAAO,EAAE,UAAU,IAAY;4BAC9B,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;wBAChC,CAAC;qBACD;iBACD;gBACD,QAAQ,EAAE;oBACT,SAAS,EAAE,MAAM,CAAC,YAAY,CAAC;iBAC/B;gBACD,KAAK,EAAE;oBACN,OAAO,EAAE,cAAc;iBACvB;aACD,CAAC,CAAC;QAEJ,CAAC;IAEF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/Docs/API/VocaDBTooltips.ts b/Docs/API/VocaDBTooltips.ts deleted file mode 100644 index 6a76d443d4..0000000000 --- a/Docs/API/VocaDBTooltips.ts +++ /dev/null @@ -1,45 +0,0 @@ -// DEPRECATED. This script is not being maintained and may be a security issue. USE IT AT YOUR OWN RISK. -// Please post a comment at https://github.com/VocaDB/vocadb/issues/892 if you have need for an updated tooltip script. - -// Add qtip tooltip to all VocaDB album and artist links on page - -interface jQueryInt { - (doc: HTMLDocument | HTMLElement | string): any; -} - -declare var jQuery: jQueryInt; - -jQuery(document).ready(() => { - jQuery("a[href^='http://vocadb.net/'], a[href^='https://vocadb.net/']").each((_, elem: HTMLElement) => { - - var regex = /http(s)?:\/\/vocadb\.net\/((Artist|Album|Song)\/Details|(Ar|Al|E|S|T))\/(\d+)/g; - var href = jQuery(elem).attr("href"); - var match = regex.test(href); - - if (match) { - - jQuery(elem).qtip({ - content: { - text: 'Loading...', - ajax: { - url: 'https://vocadb.net/Ext/EntryToolTip', - type: 'GET', - dataType: 'jsonp', - data: { url: href }, - success: function (data: string) { - this.set('content.text', data); - } - } - }, - position: { - container: jQuery('#container') - }, - style: { - classes: "tooltip-wide" - } - }); - - } - - }); -}); \ No newline at end of file diff --git a/VocaDbWeb/Controllers/AlbumController.cs b/VocaDbWeb/Controllers/AlbumController.cs index ffae2d881d..4677dd1122 100644 --- a/VocaDbWeb/Controllers/AlbumController.cs +++ b/VocaDbWeb/Controllers/AlbumController.cs @@ -107,24 +107,6 @@ public ActionResult FindDuplicate(string term1, string term2, string term3) return LowercaseJson(contracts); } - public ActionResult PopupContent(int id = InvalidId) - { - if (id == InvalidId) - return NotFound(); - - var album = Service.GetAlbum(id); - return PartialView("AlbumPopupContent", album); - } - - public ActionResult PopupWithCoverContent(int id = InvalidId) - { - if (id == InvalidId) - return NotFound(); - - var album = Service.GetAlbum(id); - return PartialView("AlbumWithCoverPopupContent", album); - } - // // GET: /Album/Details/5 diff --git a/VocaDbWeb/Controllers/Api/EntryApiController.cs b/VocaDbWeb/Controllers/Api/EntryApiController.cs index 4cae736310..74ea7933ce 100644 --- a/VocaDbWeb/Controllers/Api/EntryApiController.cs +++ b/VocaDbWeb/Controllers/Api/EntryApiController.cs @@ -101,36 +101,5 @@ public PartialFindResult GetList( /// List of entry names. [HttpGet("names")] public string[] GetNames(string query = "", NameMatchMode nameMatchMode = NameMatchMode.Auto, int maxResults = 10) => _otherService.FindNames(SearchTextQuery.Create(query, nameMatchMode), maxResults); - - [ApiExplorerSettings(IgnoreApi = true)] - [HttpGet("tooltip")] - public async Task> GetToolTip(string url) - { - if (string.IsNullOrWhiteSpace(url)) - return BadRequest("URL must be specified"); - - var entryId = _entryUrlParser.Parse(url, allowRelative: true); - - if (entryId.IsEmpty) - return BadRequest("Invalid URL"); - - var data = string.Empty; - var id = entryId.Id; - - switch (entryId.EntryType) - { - case EntryType.Album: - data = await _viewRenderService.RenderToStringAsync("AlbumWithCoverPopupContent", _albumService.GetAlbum(id)); - break; - case EntryType.Artist: - data = await _viewRenderService.RenderToStringAsync("ArtistPopupContent", _artistService.GetArtist(id)); - break; - case EntryType.Song: - data = await _viewRenderService.RenderToStringAsync("SongPopupContent", _songQueries.GetSong(id)); - break; - } - - return data; - } } -} \ No newline at end of file +} diff --git a/VocaDbWeb/Controllers/ArtistController.cs b/VocaDbWeb/Controllers/ArtistController.cs index 66abcdd7c9..054431b7d1 100644 --- a/VocaDbWeb/Controllers/ArtistController.cs +++ b/VocaDbWeb/Controllers/ArtistController.cs @@ -162,15 +162,6 @@ public ActionResult PictureThumb(int id = InvalidId) return Picture(artist); } - public ActionResult PopupContent(int id = InvalidId) - { - if (id == InvalidId) - return NoId(); - - var artist = Service.GetArtist(id); - return PartialView("ArtistPopupContent", artist); - } - [Authorize] public ActionResult Create() { diff --git a/VocaDbWeb/Controllers/EventController.cs b/VocaDbWeb/Controllers/EventController.cs index 1199c2b67c..db61eb42a7 100644 --- a/VocaDbWeb/Controllers/EventController.cs +++ b/VocaDbWeb/Controllers/EventController.cs @@ -164,18 +164,6 @@ public ActionResult ManageTagUsages(int id) return View(releaseEvent); } - [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 3600, VaryByQueryKeys = new[] { "*" })] - public ActionResult PopupContent( - int id = InvalidId, - string culture = InterfaceLanguage.DefaultCultureCode) - { - if (id == InvalidId) - return NotFound(); - - var releaseEvent = _queries.Load(id, ReleaseEventOptionalFields.AdditionalNames | ReleaseEventOptionalFields.MainPicture | ReleaseEventOptionalFields.Series); - return PartialView("_EventPopupContent", releaseEvent); - } - [Authorize] public ActionResult RemoveTagUsage(long id) { diff --git a/VocaDbWeb/Controllers/ExtController.cs b/VocaDbWeb/Controllers/ExtController.cs index 76e19eb04f..80861fb963 100644 --- a/VocaDbWeb/Controllers/ExtController.cs +++ b/VocaDbWeb/Controllers/ExtController.cs @@ -104,44 +104,6 @@ public ActionResult EmbedSong(int songId = InvalidId, int pvId = InvalidId, int? return PartialView(viewModel); } - public async Task EntryToolTip(string url, string callback) - { - if (string.IsNullOrWhiteSpace(url)) - return HttpStatusCodeResult(HttpStatusCode.BadRequest, "URL must be specified"); - - var entryId = _entryUrlParser.Parse(url, allowRelative: true); - - if (entryId.IsEmpty) - { - return HttpStatusCodeResult(HttpStatusCode.BadRequest, "Invalid URL"); - } - - var data = string.Empty; - var id = entryId.Id; - - switch (entryId.EntryType) - { - case EntryType.Album: - data = await RenderPartialViewToStringAsync("AlbumWithCoverPopupContent", _albumService.GetAlbum(id)); - break; - case EntryType.Artist: - data = await RenderPartialViewToStringAsync("ArtistPopupContent", _artistService.GetArtist(id)); - break; - case EntryType.ReleaseEvent: - data = await RenderPartialViewToStringAsync("_EventPopupContent", _eventQueries.GetOne(id, ContentLanguagePreference.Default, ReleaseEventOptionalFields.AdditionalNames | ReleaseEventOptionalFields.MainPicture | ReleaseEventOptionalFields.Series)); - break; - case EntryType.Song: - data = await RenderPartialViewToStringAsync("SongPopupContent", _songService.GetSong(id)); - break; - case EntryType.Tag: - data = await RenderPartialViewToStringAsync("_TagPopupContent", _tagQueries.LoadTag(id, t => - new TagForApiContract(t, _entryThumbPersister, ContentLanguagePreference.Default, TagOptionalFields.AdditionalNames | TagOptionalFields.MainPicture))); - break; - } - - return Json(data, callback); - } - public async Task OEmbed(string url, int maxwidth = 570, int maxheight = 400, DataFormat format = DataFormat.Json, bool responsiveWrapper = false) { if (string.IsNullOrEmpty(url)) diff --git a/VocaDbWeb/Controllers/SongController.cs b/VocaDbWeb/Controllers/SongController.cs index 1a1b6d4568..327171420d 100644 --- a/VocaDbWeb/Controllers/SongController.cs +++ b/VocaDbWeb/Controllers/SongController.cs @@ -220,34 +220,6 @@ public ActionResult PostMedia(IFormFile file) return LowercaseJson(pv); } - [ResponseCache(Location = ResponseCacheLocation.Client, Duration = 3600)] - public ActionResult PopupContent(int id = InvalidId) - { - if (id == InvalidId) - return NotFound(); - - var song = _queries.GetSong(id); - return PartialView("SongPopupContent", song); - } - - [ResponseCache(Location = ResponseCacheLocation.Client, Duration = 3600)] - public async Task PopupContentWithVote(int id = InvalidId, int? version = null, string callback = null) - { - if (id == InvalidId) - return NotFound(); - - var song = _queries.GetSongWithPVAndVote(id, false, includePVs: false); - - if (string.IsNullOrEmpty(callback)) - { - return PartialView("_SongWithVotePopupContent", song); - } - else - { - return Json(await RenderPartialViewToStringAsync("_SongWithVotePopupContent", song), callback); - } - } - public async Task Feed(IndexRouteParams indexParams) { WebHelper.VerifyUserAgent(Request); diff --git a/VocaDbWeb/Controllers/TagController.cs b/VocaDbWeb/Controllers/TagController.cs index 3184a9a53a..855d8273c0 100644 --- a/VocaDbWeb/Controllers/TagController.cs +++ b/VocaDbWeb/Controllers/TagController.cs @@ -192,20 +192,6 @@ public ActionResult Merge(int id, int? targetTagId) return RedirectToAction("Edit", new { id = targetTagId.Value }); } - [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 3600, VaryByQueryKeys = new[] { "*" })] - public ActionResult PopupContent( - int id = InvalidId, - ContentLanguagePreference lang = ContentLanguagePreference.Default, - string culture = InterfaceLanguage.DefaultCultureCode) - { - if (id == InvalidId) - return NotFound(); - - var tag = _queries.LoadTag(id, t => new TagForApiContract(t, _entryThumbPersister, - lang, TagOptionalFields.AdditionalNames | TagOptionalFields.Description | TagOptionalFields.MainPicture)); - return PartialView("_TagPopupContent", tag); - } - public ActionResult UpdateVersionVisibility(int archivedVersionId, bool hidden) { _queries.UpdateVersionVisibility(archivedVersionId, hidden); diff --git a/VocaDbWeb/Controllers/UserController.cs b/VocaDbWeb/Controllers/UserController.cs index bb25e87d40..d1ed1eb07d 100644 --- a/VocaDbWeb/Controllers/UserController.cs +++ b/VocaDbWeb/Controllers/UserController.cs @@ -291,13 +291,6 @@ public PartialViewResult OwnedArtistForUserEditRow(int artistId) return PartialView(ownedArtist); } - [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 3600, VaryByQueryKeys = new[] { "*" })] - public PartialViewResult PopupContent(int id, string culture = InterfaceLanguage.DefaultCultureCode) - { - var user = Service.GetUser(id); - return PartialView("_UserPopupContent", user); - } - #nullable enable public ActionResult Profile(string id, int? artistId = null, bool? childVoicebanks = null) { diff --git a/VocaDbWeb/Scripts/Bootstrap/Overlay.tsx b/VocaDbWeb/Scripts/Bootstrap/Overlay.tsx new file mode 100644 index 0000000000..096212652d --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/Overlay.tsx @@ -0,0 +1,138 @@ +// Code from: https://github.com/react-bootstrap/react-bootstrap/blob/33f037ba1e9870463f1bd33a4fe66b8e2a7586f6/src/Overlay.tsx. +import safeFindDOMNode from '@/Bootstrap/safeFindDOMNode'; +import { Placement, PopperRef, RootCloseEvent } from '@/Bootstrap/types'; +import useOverlayOffset from '@/Bootstrap/useOverlayOffset'; +import useCallbackRef from '@restart/hooks/useCallbackRef'; +import useEventCallback from '@restart/hooks/useEventCallback'; +import useIsomorphicEffect from '@restart/hooks/useIsomorphicEffect'; +import useMergedRefs from '@restart/hooks/useMergedRefs'; +import BaseOverlay, { + OverlayProps as BaseOverlayProps, + OverlayArrowProps, +} from '@restart/ui/Overlay'; +import { State } from '@restart/ui/usePopper'; +import classNames from 'classnames'; +import * as React from 'react'; +import { useRef } from 'react'; + +export interface OverlayInjectedProps { + ref: React.RefCallback; + style: React.CSSProperties; + 'aria-labelledby'?: string; + + arrowProps: Partial; + + show: boolean; + placement: Placement | undefined; + popper: PopperRef; + [prop: string]: any; +} + +export type OverlayChildren = + | React.ReactElement + | ((injected: OverlayInjectedProps) => React.ReactNode); + +export interface OverlayProps + extends Omit { + children: OverlayChildren; + placement?: Placement; + rootCloseEvent?: RootCloseEvent; +} + +const defaultProps: Partial = { + rootClose: false, + show: false, + placement: 'top', +}; + +function wrapRefs(props: any, arrowProps: any): void { + const { ref } = props; + const { ref: aRef } = arrowProps; + + props.ref = + ref.__wrapped || (ref.__wrapped = (r: any): any => ref(safeFindDOMNode(r))); + arrowProps.ref = + aRef.__wrapped || + (aRef.__wrapped = (r: any): any => aRef(safeFindDOMNode(r))); +} + +const Overlay = React.forwardRef( + ({ children: overlay, popperConfig = {}, ...outerProps }, outerRef) => { + const popperRef = useRef>({}); + const [firstRenderedState, setFirstRenderedState] = useCallbackRef(); + const [ref, modifiers] = useOverlayOffset(outerProps.offset); + const mergedRef = useMergedRefs( + outerRef as React.MutableRefObject, + ref, + ); + + const handleFirstUpdate = useEventCallback((state) => { + setFirstRenderedState(state); + popperConfig?.onFirstUpdate?.(state); + }); + + useIsomorphicEffect(() => { + if (firstRenderedState) { + popperRef.current.scheduleUpdate?.(); + } + }, [firstRenderedState]); + + return ( + + {( + overlayProps, + { arrowProps, popper: popperObj, show }, + ): React.ReactNode => { + wrapRefs(overlayProps, arrowProps); + // Need to get placement from popper object, handling case when overlay is flipped using 'flip' prop + const updatedPlacement = popperObj?.placement; + const popper = Object.assign(popperRef.current, { + state: popperObj?.state, + scheduleUpdate: popperObj?.update, + placement: updatedPlacement, + outOfBoundaries: + popperObj?.state?.modifiersData.hide?.isReferenceHidden || false, + }); + + if (typeof overlay === 'function') + return overlay({ + ...overlayProps, + placement: updatedPlacement, + show, + ...(show && { className: 'show' }), + popper, + arrowProps, + }); + + return React.cloneElement(overlay as React.ReactElement, { + ...overlayProps, + placement: updatedPlacement, + arrowProps, + popper, + className: classNames( + (overlay as React.ReactElement).props.className, + show && 'show', + ), + style: { + ...(overlay as React.ReactElement).props.style, + ...overlayProps.style, + }, + }); + }} + + ); + }, +); + +Overlay.displayName = 'Overlay'; +Overlay.defaultProps = defaultProps; + +export default Overlay; diff --git a/VocaDbWeb/Scripts/Bootstrap/OverlayTrigger.tsx b/VocaDbWeb/Scripts/Bootstrap/OverlayTrigger.tsx new file mode 100644 index 0000000000..370fc95807 --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/OverlayTrigger.tsx @@ -0,0 +1,326 @@ +// Code from: https://github.com/react-bootstrap/react-bootstrap/blob/33f037ba1e9870463f1bd33a4fe66b8e2a7586f6/src/OverlayTrigger.tsx. +import Overlay, { OverlayChildren, OverlayProps } from '@/Bootstrap/Overlay'; +import safeFindDOMNode from '@/Bootstrap/safeFindDOMNode'; +import useMergedRefs from '@restart/hooks/useMergedRefs'; +import useTimeout from '@restart/hooks/useTimeout'; +import contains from 'dom-helpers/contains'; +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { cloneElement, useCallback, useRef } from 'react'; +import { useUncontrolledProp } from 'uncontrollable'; +import warning from 'warning'; + +export type OverlayTriggerType = 'hover' | 'click' | 'focus'; + +export type OverlayDelay = number | { show: number; hide: number }; + +export type OverlayInjectedProps = { + onFocus?: (...args: any[]) => any; +}; + +export type OverlayTriggerRenderProps = OverlayInjectedProps & { + ref: React.Ref; +}; + +export interface OverlayTriggerProps + extends Omit { + children: + | React.ReactElement + | ((props: OverlayTriggerRenderProps) => React.ReactNode); + trigger?: OverlayTriggerType | OverlayTriggerType[]; + delay?: OverlayDelay; + show?: boolean; + defaultShow?: boolean; + onToggle?: (nextShow: boolean) => void; + flip?: boolean; + overlay: OverlayChildren; + + target?: never; + onHide?: never; +} + +function normalizeDelay( + delay?: OverlayDelay, +): { show?: number; hide?: number } { + return delay && typeof delay === 'object' + ? delay + : { + show: delay, + hide: delay, + }; +} + +// Simple implementation of mouseEnter and mouseLeave. +// React's built version is broken: https://github.com/facebook/react/issues/4251 +// for cases when the trigger is disabled and mouseOut/Over can cause flicker +// moving from one child element to another. +function handleMouseOverOut( + // eslint-disable-next-line @typescript-eslint/no-shadow + handler: (...args: [React.MouseEvent, ...any[]]) => any, + args: [React.MouseEvent, ...any[]], + relatedNative: 'fromElement' | 'toElement', +): void { + const [e] = args; + const target = e.currentTarget; + const related = (e.relatedTarget || + e.nativeEvent[relatedNative as keyof Event]) as Element; + + if ((!related || related !== target) && !contains(target, related)) { + handler(...args); + } +} + +const triggerType = PropTypes.oneOf(['click', 'hover', 'focus']); + +const propTypes = { + children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, + + /** + * Specify which action or actions trigger Overlay visibility + * + * The `click` trigger ignores the configured `delay`. + * + * @type {'hover' | 'click' |'focus' | Array<'hover' | 'click' |'focus'>} + */ + trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]), + + /** + * A millisecond delay amount to show and hide the Overlay once triggered + */ + delay: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + show: PropTypes.number, + hide: PropTypes.number, + }), + ]), + + /** + * The visibility of the Overlay. `show` is a _controlled_ prop so should be paired + * with `onToggle` to avoid breaking user interactions. + * + * Manually toggling `show` does **not** wait for `delay` to change the visibility. + * + * @controllable onToggle + */ + show: PropTypes.bool, + + /** + * The initial visibility state of the Overlay. + */ + defaultShow: PropTypes.bool, + + /** + * A callback that fires when the user triggers a change in tooltip visibility. + * + * `onToggle` is called with the desired next `show`, and generally should be passed + * back to the `show` prop. `onToggle` fires _after_ the configured `delay` + * + * @controllable `show` + */ + onToggle: PropTypes.func, + + /** + The initial flip state of the Overlay. + */ + flip: PropTypes.bool, + + /** + * An element or text to overlay next to the target. + */ + overlay: PropTypes.oneOfType([PropTypes.func, PropTypes.element.isRequired]), + + /** + * A Popper.js config object passed to the the underlying popper instance. + */ + popperConfig: PropTypes.object, + + // Overridden props from ``. + /** + * @private + */ + target: PropTypes.oneOf([null]), + + /** + * @private + */ + onHide: PropTypes.oneOf([null]), + + /** + * The placement of the Overlay in relation to it's `target`. + */ + placement: PropTypes.oneOf([ + 'auto-start', + 'auto', + 'auto-end', + 'top-start', + 'top', + 'top-end', + 'right-start', + 'right', + 'right-end', + 'bottom-end', + 'bottom', + 'bottom-start', + 'left-end', + 'left', + 'left-start', + ]), +}; + +const defaultProps = { + defaultShow: false, + trigger: ['hover', 'focus'], +}; + +function OverlayTrigger({ + trigger, + overlay, + children, + popperConfig = {}, + + show: propsShow, + defaultShow = false, + onToggle, + + delay: propsDelay, + placement, + flip = placement && placement.indexOf('auto') !== -1, + ...props +}: OverlayTriggerProps): React.ReactElement { + const triggerNodeRef = useRef(null); + const mergedRef = useMergedRefs( + triggerNodeRef, + (children as any).ref, + ); + const timeout = useTimeout(); + const hoverStateRef = useRef(''); + + const [show, setShow] = useUncontrolledProp(propsShow, defaultShow, onToggle); + + const delay = normalizeDelay(propsDelay); + + const { onFocus, onBlur, onClick } = + typeof children !== 'function' + ? React.Children.only(children).props + : ({} as any); + + const attachRef = ( + r: React.ComponentClass | Element | null | undefined, + ): void => { + mergedRef(safeFindDOMNode(r)); + }; + + const handleShow = useCallback(() => { + timeout.clear(); + hoverStateRef.current = 'show'; + + if (!delay.show) { + setShow(true); + return; + } + + timeout.set(() => { + if (hoverStateRef.current === 'show') setShow(true); + }, delay.show); + }, [delay.show, setShow, timeout]); + + const handleHide = useCallback(() => { + timeout.clear(); + hoverStateRef.current = 'hide'; + + if (!delay.hide) { + setShow(false); + return; + } + + timeout.set(() => { + if (hoverStateRef.current === 'hide') setShow(false); + }, delay.hide); + }, [delay.hide, setShow, timeout]); + + const handleFocus = useCallback( + (...args: any[]) => { + handleShow(); + onFocus?.(...args); + }, + [handleShow, onFocus], + ); + + const handleBlur = useCallback( + (...args: any[]) => { + handleHide(); + onBlur?.(...args); + }, + [handleHide, onBlur], + ); + + const handleClick = useCallback( + (...args: any[]) => { + setShow(!show); + onClick?.(...args); + }, + [onClick, setShow, show], + ); + + const handleMouseOver = useCallback( + (...args: [React.MouseEvent, ...any[]]) => { + handleMouseOverOut(handleShow, args, 'fromElement'); + }, + [handleShow], + ); + + const handleMouseOut = useCallback( + (...args: [React.MouseEvent, ...any[]]) => { + handleMouseOverOut(handleHide, args, 'toElement'); + }, + [handleHide], + ); + + const triggers: string[] = trigger == null ? [] : [].concat(trigger as any); + const triggerProps: any = { + ref: attachRef, + }; + + if (triggers.indexOf('click') !== -1) { + triggerProps.onClick = handleClick; + } + + if (triggers.indexOf('focus') !== -1) { + triggerProps.onFocus = handleFocus; + triggerProps.onBlur = handleBlur; + } + + if (triggers.indexOf('hover') !== -1) { + warning( + triggers.length > 1, + '[react-bootstrap] Specifying only the `"hover"` trigger limits the visibility of the overlay to just mouse users. Consider also including the `"focus"` trigger so that touch and keyboard only users can see the overlay as well.', + ); + triggerProps.onMouseOver = handleMouseOver; + triggerProps.onMouseOut = handleMouseOut; + } + + return ( + <> + {typeof children === 'function' + ? children(triggerProps) + : cloneElement(children, triggerProps)} + + {overlay} + + + ); +} + +OverlayTrigger.propTypes = propTypes; +OverlayTrigger.defaultProps = defaultProps; + +export default OverlayTrigger; diff --git a/VocaDbWeb/Scripts/Bootstrap/safeFindDOMNode.ts b/VocaDbWeb/Scripts/Bootstrap/safeFindDOMNode.ts new file mode 100644 index 0000000000..aea18b1b08 --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/safeFindDOMNode.ts @@ -0,0 +1,11 @@ +// Code from: https://github.com/react-bootstrap/react-bootstrap/blob/33f037ba1e9870463f1bd33a4fe66b8e2a7586f6/src/safeFindDOMNode.ts. +import ReactDOM from 'react-dom'; + +export default function safeFindDOMNode( + componentOrElement: React.ComponentClass | Element | null | undefined, +): Element | Text | null { + if (componentOrElement && 'setState' in componentOrElement) { + return ReactDOM.findDOMNode(componentOrElement); + } + return (componentOrElement ?? null) as Element | Text | null; +} diff --git a/VocaDbWeb/Scripts/Bootstrap/types.tsx b/VocaDbWeb/Scripts/Bootstrap/types.tsx index 700e4cf54d..88467ef0a7 100644 --- a/VocaDbWeb/Scripts/Bootstrap/types.tsx +++ b/VocaDbWeb/Scripts/Bootstrap/types.tsx @@ -1,4 +1,5 @@ // Code from: https://github.com/react-bootstrap/react-bootstrap/blob/8a7e095e8032fdeac4fd1fdb41e6dfb452ae4494/src/types.tsx +import { State } from '@restart/ui/usePopper'; export type Variant = | 'primary' @@ -11,3 +12,14 @@ export type Variant = export type ButtonVariant = Variant; export type EventKey = string | number; + +export type Placement = import('@restart/ui/usePopper').Placement; + +export type RootCloseEvent = 'click' | 'mousedown'; + +export interface PopperRef { + state: State | undefined; + outOfBoundaries: boolean; + placement: Placement | undefined; + scheduleUpdate?: () => void; +} diff --git a/VocaDbWeb/Scripts/Bootstrap/useOverlayOffset.tsx b/VocaDbWeb/Scripts/Bootstrap/useOverlayOffset.tsx new file mode 100644 index 0000000000..2ed09ac2c0 --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/useOverlayOffset.tsx @@ -0,0 +1,33 @@ +import { useBootstrapPrefix } from '@/Bootstrap/ThemeProvider'; +import { Offset, Options } from '@restart/ui/usePopper'; +import hasClass from 'dom-helpers/hasClass'; +import { useMemo, useRef } from 'react'; + +// This is meant for internal use. +// This applies a custom offset to the overlay if it's a popover. +export default function useOverlayOffset( + customOffset?: Offset, +): [React.RefObject, Options['modifiers']] { + const overlayRef = useRef(null); + const popoverClass = useBootstrapPrefix(undefined, 'popover'); + + const offset = useMemo( + () => ({ + name: 'offset', + options: { + offset: (): Offset => { + if ( + overlayRef.current && + hasClass(overlayRef.current, popoverClass) + ) { + return customOffset || [0, 8]; + } + return customOffset || [0, 0]; + }, + }, + }), + [customOffset, popoverClass], + ); + + return [overlayRef, [offset]]; +} diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx index 9a143398a6..0571ac9f7b 100644 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -1,350 +1,417 @@ -import { BsPrefixRefForwardingComponent } from '@/Bootstrap/helpers'; +import OverlayTrigger from '@/Bootstrap/OverlayTrigger'; +import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; +import { AlbumWithCoverPopupContent } from '@/Components/Shared/AlbumWithCoverPopupContent'; +import { ArtistPopupContent } from '@/Components/Shared/ArtistPopupContent'; +import { EventPopupContent } from '@/Components/Shared/EventPopupContent'; +import { SongWithVotePopupContent } from '@/Components/Shared/SongWithVotePopupContent'; +import { TagPopupContent } from '@/Components/Shared/TagPopupContent'; +import { UserPopupContent } from '@/Components/Shared/UserPopupContent'; +import { AlbumContract } from '@/DataContracts/Album/AlbumContract'; +import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; import { EntryRefContract } from '@/DataContracts/EntryRefContract'; -import { functions } from '@/Shared/GlobalFunctions'; -import $ from 'jquery'; -import _ from 'lodash'; -import 'qtip2'; +import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; +import { SongWithPVAndVoteContract } from '@/DataContracts/Song/SongWithPVAndVoteContract'; +import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; +import { UserApiContract } from '@/DataContracts/User/UserApiContract'; +import { EntryType } from '@/Models/EntryType'; +import { QTipToolTip } from '@/QTip/QTipToolTip'; +import { + AlbumOptionalField, + AlbumRepository, +} from '@/Repositories/AlbumRepository'; +import { + ArtistOptionalField, + ArtistRepository, +} from '@/Repositories/ArtistRepository'; +import { + ReleaseEventOptionalField, + ReleaseEventRepository, +} from '@/Repositories/ReleaseEventRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; +import { TagOptionalField, TagRepository } from '@/Repositories/TagRepository'; +import { + UserOptionalField, + UserRepository, +} from '@/Repositories/UserRepository'; +import { HttpClient } from '@/Shared/HttpClient'; +import { UrlMapper } from '@/Shared/UrlMapper'; import React from 'react'; -const allowedDomains = [ - 'http://vocadb.net', - 'https://vocadb.net', - 'http://utaitedb.net', - 'https://utaitedb.net', - 'https://touhoudb.com', -]; +const httpClient = new HttpClient(); +const urlMapper = new UrlMapper(vdb.values.baseAddress); -interface ToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - relativeUrl: string; +const albumRepo = new AlbumRepository(httpClient, vdb.values.baseAddress); +const artistRepo = new ArtistRepository(httpClient, vdb.values.baseAddress); +const eventRepo = new ReleaseEventRepository(httpClient, urlMapper); +const songRepo = new SongRepository(httpClient, vdb.values.baseAddress); +const tagRepo = new TagRepository(httpClient, vdb.values.baseAddress); +const userRepo = new UserRepository(httpClient, urlMapper); + +interface AlbumToolTipProps { id: number; - params?: any; - foreignDomain?: string; + children?: React.ReactNode; + withCover?: boolean; } -const ToolTip = React.forwardRef( - ( - { - as: Component = 'div', - children, - relativeUrl, - id, - params, - foreignDomain, - ...props - }: ToolTipProps, - ref, - ): React.ReactElement => { - const el = React.useRef(undefined!); - React.useImperativeHandle(ref, () => el.current); +export const AlbumToolTip = React.memo( + ({ id, children, withCover }: AlbumToolTipProps): React.ReactElement => { + const [show, setShow] = React.useState(false); + + const [album, setAlbum] = React.useState(); React.useEffect(() => { - const url = - foreignDomain && - allowedDomains.some((domain) => - foreignDomain.toLocaleLowerCase().includes(domain), - ) - ? functions.mergeUrls(foreignDomain, relativeUrl) - : functions.mapAbsoluteUrl(relativeUrl); - const data = _.assign({ id: id }, params); - - $(el.current).qtip({ - content: { - text: 'Loading...' /* TODO: localize */, - ajax: { - url: url, - type: 'GET', - data: data, - dataType: foreignDomain ? 'jsonp' : undefined, - }, - }, - position: { - viewport: $(window), - }, - style: { - classes: 'tooltip-wide', - }, - }); - - return (): void => { - $('.qtip').remove(); - }; - }); + if (album) return; - return ( - - {children} - - ); - }, -); + if (!show) return; -interface AlbumToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - id: number; -} + albumRepo + .getOneWithComponents({ + id: id, + fields: [ + AlbumOptionalField.AdditionalNames, + AlbumOptionalField.MainPicture, + ], + lang: vdb.values.languagePreference, + }) + .then((album) => setAlbum(album)); + }, [album, show, id]); -export const AlbumToolTip: BsPrefixRefForwardingComponent< - 'div', - AlbumToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: AlbumToolTipProps, - ref, - ): React.ReactElement => { return ( - + {withCover ? ( + + ) : ( + + )} + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} > - {children} - + {children} + ); }, ); interface ArtistToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; id: number; + children?: React.ReactNode; } -export const ArtistToolTip: BsPrefixRefForwardingComponent< - 'div', - ArtistToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: ArtistToolTipProps, - ref, - ): React.ReactElement => { +export const ArtistToolTip = React.memo( + ({ id, children }: ArtistToolTipProps): React.ReactElement => { + const [show, setShow] = React.useState(false); + + const [artist, setArtist] = React.useState(); + + React.useEffect(() => { + if (artist) return; + + if (!show) return; + + artistRepo + .getOneWithComponents({ + id: id, + fields: [ + ArtistOptionalField.AdditionalNames, + ArtistOptionalField.MainPicture, + ], + lang: vdb.values.languagePreference, + }) + .then((artist) => setArtist(artist)); + }, [artist, show, id]); + return ( - + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} > - {children} - + {children} + ); }, ); interface EventToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; id: number; + children?: React.ReactNode; } -export const EventToolTip: BsPrefixRefForwardingComponent< - 'div', - EventToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: EventToolTipProps, - ref, - ): React.ReactElement => { - const culture = vdb.values.uiCulture || undefined; +export const EventToolTip = React.memo( + ({ id, children }: EventToolTipProps): React.ReactElement => { + const [show, setShow] = React.useState(false); + + const [event, setEvent] = React.useState(); + + React.useEffect(() => { + if (event) return; + + if (!show) return; + + eventRepo + .getOne({ + id: id, + fields: [ + ReleaseEventOptionalField.AdditionalNames, + ReleaseEventOptionalField.Description, + ReleaseEventOptionalField.MainPicture, + ReleaseEventOptionalField.Series, + ], + }) + .then((event) => setEvent(event)); + }, [event, show, id]); return ( - + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} > - {children} - + {children} + ); }, ); +const allowedDomains = [ + 'http://vocadb.net', + 'https://vocadb.net', + 'http://utaitedb.net', + 'https://utaitedb.net', + 'https://touhoudb.com', +]; + interface SongToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; id: number; - toolTipDomain?: string; - version?: number; + children?: React.ReactNode; + foreignDomain?: string; } -export const SongToolTip: BsPrefixRefForwardingComponent< - 'div', - SongToolTipProps -> = React.forwardRef( - ( - { as, children, id, toolTipDomain, version, ...props }: SongToolTipProps, - ref, - ): React.ReactElement => { +export const SongToolTip = React.memo( + ({ id, children, foreignDomain }: SongToolTipProps): React.ReactElement => { + const [show, setShow] = React.useState(false); + + const [song, setSong] = React.useState(); + + React.useEffect(() => { + if (song) return; + + if (!show) return; + + const baseUrl = + foreignDomain && + allowedDomains.some((domain) => + foreignDomain.toLocaleLowerCase().includes(domain), + ) + ? foreignDomain + : undefined; + + songRepo + .getOneWithComponents({ + baseUrl: baseUrl, + id: id, + fields: [ + SongOptionalField.AdditionalNames, + SongOptionalField.ThumbUrl, + ], + lang: vdb.values.languagePreference, + }) + .then(async (song) => { + const vote = vdb.values.loggedUser + ? await userRepo.getSongRating({ + userId: vdb.values.loggedUser.id, + songId: song.id, + }) + : 'Nothing'; + + setSong({ ...song, pvs: [], vote: vote }); + }); + }, [song, show, id, foreignDomain]); + return ( - + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} > - {children} - + {children} + ); }, ); interface TagToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; id: number; + children?: React.ReactNode; } -export const TagToolTip: BsPrefixRefForwardingComponent< - 'div', - TagToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: TagToolTipProps, - ref, - ): React.ReactElement => { - const culture = vdb.values.uiCulture || undefined; - const lang = vdb.values.languagePreference; +export const TagToolTip = React.memo( + ({ id, children }: TagToolTipProps): React.ReactElement => { + const [show, setShow] = React.useState(false); + + const [tag, setTag] = React.useState(); + + React.useEffect(() => { + if (tag) return; + + if (!show) return; + + tagRepo + .getById({ + id: id, + fields: [ + TagOptionalField.AdditionalNames, + TagOptionalField.Description, + TagOptionalField.MainPicture, + ], + lang: vdb.values.languagePreference, + }) + .then((tag) => setTag(tag)); + }, [tag, show, id]); return ( - + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} > - {children} - + {children} + ); }, ); interface UserToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; id: number; + children?: React.ReactNode; } -export const UserToolTip: BsPrefixRefForwardingComponent< - 'div', - UserToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: UserToolTipProps, - ref, - ): React.ReactElement => { - var culture = vdb.values.uiCulture || undefined; +export const UserToolTip = React.memo( + ({ id, children }: UserToolTipProps): React.ReactElement => { + const [show, setShow] = React.useState(false); + + const [user, setUser] = React.useState(); + + React.useEffect(() => { + if (user) return; + + if (!show) return; + + userRepo + .getOne({ id: id, fields: [UserOptionalField.MainPicture] }) + .then((user) => setUser(user)); + }, [user, show, id]); return ( - + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} > - {children} - + {children} + ); }, ); interface EntryToolTipProps { - as?: React.ElementType; + entry: EntryRefContract; children?: React.ReactNode; - value: EntryRefContract; } -export const EntryToolTip: BsPrefixRefForwardingComponent< - 'div', - EntryToolTipProps -> = React.forwardRef( - ( - { as, children, value, ...props }: EntryToolTipProps, - ref, - ): React.ReactElement => { - switch (value.entryType) { - case 'Album' /* TODO: enum */: - return ( - - ); - - case 'Artist' /* TODO: enum */: - return ( - - ); - - case 'ReleaseEvent' /* TODO: enum */: - return ( - - ); - - case 'Song' /* TODO: enum */: - return ( - - ); - - case 'Tag' /* TODO: enum */: - return ( - - ); - - case 'User' /* TODO: enum */: - return ( - - ); - - default: - return <>; - } - }, -); +export const EntryToolTip = ({ + entry, + children, +}: EntryToolTipProps): React.ReactElement => { + switch (entry.entryType) { + case EntryType[EntryType.Album]: + return {children}; + + case EntryType[EntryType.Artist]: + return {children}; + + case EntryType[EntryType.ReleaseEvent]: + return {children}; + + case EntryType[EntryType.Song]: + return {children}; + + case EntryType[EntryType.Tag]: + return {children}; + + case EntryType[EntryType.User]: + return {children}; + + default: + return <>; + } +}; diff --git a/VocaDbWeb/Scripts/Components/Shared/AlbumPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/AlbumPopupContent.tsx new file mode 100644 index 0000000000..6a12157839 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/AlbumPopupContent.tsx @@ -0,0 +1,51 @@ +import { StarsMetaSpan } from '@/Components/Shared/Partials/Shared/StarsMetaSpan'; +import { AlbumContract } from '@/DataContracts/Album/AlbumContract'; +import { AlbumType } from '@/Models/Albums/AlbumType'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface AlbumPopupContentProps { + album: AlbumContract; +} + +export const AlbumPopupContent = React.memo( + ({ album }: AlbumPopupContentProps): React.ReactElement => { + const { t } = useTranslation([ + 'HelperRes', + 'ViewRes.Album', + 'VocaDb.Model.Resources.Albums', + ]); + + return ( + <> +

+ {album.name} + {album.additionalNames && ( + <> +
+ {album.additionalNames} + + )} +

+

+ {album.artistString} +
+ {album.discType !== AlbumType.Unknown && + t(`VocaDb.Model.Resources.Albums:DiscTypeNames.${album.discType}`)} +

+ {!album.releaseDate.isEmpty && ( +

+ {t('HelperRes:AlbumHelpers.Released')} {album.releaseDate.formatted} + {album.releaseEvent && <> ({album.releaseEvent.name})} +

+ )} + {album.ratingCount > 0 && ( + <> + ( + {album.ratingCount} {t('ViewRes.Album:Details.Ratings')}) + + )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/AlbumWithCoverPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/AlbumWithCoverPopupContent.tsx new file mode 100644 index 0000000000..7fa43e3b0f --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/AlbumWithCoverPopupContent.tsx @@ -0,0 +1,26 @@ +import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; +import { AlbumContract } from '@/DataContracts/Album/AlbumContract'; +import { UrlHelper } from '@/Helpers/UrlHelper'; +import { ImageSize } from '@/Models/Images/ImageSize'; +import React from 'react'; + +interface AlbumWithCoverPopupContentProps { + album: AlbumContract; +} + +export const AlbumWithCoverPopupContent = ({ + album, +}: AlbumWithCoverPopupContentProps): React.ReactElement => { + return ( + <> + Cover +
+
+ + + ); +}; diff --git a/VocaDbWeb/Scripts/Components/Shared/ArtistPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/ArtistPopupContent.tsx new file mode 100644 index 0000000000..38806162aa --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/ArtistPopupContent.tsx @@ -0,0 +1,50 @@ +import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; +import { UrlHelper } from '@/Helpers/UrlHelper'; +import { ImageSize } from '@/Models/Images/ImageSize'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ArtistPopupContentProps { + artist: ArtistContract; +} + +export const ArtistPopupContent = React.memo( + ({ artist }: ArtistPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['ViewRes.Artist', 'VocaDb.Model.Resources']); + + return ( + <> +
+ Thumb +
+ +

+ {artist.name} + + {artist.additionalNames && ( + <> +
+ {artist.additionalNames} + + )} +

+ +

+ {t(`VocaDb.Model.Resources:ArtistTypeNames.${artist.artistType}`)} +

+ + {artist.releaseDate && ( +

+ {t('ViewRes.Artist:Details.ReleaseDate')}{' '} + {moment(artist.releaseDate).format('l')} +

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/EventPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/EventPopupContent.tsx new file mode 100644 index 0000000000..f834b2117f --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/EventPopupContent.tsx @@ -0,0 +1,65 @@ +import { Markdown } from '@/Components/KnockoutExtensions/Markdown'; +import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; +import { EventCategory } from '@/Models/Events/EventCategory'; +import _ from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface EventPopupContentProps { + event: ReleaseEventContract; +} + +export const EventPopupContent = React.memo( + ({ event }: EventPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['VocaDb.Web.Resources.Domain.ReleaseEvents']); + + const category = event.series ? event.series.category : event.category; + + return ( + <> + {event.mainPicture && event.mainPicture.urlSmallThumb && ( +
+ Thumb +
+ )} + + {event.name} + + {event.additionalNames &&

{event.additionalNames}

} + + {category !== EventCategory.Unspecified && + category !== EventCategory.Other && ( +

+ {t( + `VocaDb.Web.Resources.Domain.ReleaseEvents:EventCategoryNames.${category}`, + )} +

+ )} + + {event.description && ( +

+ + {_.truncate(event.description, { + length: 100, + })} + +

+ )} + + {event.date && ( +

+ {t('ViewRes.Event:Details:OccurrenceDate')}{' '} + {moment(event.date).format('l')} + {event.endDate && ` - ${moment(event.endDate).format('l')}`} +

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx b/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx index 7b745d4e60..74f4d07f7f 100644 --- a/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx @@ -115,7 +115,9 @@ export const NamesEditor = observer( {showAliases && ( <> diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx index d3ef983e10..7b80a0599a 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx @@ -5,6 +5,22 @@ import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link } from 'react-router-dom'; +interface AlbumLinkBaseProps { + album: AlbumForApiContract; +} + +const AlbumLinkBase = ({ album }: AlbumLinkBaseProps): React.ReactElement => { + return ( + + {album.name} + + ); +}; + interface AlbumLinkProps { album: AlbumForApiContract; tooltip: boolean; @@ -15,22 +31,10 @@ export const AlbumLink = ({ tooltip = false, }: AlbumLinkProps): React.ReactElement => { return tooltip ? ( - - {album.name} + + ) : ( - - {album.name} - + ); }; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx index 0f8c71efc2..8243e25ae2 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx @@ -4,7 +4,28 @@ import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; import { EntryType } from '@/Models/EntryType'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, LinkProps } from 'react-router-dom'; + +interface ArtistLinkBaseProps extends Omit { + artist: ArtistContract; + children?: React.ReactNode; +} + +const ArtistLinkBase = ({ + artist, + children, + ...props +}: ArtistLinkBaseProps): React.ReactElement => { + return ( + + {children ?? artist.name} + + ); +}; interface ArtistLinkProps { artist: ArtistContract; @@ -26,23 +47,13 @@ export const ArtistLink = ({ {typeLabel && } {typeLabel && ' '} {tooltip ? ( - - {name ?? artist.name} + + {name} ) : ( - - {name ?? artist.name} - + + {name} + )} {releaseYear && artist.releaseDate && ( <> diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx index 196151b358..cf373d9fc9 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx @@ -5,6 +5,20 @@ import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link } from 'react-router-dom'; +interface EventLinkBaseProps { + event: ReleaseEventContract; +} + +const EventLinkBase = ({ event }: EventLinkBaseProps): React.ReactElement => { + return ( + + {event.name} + + ); +}; + interface EventLinkProps { event: ReleaseEventContract; tooltip?: boolean; @@ -15,18 +29,10 @@ export const EventLink = ({ tooltip, }: EventLinkProps): React.ReactElement => { return tooltip ? ( - - {event.name} + + ) : ( - - {event.name} - + ); }; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx index c37a75ba45..f8a2b960bf 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx @@ -42,9 +42,27 @@ export const AlbumThumbItem = React.memo( [album, playQueue], ); + const thumbItemRef = React.useRef(undefined!); + const [hover, setHover] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false); + // HACK: https://github.com/facebook/react/issues/6807#issuecomment-1240312500. + React.useLayoutEffect(() => { + const handleMouseEnter = (): void => setHover(true); + const handleMouseLeave = (): void => setHover(false); + + const thumbItem = thumbItemRef.current; + + thumbItem.addEventListener('mouseenter', handleMouseEnter); + thumbItem.addEventListener('mouseleave', handleMouseLeave); + + return (): void => { + thumbItem.removeEventListener('mouseenter', handleMouseEnter); + thumbItem.removeEventListener('mouseleave', handleMouseLeave); + }; + }, []); + return ( setHover(true)} - onMouseLeave={(): void => setHover(false)} + ref={thumbItemRef} > {(hover || isOpen) && ( { + entry: EntryBaseContract; + children?: React.ReactNode; +} + +const EntryLinkBase = ({ + entry, + children, + ...props +}: EntryLinkBaseProps): React.ReactElement => { + return ( + + {children ?? entry.defaultName} + + ); +}; + interface EntryLinkProps extends Omit { entry: EntryBaseContract; children?: React.ReactNode; @@ -18,18 +35,15 @@ export const EntryLink = React.memo( ...props }: EntryLinkProps): React.ReactElement => { return tooltip ? ( - - {children ?? entry.defaultName} + + + {children} + ) : ( - - {children ?? entry.defaultName} - + + {children} + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx index c620e9bfc1..eb74b3a482 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx @@ -1,34 +1,38 @@ -import $ from 'jquery'; -import 'qtip2'; +import OverlayTrigger from '@/Bootstrap/OverlayTrigger'; +import { QTipToolTip } from '@/QTip/QTipToolTip'; import React from 'react'; interface HelpLabelProps { label: string; - title: string; + dangerouslySetInnerHTML: { + __html: string; + }; forElem?: string; } // Displays label element with attached qTip tooltip export const HelpLabel = ({ label, - title, + dangerouslySetInnerHTML, forElem, }: HelpLabelProps): React.ReactElement => { - const el = React.useRef(undefined!); - - React.useEffect(() => { - $(el.current).qtip({ - style: { classes: 'tooltip-wider' }, - }); - - return (): void => { - $('.qtip').remove(); - }; - }, []); - return ( - + + + + } + > + + + + ); }; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx index 9695ce8f1c..d414021d0d 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx @@ -16,49 +16,54 @@ interface ThumbItemProps { export const ThumbItem: BsPrefixRefForwardingComponent/* TODO */ < 'a', ThumbItemProps -> = ({ - linkAs: LinkComponent = 'a', - linkProps, - thumbUrl, - caption, - entry, - tooltip, - children, - ...props -}: ThumbItemProps): React.ReactElement => { - return ( -
-
-
- {entry ? ( - - Preview - - ) : ( - - Preview - - )} -
+> = React.forwardRef( + ( + { + linkAs: LinkComponent = 'a', + linkProps, + thumbUrl, + caption, + entry, + tooltip, + children, + ...props + }: ThumbItemProps, + ref, + ): React.ReactElement => { + return ( +
+
+
+ {entry ? ( + + Preview + + ) : ( + + Preview + + )} +
- {children} + {children} +
+ {caption &&

{caption}

}
- {caption &&

{caption}

} -
- ); -}; + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx index 851960d16a..acfef9fa4a 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx @@ -1,26 +1,32 @@ -import $ from 'jquery'; -import 'qtip2'; +import OverlayTrigger from '@/Bootstrap/OverlayTrigger'; +import { QTipToolTip } from '@/QTip/QTipToolTip'; import React from 'react'; interface ValidationErrorIconProps { - title: string; + dangerouslySetInnerHTML: { + __html: string; + }; } // Displays label element with attached qTip tooltip export const ValidationErrorIcon = ({ - title, + dangerouslySetInnerHTML, }: ValidationErrorIconProps): React.ReactElement => { - const el = React.useRef(undefined!); - - React.useEffect(() => { - $(el.current).qtip({ - style: { classes: 'tooltip-wider' }, - }); - - return (): void => { - $('.qtip').remove(); - }; - }, []); - - return ; + return ( + + + + } + > + + + + + ); }; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx index cbf0944906..709c6e0da7 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx @@ -3,13 +3,34 @@ import { SongApiContract } from '@/DataContracts/Song/SongApiContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import qs from 'qs'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, LinkProps } from 'react-router-dom'; + +interface SongLinkBaseProps extends Omit { + song: SongApiContract; + albumId?: number; +} + +const SongLinkBase = ({ + song, + albumId, + ...props +}: SongLinkBaseProps): React.ReactElement => { + return ( + + {song.name} + + ); +}; interface SongLinkProps { song: SongApiContract; albumId?: number; tooltip?: boolean; - toolTipDomain?: string; target?: string; } @@ -18,32 +39,19 @@ export const SongLink = React.memo( song, albumId, tooltip = false, - toolTipDomain, target, }: SongLinkProps): React.ReactElement => { return tooltip ? ( - - {song.name} + + ) : ( - - {song.name} - + title={song.additionalNames} + /> ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx index 1893056167..6b39da106c 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx @@ -3,6 +3,24 @@ import { SongLink } from '@/Components/Shared/Partials/Song/SongLink'; import { SongApiContract } from '@/DataContracts/Song/SongApiContract'; import React from 'react'; +interface SongLinkKnockoutBaseProps + extends React.AnchorHTMLAttributes { + song: SongApiContract; + extUrl?: string; +} + +const SongLinkKnockoutBase = ({ + song, + extUrl, + ...props +}: SongLinkKnockoutBaseProps): React.ReactElement => { + return ( + + {song.name} + + ); +}; + interface SongLinkKnockoutProps { song: SongApiContract; albumId?: number; @@ -21,28 +39,23 @@ export const SongLinkKnockout = React.memo( }: SongLinkKnockoutProps): React.ReactElement => { return extUrl ? ( tooltip ? ( - - {song.name} + + ) : ( - - {song.name} - + ) ) : ( - + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx index 801f5529e0..07cce58d93 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx @@ -2,7 +2,24 @@ import { TagToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { TagBaseContract } from '@/DataContracts/Tag/TagBaseContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, LinkProps } from 'react-router-dom'; + +interface TagLinkBaseProps extends Omit { + tag: TagBaseContract; + children?: React.ReactNode; +} + +const TagLinkBase = ({ + tag, + children, + ...props +}: TagLinkBaseProps): React.ReactElement => { + return ( + + {children ?? tag.name} + + ); +}; interface TagLinkProps { tag: TagBaseContract; @@ -13,21 +30,13 @@ interface TagLinkProps { export const TagLink = React.memo( ({ tag, children, tooltip }: TagLinkProps): React.ReactElement => { return tooltip ? ( - - {children ?? tag.name} + + {children} ) : ( - - {children ?? tag.name} - + + {children} + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx index e6ed2943df..0bb6d783c2 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx @@ -4,6 +4,23 @@ import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link, LinkProps } from 'react-router-dom'; +interface UserLinkBaseProps extends Omit { + user: UserBaseContract; + children?: React.ReactNode; +} + +const UserLinkBase = ({ + user, + children, + ...props +}: UserLinkBaseProps): React.ReactElement => { + return ( + + {children ?? user.name} + + ); +}; + interface UserLinkProps extends Omit { user: UserBaseContract; children?: React.ReactNode; @@ -18,18 +35,15 @@ export const UserLink = React.memo( ...props }: UserLinkProps): React.ReactElement => { return tooltip ? ( - - {children ?? user.name} + + + {children} + ) : ( - - {children ?? user.name} - + + {children} + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/SongPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/SongPopupContent.tsx new file mode 100644 index 0000000000..4abc1a365e --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/SongPopupContent.tsx @@ -0,0 +1,51 @@ +import { SongContract } from '@/DataContracts/Song/SongContract'; +import { DateTimeHelper } from '@/Helpers/DateTimeHelper'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface SongPopupContentProps { + song: SongContract; +} + +export const SongPopupContent = React.memo( + ({ song }: SongPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['ViewRes', 'VocaDb.Model.Resources.Songs']); + + return ( + <> + {song.thumbUrl && ( +
+ Thumb +
+ )} + + {song.name} + + {song.additionalNames &&

{song.additionalNames}

} + +

+ {song.artistString} +
+ {t(`VocaDb.Model.Resources.Songs:SongTypeNames.${song.songType}`)} +

+ + {song.publishDate && ( +

+ {t('ViewRes:EntryDetails.PublishDate')}{' '} + {moment(song.publishDate).format('l')} +

+ )} + + {song.lengthSeconds > 0 && ( +

{DateTimeHelper.formatFromSeconds(song.lengthSeconds)}

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/SongWithVotePopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/SongWithVotePopupContent.tsx new file mode 100644 index 0000000000..f19dbdea37 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/SongWithVotePopupContent.tsx @@ -0,0 +1,24 @@ +import { SongPopupContent } from '@/Components/Shared/SongPopupContent'; +import { SongWithPVAndVoteContract } from '@/DataContracts/Song/SongWithPVAndVoteContract'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface SongWithVotePopupContentProps { + song: SongWithPVAndVoteContract; +} + +export const SongWithVotePopupContent = ({ + song, +}: SongWithVotePopupContentProps): React.ReactElement => { + const { t } = useTranslation(['Resources']); + + return ( + <> + + + {song.vote !== 'Nothing' && ( +

{t(`Resources:SongVoteRatingNames.${song.vote}`)}

+ )} + + ); +}; diff --git a/VocaDbWeb/Scripts/Components/Shared/TagPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/TagPopupContent.tsx new file mode 100644 index 0000000000..0d8d34e964 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/TagPopupContent.tsx @@ -0,0 +1,43 @@ +import { Markdown } from '@/Components/KnockoutExtensions/Markdown'; +import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; +import _ from 'lodash'; +import React from 'react'; + +interface TagPopupContentProps { + tag: TagApiContract; +} + +export const TagPopupContent = React.memo( + ({ tag }: TagPopupContentProps): React.ReactElement => { + return ( + <> + {tag.mainPicture && tag.mainPicture.urlSmallThumb && ( +
+ Thumb +
+ )} + + {tag.name} + + {tag.additionalNames &&

{tag.additionalNames}

} + + {tag.categoryName &&

{tag.categoryName}

} + + {tag.description && ( +

+ + {_.truncate(tag.description, { + length: 100, + })} + +

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/UserPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/UserPopupContent.tsx new file mode 100644 index 0000000000..1824885672 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/UserPopupContent.tsx @@ -0,0 +1,37 @@ +import { ProfileIcon } from '@/Components/Shared/Partials/User/ProfileIcon'; +import { UserApiContract } from '@/DataContracts/User/UserApiContract'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface UserPopupContentProps { + user: UserApiContract; +} + +export const UserPopupContent = React.memo( + ({ user }: UserPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['Resources']); + + return ( + <> + {user.mainPicture && user.mainPicture.urlThumb && ( +
+ +
+ )} + + {user.name} + +

{t(`Resources:UserGroupNames.${user.groupId}`)}

+ + {user.verifiedArtist && ( +

{t('ViewRes.User:Details.VerifiedAccount')}

+ )} + +

+ Joined{/* TODO: localize */} {moment(user.memberSince).format('l')} +

+ + ); + }, +); diff --git a/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts b/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts index 5b1d7b9250..383046f906 100644 --- a/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts +++ b/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts @@ -12,42 +12,24 @@ import { EventCategory } from '@/Models/Events/EventCategory'; // Matches ReleaseEventForApiContract export interface ReleaseEventContract { additionalNames?: string; - artists: ArtistForEventContract[]; - category: EventCategory; - date?: string; - defaultNameLanguage: string; - + description?: string; endDate?: string; - id: number; - mainPicture?: EntryThumbContract; - name: string; - names?: LocalizedStringWithIdContract[]; - pvs?: PVContract[]; - series?: EventSeriesContract; - songList?: SongListBaseContract; - status?: string; - tags?: TagUsageForApiContract[]; - urlSlug?: string; - venue?: VenueForApiContract; - version?: number; - venueName?: string; - webLinks: WebLinkContract[]; } diff --git a/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts b/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts index c95fc4592e..d3f5b2a262 100644 --- a/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts +++ b/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts @@ -4,10 +4,8 @@ import { UserGroup } from '@/Models/Users/UserGroup'; export interface UserApiContract extends UserBaseContract { active?: boolean; - groupId?: UserGroup; - mainPicture?: EntryThumbContract; - memberSince?: Date; + verifiedArtist?: boolean; } diff --git a/VocaDbWeb/Scripts/KnockoutExtensions/EntryToolTip.ts b/VocaDbWeb/Scripts/KnockoutExtensions/EntryToolTip.ts deleted file mode 100644 index 30532b7671..0000000000 --- a/VocaDbWeb/Scripts/KnockoutExtensions/EntryToolTip.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { EntryRefContract } from '@/DataContracts/EntryRefContract'; -import { functions } from '@/Shared/GlobalFunctions'; -import $ from 'jquery'; -import ko, { Observable } from 'knockout'; -import _ from 'lodash'; - -declare global { - interface KnockoutBindingHandlers { - albumToolTip: KnockoutBindingHandler; - artistToolTip: KnockoutBindingHandler; - entryToolTip: KnockoutBindingHandler; - eventToolTip: KnockoutBindingHandler; - songToolTip: KnockoutBindingHandler; - tagToolTip: KnockoutBindingHandler; - userToolTip: KnockoutBindingHandler; - } -} - -export function initToolTip( - element: HTMLElement, - relativeUrl: string, - id: number, - params?: any, - foreignDomain?: string, -): void { - const allowedDomains = [ - 'http://vocadb.net', - 'https://vocadb.net', - 'http://utaitedb.net', - 'https://utaitedb.net', - 'https://touhoudb.com', - ]; - const url = - foreignDomain && - allowedDomains.some((domain) => - foreignDomain.toLocaleLowerCase().includes(domain), - ) - ? functions.mergeUrls(foreignDomain, relativeUrl) - : functions.mapAbsoluteUrl(relativeUrl); - const data = _.assign({ id: id }, params); - - $(element).qtip({ - content: { - text: 'Loading...', - ajax: { - url: url, - type: 'GET', - data: data, - dataType: foreignDomain ? 'jsonp' : undefined, - }, - }, - position: { - viewport: $(window), - }, - style: { - classes: 'tooltip-wide', - }, - }); -} - -export interface TooltipOptions { - toolTipDomain?: string; - version?: number; -} - -ko.bindingHandlers.entryToolTip = { - init: ( - element: HTMLElement, - valueAccessor: () => Observable, - ): void => { - var value: EntryRefContract = ko.unwrap(valueAccessor()); - - switch (value.entryType) { - case 'Album': - initToolTip(element, '/Album/PopupContent', value.id); - break; - case 'Artist': - initToolTip(element, '/Artist/PopupContent', value.id); - break; - } - }, -}; - -ko.bindingHandlers.albumToolTip = { - init: ( - element: HTMLElement, - valueAccessor: () => Observable, - ): void => { - initToolTip(element, '/Album/PopupContent', ko.unwrap(valueAccessor())); - }, -}; - -ko.bindingHandlers.artistToolTip = { - init: ( - element: HTMLElement, - valueAccessor: () => Observable, - ): void => { - initToolTip(element, '/Artist/PopupContent', ko.unwrap(valueAccessor())); - }, -}; - -ko.bindingHandlers.eventToolTip = { - init: ( - element: HTMLElement, - valueAccessor: () => Observable, - ): void => { - const culture = vdb.values.uiCulture || undefined; - initToolTip(element, '/Event/PopupContent', ko.unwrap(valueAccessor()), { - culture: culture, - }); - }, -}; - -ko.bindingHandlers.songToolTip = { - init: ( - element: HTMLElement, - valueAccessor: () => Observable, - allPropertiesAccessor?: () => TooltipOptions, - ): void => { - const allProps = allPropertiesAccessor!(); - initToolTip( - element, - '/Song/PopupContentWithVote', - ko.unwrap(valueAccessor()), - { version: allProps.version }, - allProps.toolTipDomain, - ); - }, -}; - -ko.bindingHandlers.tagToolTip = { - init: ( - element: HTMLElement, - valueAccessor: () => Observable, - ): void => { - var culture = vdb.values.uiCulture || undefined; - var lang = vdb.values.languagePreference; - initToolTip(element, '/Tag/PopupContent', ko.unwrap(valueAccessor()), { - culture: culture, - lang: lang, - }); - }, -}; - -ko.bindingHandlers.userToolTip = { - init: ( - element: HTMLElement, - valueAccessor: () => Observable, - ): void => { - var culture = vdb.values.uiCulture || undefined; - initToolTip(element, '/User/PopupContent', ko.unwrap(valueAccessor()), { - culture: culture, - }); - }, -}; diff --git a/VocaDbWeb/Scripts/KnockoutExtensions/qTip.ts b/VocaDbWeb/Scripts/KnockoutExtensions/qTip.ts deleted file mode 100644 index 0d401da41b..0000000000 --- a/VocaDbWeb/Scripts/KnockoutExtensions/qTip.ts +++ /dev/null @@ -1,22 +0,0 @@ -import $ from 'jquery'; -import ko, { Observable } from 'knockout'; - -declare global { - interface KnockoutBindingHandlers { - // Knockout binding for qTip tooltip. - qTip: KnockoutBindingHandler; - } -} - -ko.bindingHandlers.qTip = { - init: ( - element: Element, - valueAccessor: () => Observable, - ): void => { - var params = ko.unwrap(valueAccessor()) || { - style: { classes: 'tooltip-wider' }, - }; - - $(element).qtip(params); - }, -}; diff --git a/VocaDbWeb/Scripts/Pages/Album/AlbumEdit.tsx b/VocaDbWeb/Scripts/Pages/Album/AlbumEdit.tsx index 094ba336b9..ae812b774e 100644 --- a/VocaDbWeb/Scripts/Pages/Album/AlbumEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/Album/AlbumEdit.tsx @@ -131,13 +131,19 @@ const BasicInfoTabContent = observer(
{' '} {albumEditStore.validationError_unspecifiedNames && ( <> {' '} - + )}
@@ -191,7 +197,7 @@ const BasicInfoTabContent = observer(
@@ -208,9 +214,11 @@ const BasicInfoTabContent = observer( <> {' '} )} @@ -256,9 +264,11 @@ const BasicInfoTabContent = observer( <> {' '} )} @@ -332,9 +342,10 @@ const BasicInfoTabContent = observer(
@@ -352,7 +363,9 @@ const BasicInfoTabContent = observer(

`} /* TODO: localize */ + dangerouslySetInnerHTML={{ + __html: `Barcodes are usually plain numbers, for example 01234567. They can be scanned from the product package.

` /* TODO: localize */, + }} />
@@ -391,7 +404,9 @@ const BasicInfoTabContent = observer(
@@ -403,7 +418,9 @@ const BasicInfoTabContent = observer(
diff --git a/VocaDbWeb/Scripts/Pages/Artist/ArtistEdit.tsx b/VocaDbWeb/Scripts/Pages/Artist/ArtistEdit.tsx index d9e921631a..78a76e72d0 100644 --- a/VocaDbWeb/Scripts/Pages/Artist/ArtistEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/Artist/ArtistEdit.tsx @@ -92,16 +92,20 @@ const BasicInfoTabContent = observer(
{' '} {artistEditStore.validationError_unspecifiedNames && ( <> {' '} )} @@ -153,9 +157,11 @@ const BasicInfoTabContent = observer( <> {' '} )} @@ -182,9 +188,11 @@ const BasicInfoTabContent = observer( <> {' '} )} @@ -195,7 +203,10 @@ const BasicInfoTabContent = observer(
@@ -212,7 +223,10 @@ const BasicInfoTabContent = observer( @@ -242,7 +256,10 @@ const BasicInfoTabContent = observer( @@ -447,7 +464,9 @@ const BasicInfoTabContent = observer(
@@ -459,7 +478,9 @@ const BasicInfoTabContent = observer(
diff --git a/VocaDbWeb/Scripts/Pages/Event/EventEdit.tsx b/VocaDbWeb/Scripts/Pages/Event/EventEdit.tsx index 727dbeac49..c3a5da0eb6 100644 --- a/VocaDbWeb/Scripts/Pages/Event/EventEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/Event/EventEdit.tsx @@ -197,7 +197,9 @@ const BasicInfoTabContent = observer(
@@ -265,7 +267,10 @@ const BasicInfoTabContent = observer(
@@ -299,7 +304,10 @@ const BasicInfoTabContent = observer(
@@ -321,7 +329,10 @@ const BasicInfoTabContent = observer(
@@ -378,7 +389,9 @@ const BasicInfoTabContent = observer(
@@ -390,7 +403,9 @@ const BasicInfoTabContent = observer(
diff --git a/VocaDbWeb/Scripts/Pages/Event/EventEditSeries.tsx b/VocaDbWeb/Scripts/Pages/Event/EventEditSeries.tsx index 4db82f120c..4749f85a9e 100644 --- a/VocaDbWeb/Scripts/Pages/Event/EventEditSeries.tsx +++ b/VocaDbWeb/Scripts/Pages/Event/EventEditSeries.tsx @@ -227,7 +227,9 @@ const EventEditSeriesLayout = observer(
@@ -344,7 +346,9 @@ const EventEditSeriesLayout = observer(
@@ -356,7 +360,9 @@ const EventEditSeriesLayout = observer(
diff --git a/VocaDbWeb/Scripts/Pages/Song/Partials/LyricsForSongEdit.tsx b/VocaDbWeb/Scripts/Pages/Song/Partials/LyricsForSongEdit.tsx index 2f07037323..63e8f0e731 100644 --- a/VocaDbWeb/Scripts/Pages/Song/Partials/LyricsForSongEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/Song/Partials/LyricsForSongEdit.tsx @@ -72,7 +72,10 @@ const LyricsForSongEdit = observer(

{' '} {' '}

diff --git a/VocaDbWeb/Scripts/Pages/Song/SongCreate.tsx b/VocaDbWeb/Scripts/Pages/Song/SongCreate.tsx index f1d3962710..cd2e1df8af 100644 --- a/VocaDbWeb/Scripts/Pages/Song/SongCreate.tsx +++ b/VocaDbWeb/Scripts/Pages/Song/SongCreate.tsx @@ -264,7 +264,9 @@ const SongCreateLayout = observer(
diff --git a/VocaDbWeb/Scripts/Pages/Song/SongEdit.tsx b/VocaDbWeb/Scripts/Pages/Song/SongEdit.tsx index 0472a571e9..ab07f79799 100644 --- a/VocaDbWeb/Scripts/Pages/Song/SongEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/Song/SongEdit.tsx @@ -99,7 +99,9 @@ const BasicInfoTabContent = observer(
@@ -116,16 +118,20 @@ const BasicInfoTabContent = observer(
{' '} {songEditStore.validationError_unspecifiedNames && ( <> {' '} )} @@ -161,9 +167,11 @@ const BasicInfoTabContent = observer( <> {' '} )} @@ -176,7 +184,9 @@ const BasicInfoTabContent = observer(
@@ -312,7 +322,9 @@ const BasicInfoTabContent = observer(
@@ -383,7 +395,9 @@ const BasicInfoTabContent = observer(
@@ -395,7 +409,9 @@ const BasicInfoTabContent = observer(
@@ -535,7 +551,9 @@ const PVsTabContent = observer( diff --git a/VocaDbWeb/Scripts/Pages/SongList/SongListEdit.tsx b/VocaDbWeb/Scripts/Pages/SongList/SongListEdit.tsx index adb0375282..b439f6ace8 100644 --- a/VocaDbWeb/Scripts/Pages/SongList/SongListEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/SongList/SongListEdit.tsx @@ -134,7 +134,9 @@ const PropertiesTabContent = observer(
@@ -179,7 +181,9 @@ const PropertiesTabContent = observer(
diff --git a/VocaDbWeb/Scripts/Pages/Tag/TagEdit.tsx b/VocaDbWeb/Scripts/Pages/Tag/TagEdit.tsx index 67fd19efea..5d27040fe6 100644 --- a/VocaDbWeb/Scripts/Pages/Tag/TagEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/Tag/TagEdit.tsx @@ -217,7 +217,10 @@ const TagEditLayout = observer( />
- +
- {' '} + {' '}
@@ -256,7 +262,10 @@ const TagEditLayout = observer(
@@ -382,7 +391,9 @@ const TagEditLayout = observer(
@@ -394,7 +405,9 @@ const TagEditLayout = observer(
diff --git a/VocaDbWeb/Scripts/Pages/Venue/VenueEdit.tsx b/VocaDbWeb/Scripts/Pages/Venue/VenueEdit.tsx index 4d184e864f..9e32a521de 100644 --- a/VocaDbWeb/Scripts/Pages/Venue/VenueEdit.tsx +++ b/VocaDbWeb/Scripts/Pages/Venue/VenueEdit.tsx @@ -186,7 +186,9 @@ const VenueEditLayout = observer(
@@ -323,7 +325,9 @@ const VenueEditLayout = observer(
@@ -335,7 +339,9 @@ const VenueEditLayout = observer(
diff --git a/VocaDbWeb/Scripts/QTip/QTipToolTip.tsx b/VocaDbWeb/Scripts/QTip/QTipToolTip.tsx new file mode 100644 index 0000000000..27f7f90584 --- /dev/null +++ b/VocaDbWeb/Scripts/QTip/QTipToolTip.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +type QTipToolTipProps = React.HTMLAttributes; + +export const QTipToolTip = React.forwardRef( + ({ children, ...props }: QTipToolTipProps, ref): React.ReactElement => { + return ( +
+
{children}
+
+ ); + }, +); diff --git a/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts b/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts index 62567cc154..0c0b3cb13b 100644 --- a/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts @@ -131,9 +131,17 @@ export class ReleaseEventRepository extends BaseRepository { ); }; - public getOne = ({ id }: { id: number }): Promise => { + public getOne = ({ + id, + fields, + }: { + id: number; + fields?: ReleaseEventOptionalField[]; + }): Promise => { var url = functions.mergeUrls(this.baseUrl, `/api/releaseEvents/${id}`); - return this.httpClient.get(url); + return this.httpClient.get(url, { + fields: fields?.join(','), + }); }; public getOneSeries = ({ diff --git a/VocaDbWeb/Scripts/Repositories/SongRepository.ts b/VocaDbWeb/Scripts/Repositories/SongRepository.ts index 6485050981..67506d891c 100644 --- a/VocaDbWeb/Scripts/Repositories/SongRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/SongRepository.ts @@ -294,17 +294,19 @@ export class SongRepository private getJSON: (relative: string, params: any) => Promise; public getOneWithComponents = ({ + baseUrl, id, fields, lang, }: { + baseUrl?: string; id: number; - fields: SongOptionalField[]; + fields?: SongOptionalField[]; lang: ContentLanguagePreference; }): Promise => { - var url = functions.mergeUrls(this.baseUrl, `/api/songs/${id}`); + var url = functions.mergeUrls(baseUrl ?? this.baseUrl, `/api/songs/${id}`); return this.httpClient.get(url, { - fields: fields.join(','), + fields: fields?.join(','), lang: lang, }); }; diff --git a/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts b/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts index 4de293b36a..b309c175b7 100644 --- a/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts +++ b/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts @@ -366,7 +366,6 @@ export class SongDetailsStore { songRepo .getOneWithComponents({ id: id, - fields: [], lang: this.values.languagePreference, }) .then((song) => { diff --git a/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts b/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts index a6a12f0e36..003bfdc72a 100644 --- a/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts +++ b/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts @@ -74,7 +74,7 @@ export class FakeSongRepository extends SongRepository { lang, }: { id: number; - fields: SongOptionalField[]; + fields?: SongOptionalField[]; lang: ContentLanguagePreference; }): Promise => { return FakePromise.resolve(this.song); diff --git a/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts index 7dc0157169..65b245f04a 100644 --- a/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts @@ -134,7 +134,6 @@ export class SongDetailsViewModel { repo .getOneWithComponents({ id: id, - fields: [], lang: this.values.languagePreference, }) .then((song) => { diff --git a/VocaDbWeb/Scripts/VocaDb.js b/VocaDbWeb/Scripts/VocaDb.js deleted file mode 100644 index ce9a66fd0c..0000000000 --- a/VocaDbWeb/Scripts/VocaDb.js +++ /dev/null @@ -1,75 +0,0 @@ -vdb = {}; -vdb.values = vdb.values || {}; - -(function ($) { - $.fn.vdbArtistToolTip = function () { - this.each(function () { - var elem = this; - - $(elem).qtip({ - content: { - text: 'Loading...', - ajax: { - url: app.functions.mapAbsoluteUrl('/Artist/PopupContent'), - type: 'GET', - data: { id: $(elem).data('entryId') }, - }, - }, - position: { - viewport: $(window), - }, - style: { - classes: 'tooltip-wide', - }, - }); - }); - }; -})(jQuery); - -(function ($) { - $.fn.vdbAlbumToolTip = function () { - this.each(function () { - var elem = this; - - $(elem).qtip({ - content: { - text: 'Loading...', - ajax: { - url: app.functions.mapAbsoluteUrl('/Album/PopupContent'), - type: 'GET', - data: { id: $(elem).data('entryId') }, - }, - }, - position: { - viewport: $(window), - }, - }); - }); - }; -})(jQuery); - -(function ($) { - $.fn.vdbAlbumWithCoverToolTip = function () { - this.each(function () { - var elem = this; - - $(elem).qtip({ - content: { - text: 'Loading...', - ajax: { - url: app.functions.mapAbsoluteUrl('/Album/PopupWithCoverContent'), - type: 'GET', - data: { id: $(elem).data('entryId') }, - }, - }, - position: { - viewport: $(window), - }, - }); - }); - }; -})(jQuery); - -$(document).ready(function () { - $('#loginPopup').dialog({ autoOpen: false, width: 400, modal: true }); -}); diff --git a/VocaDbWeb/Scripts/libs.js b/VocaDbWeb/Scripts/libs.js index c29007f2e9..ff5f531cd1 100644 --- a/VocaDbWeb/Scripts/libs.js +++ b/VocaDbWeb/Scripts/libs.js @@ -25,8 +25,6 @@ require('knockout-punches'); window._ = require('lodash'); -require('qtip2'); - window.marked = require('marked'); window.moment = require('moment'); diff --git a/VocaDbWeb/Scripts/typings/qTip2/qtip.d.ts b/VocaDbWeb/Scripts/typings/qTip2/qtip.d.ts deleted file mode 100644 index 1ddd94e379..0000000000 --- a/VocaDbWeb/Scripts/typings/qTip2/qtip.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -interface JQuery { - qtip: (qtipProperties: QTipProperties) => void; -} - -interface QTipProperties { - content?: any; - - position?: any; - - style?: any; -} diff --git a/VocaDbWeb/Scripts/vdb.ts b/VocaDbWeb/Scripts/vdb.ts index 0dc0cd0318..d819218310 100644 --- a/VocaDbWeb/Scripts/vdb.ts +++ b/VocaDbWeb/Scripts/vdb.ts @@ -18,14 +18,12 @@ export { SharedLayoutScripts } from '@/Shared/LayoutScripts'; export { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import '@/KnockoutExtensions/ConfirmClick'; import '@/KnockoutExtensions/Dialog'; -import '@/KnockoutExtensions/EntryToolTip'; import '@/KnockoutExtensions/jqButton'; import '@/KnockoutExtensions/jqButtonset'; import '@/KnockoutExtensions/Markdown'; import '@/KnockoutExtensions/ToggleClick'; import '@/KnockoutExtensions/Song/SongTypeLabel'; import '@/KnockoutExtensions/Bootstrap/Tooltip'; -import '@/KnockoutExtensions/qTip'; import '@/KnockoutExtensions/TagAutoComplete'; import '@/KnockoutExtensions/Filters/Truncate'; export { RepositoryFactory } from '@/Repositories/RepositoryFactory'; diff --git a/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml b/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml index bd33ca1c7e..0ed729d00b 100644 --- a/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml +++ b/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml @@ -4,6 +4,5 @@ @if (Model.User != null && !string.IsNullOrEmpty(Model.User.Email)) {
- @Gravatar.GetHtml(Model.User.Email, Model.Size)
} \ No newline at end of file diff --git a/VocaDbWeb/Views/Shared/Partials/_LayoutScripts.cshtml b/VocaDbWeb/Views/Shared/Partials/_LayoutScripts.cshtml index 35ddf3072a..ec1b9f25b6 100644 --- a/VocaDbWeb/Views/Shared/Partials/_LayoutScripts.cshtml +++ b/VocaDbWeb/Views/Shared/Partials/_LayoutScripts.cshtml @@ -13,7 +13,6 @@ -