@3birds/midori-ui 0.0.40 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/file-tree-outline.svg +1 -1
- package/dist/assets/star-yellow.svg +7 -0
- package/dist/components/Alert.svelte +2 -2
- package/dist/components/Breadcrumbs.svelte +3 -3
- package/dist/components/Card.svelte +5 -20
- package/dist/components/CardSide.svelte +17 -0
- package/dist/components/CardSide.svelte.d.ts +8 -0
- package/dist/components/Filter.svelte +4 -4
- package/dist/components/Img.svelte +13 -0
- package/dist/components/Img.svelte.d.ts +8 -0
- package/dist/components/Input.svelte +2 -2
- package/dist/components/Input.svelte.d.ts +1 -1
- package/dist/components/Modal.svelte +2 -2
- package/dist/constants/index.d.ts +6 -0
- package/dist/constants/index.js +6 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +69 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#18181B99" d="M12 13H7V18H12V20H5V10H7V11H12V13M8 4V6H4V4H8M10 2H2V8H10V2M20 11V13H16V11H20M22 9H14V15H22V9M20 18V20H16V18H20M22 16H14V22H22V16Z" /></svg>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { alertSvg, checkCircleSvg, closeCircleSvg, informationSvg } from '..';
|
|
2
|
+
import { alertSvg, checkCircleSvg, closeCircleSvg, Img, informationSvg } from '..';
|
|
3
3
|
import { alert } from '../alert';
|
|
4
4
|
|
|
5
5
|
let popover: HTMLButtonElement | undefined = $state(undefined);
|
|
@@ -43,6 +43,6 @@
|
|
|
43
43
|
class="alert rounded-3xl {color} fixed top-4 right-4 left-4 {$alert.show ? 'flex' : 'hidden'}"
|
|
44
44
|
onclick={() => alert.close()}
|
|
45
45
|
>
|
|
46
|
-
<
|
|
46
|
+
<Img src={icon} alt="アラート" class="size-6" />
|
|
47
47
|
{$alert.message}
|
|
48
48
|
</button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { fileTreeOutlineSvg, type BreadcrumbsItem } from '..';
|
|
2
|
+
import { fileTreeOutlineSvg, Img, type BreadcrumbsItem } from '..';
|
|
3
3
|
|
|
4
4
|
let {
|
|
5
5
|
items,
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
} = $props();
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
|
-
<nav class="breadcrumbs p-0 text-
|
|
13
|
+
<nav class="breadcrumbs p-0 text-xs text-base-content/60 {className}">
|
|
14
14
|
<ul class="flex-wrap items-center">
|
|
15
|
-
<
|
|
15
|
+
<Img src={fileTreeOutlineSvg} alt="パンくずリスト" class="mr-2 size-4" />
|
|
16
16
|
{#each items as item}
|
|
17
17
|
<li class={item.href ? 'shrink-0' : 'min-w-0'}>
|
|
18
18
|
{#if item.href}
|
|
@@ -1,40 +1,25 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { calendarMonthGraySvg,
|
|
2
|
+
import { calendarMonthGraySvg, Img } from '..';
|
|
3
3
|
import type { ContentCasual } from '../types';
|
|
4
4
|
|
|
5
5
|
let { content, searchParams = '' }: { content: ContentCasual; searchParams?: string } = $props();
|
|
6
|
-
|
|
7
|
-
const onErrorImage = (e: Event): void => {
|
|
8
|
-
const target = e.currentTarget as HTMLImageElement;
|
|
9
|
-
target.src = noImageSvg;
|
|
10
|
-
target.onerror = null;
|
|
11
|
-
};
|
|
12
6
|
</script>
|
|
13
7
|
|
|
14
8
|
<a class="card w-full rounded-3xl card-sm" href="/contents/{content.id}{searchParams}">
|
|
15
9
|
<figure class="aspect-3/2 rounded-3xl">
|
|
16
|
-
<
|
|
17
|
-
src={content.thumbnail}
|
|
18
|
-
alt={content.name}
|
|
19
|
-
class="h-full w-full object-cover"
|
|
20
|
-
onerror={onErrorImage}
|
|
21
|
-
/>
|
|
10
|
+
<Img src={content.thumbnail} alt={content.name} class="h-full w-full object-cover" />
|
|
22
11
|
</figure>
|
|
23
12
|
<div class="card-body gap-0.5 px-1 py-2">
|
|
24
13
|
{#if content.schedules.length > 0}
|
|
25
14
|
<p class="flex min-w-0 items-center gap-0.5 text-base-content/60">
|
|
26
|
-
<
|
|
15
|
+
<Img src={calendarMonthGraySvg} alt="スケジュール" class="size-3 shrink-0" />
|
|
27
16
|
<span class="block min-w-0 truncate">{content.schedules.join(',')}</span>
|
|
28
17
|
</p>
|
|
29
18
|
{/if}
|
|
30
19
|
<div class="flex">
|
|
31
|
-
<h2 class="card-title min-w-0">
|
|
32
|
-
|
|
20
|
+
<h2 class="card-title min-w-0 truncate">
|
|
21
|
+
{content.name}
|
|
33
22
|
</h2>
|
|
34
|
-
<p class="flex shrink-0 items-center justify-end gap-0.5 text-base-content/60">
|
|
35
|
-
<img src={StarGraySvg} alt="評価" class="size-3 shrink-0" />
|
|
36
|
-
{content.rate_average}
|
|
37
|
-
</p>
|
|
38
23
|
</div>
|
|
39
24
|
{#if content.parents}
|
|
40
25
|
<p class="flex items-center gap-1 text-base-content/60">
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Img, type ContentGeo } from '..';
|
|
3
|
+
|
|
4
|
+
let { content, searchParams = '' }: { content: ContentGeo; searchParams?: string } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<a
|
|
8
|
+
class="card fixed right-22 bottom-4 left-4 z-20 mx-auto card-side h-32 max-w-sm rounded-3xl bg-base-100 shadow-sm card-sm sm:right-4"
|
|
9
|
+
href="/contents/{content.id}{searchParams}"
|
|
10
|
+
>
|
|
11
|
+
<figure class="aspect-1">
|
|
12
|
+
<Img src={content.thumbnail} alt={content.name} class="h-full w-full object-cover" />
|
|
13
|
+
</figure>
|
|
14
|
+
<div class="card-body justify-center">
|
|
15
|
+
<h2 class="card-title">{content.name}</h2>
|
|
16
|
+
</div>
|
|
17
|
+
</a>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ContentGeo } from '..';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
content: ContentGeo;
|
|
4
|
+
searchParams?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const CardSide: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type CardSide = ReturnType<typeof CardSide>;
|
|
8
|
+
export default CardSide;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Btn, closeSvg, type FilterItem } from '..';
|
|
2
|
+
import { Btn, closeSvg, Img, type FilterItem } from '..';
|
|
3
3
|
|
|
4
4
|
let {
|
|
5
5
|
value = $bindable(),
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<p class="text-sm text-base-content/60">絞り込み:</p>
|
|
17
17
|
{#each items as item}
|
|
18
18
|
<input
|
|
19
|
-
class="btn m-0.5 rounded-full btn-sm"
|
|
19
|
+
class="btn m-0.5 rounded-full bg-base-100 btn-outline btn-sm"
|
|
20
20
|
type="checkbox"
|
|
21
21
|
name="frameworks"
|
|
22
22
|
aria-label={item.label}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
bind:group={value}
|
|
25
25
|
/>
|
|
26
26
|
{/each}
|
|
27
|
-
<Btn class="m-0.5 btn-circle btn-sm" type="reset">
|
|
28
|
-
<
|
|
27
|
+
<Btn class="m-0.5 btn-circle btn-outline btn-sm" type="reset">
|
|
28
|
+
<Img src={closeSvg} alt="クリア" class="size-4" />
|
|
29
29
|
</Btn>
|
|
30
30
|
</form>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { noImageSvg } from '..';
|
|
3
|
+
|
|
4
|
+
let { src, alt, class: className }: { src: string; alt: string; class?: string } = $props();
|
|
5
|
+
|
|
6
|
+
const onerror = (e: Event): void => {
|
|
7
|
+
const target = e.currentTarget as HTMLImageElement;
|
|
8
|
+
target.src = noImageSvg;
|
|
9
|
+
target.onerror = null;
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<img {src} {alt} class={className} {onerror} />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
2
|
+
import { Img, type InputType } from '..';
|
|
3
3
|
|
|
4
4
|
let {
|
|
5
5
|
type,
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
{/if}
|
|
45
45
|
<label class="input w-full rounded-full {icon ? '' : 'px-5'}">
|
|
46
46
|
{#if icon}
|
|
47
|
-
<
|
|
47
|
+
<Img src={icon} class="size-4" alt="アイコン" />
|
|
48
48
|
{/if}
|
|
49
49
|
<input
|
|
50
50
|
{type}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Btn, closeSvg } from '..';
|
|
2
|
+
import { Btn, closeSvg, Img } from '..';
|
|
3
3
|
|
|
4
4
|
let {
|
|
5
5
|
value = $bindable(),
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<div class="modal-action">
|
|
15
15
|
<form method="dialog">
|
|
16
16
|
<Btn class="absolute top-2 right-2 btn-circle btn-ghost btn-sm" type="submit">
|
|
17
|
-
<
|
|
17
|
+
<Img src={closeSvg} alt="閉じる" class="size-6" />
|
|
18
18
|
</Btn>
|
|
19
19
|
</form>
|
|
20
20
|
</div>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { PriorityChoice } from '../types';
|
|
2
|
+
export declare const MAP_ATTRIBUTION: string;
|
|
3
|
+
export declare const MAP_DEFAULT_ICON_SIZE: number[];
|
|
4
|
+
export declare const MAP_DEFAULT_ICON_ANCHOR: number[];
|
|
5
|
+
export declare const MAP_MAX_ZOOM: number;
|
|
6
|
+
export declare const MAP_DEFAULT_LATITUDE: number;
|
|
7
|
+
export declare const MAP_DEFAULT_LONGITUDE: number;
|
|
2
8
|
export declare const PRIORITIES: PriorityChoice[];
|
|
3
9
|
export declare const PRIORITIES_WITHOUT_LOG: PriorityChoice[];
|
|
4
10
|
export declare const PREFECTURE_SVG: Record<string, string>;
|
package/dist/constants/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export const MAP_ATTRIBUTION = '<a href="https://github.com/protomaps/basemaps">Protomaps</a> © <a href="https://openstreetmap.org">OpenStreetMap</a>';
|
|
2
|
+
export const MAP_DEFAULT_ICON_SIZE = [20, 20];
|
|
3
|
+
export const MAP_DEFAULT_ICON_ANCHOR = [10, 10];
|
|
4
|
+
export const MAP_MAX_ZOOM = 20;
|
|
5
|
+
export const MAP_DEFAULT_LATITUDE = 35.658;
|
|
6
|
+
export const MAP_DEFAULT_LONGITUDE = 139.745;
|
|
1
7
|
export const PRIORITIES = [
|
|
2
8
|
{ label: 'フィールド', value: '50' },
|
|
3
9
|
{ label: 'イベント', value: '40' },
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ export { default as Alert } from './components/Alert.svelte';
|
|
|
7
7
|
export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
|
|
8
8
|
export { default as Filter } from './components/Filter.svelte';
|
|
9
9
|
export { default as Card } from './components/Card.svelte';
|
|
10
|
+
export { default as CardSide } from './components/CardSide.svelte';
|
|
10
11
|
export { default as Header } from './components/Header.svelte';
|
|
11
12
|
export { default as Pagination } from './components/Pagination.svelte';
|
|
13
|
+
export { default as Img } from './components/Img.svelte';
|
|
12
14
|
export { default as accountSvg } from './assets/account.svg';
|
|
13
15
|
export { default as alertSvg } from './assets/alert.svg';
|
|
14
16
|
export { default as calendarMonthSvg } from './assets/calendar-month.svg';
|
|
@@ -48,6 +50,7 @@ export { default as refreshSvg } from './assets/refresh.svg';
|
|
|
48
50
|
export { default as shapeOutlineSvg } from './assets/shape-outline.svg';
|
|
49
51
|
export { default as StarSvg } from './assets/star.svg';
|
|
50
52
|
export { default as StarGraySvg } from './assets/star-gray.svg';
|
|
53
|
+
export { default as StarYellowSvg } from './assets/star-yellow.svg';
|
|
51
54
|
export { default as 北海道 } from './assets/prefs/北海道.svg';
|
|
52
55
|
export { default as 青森県 } from './assets/prefs/青森県.svg';
|
|
53
56
|
export { default as 岩手県 } from './assets/prefs/岩手県.svg';
|
|
@@ -244,5 +247,6 @@ export { authState } from './auth.svelte.ts';
|
|
|
244
247
|
export * as firebase from './auth';
|
|
245
248
|
export { auth } from './firebase';
|
|
246
249
|
export * as modal from './modals-context.ts';
|
|
250
|
+
export * from './utils/index.ts';
|
|
247
251
|
export { PRIORITIES, PRIORITIES_WITHOUT_LOG, PREFECTURE_SVG, POI_ICON } from './constants/index.ts';
|
|
248
|
-
export type { BtnType, InputType, AlertType, AlertState, BreadcrumbsItem, FilterItem, Priority, PriorityChoice, ModalsContext, Data, ContentCasual, ContentGeo, Dashboard, DashboardPref } from './types/index.ts';
|
|
252
|
+
export type { BtnType, InputType, AlertType, AlertState, BreadcrumbsItem, FilterItem, Priority, PriorityChoice, ModalsContext, Data, ContentAdditionalInfo, ContentAdmin, ContentImage, ContentReview, ContentMinimal, ContentCasual, ContentFormal, ContentGeo, Dashboard, DashboardPref } from './types/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,10 @@ export { default as Alert } from './components/Alert.svelte';
|
|
|
8
8
|
export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
|
|
9
9
|
export { default as Filter } from './components/Filter.svelte';
|
|
10
10
|
export { default as Card } from './components/Card.svelte';
|
|
11
|
+
export { default as CardSide } from './components/CardSide.svelte';
|
|
11
12
|
export { default as Header } from './components/Header.svelte';
|
|
12
13
|
export { default as Pagination } from './components/Pagination.svelte';
|
|
14
|
+
export { default as Img } from './components/Img.svelte';
|
|
13
15
|
export { default as accountSvg } from './assets/account.svg';
|
|
14
16
|
export { default as alertSvg } from './assets/alert.svg';
|
|
15
17
|
export { default as calendarMonthSvg } from './assets/calendar-month.svg';
|
|
@@ -49,6 +51,7 @@ export { default as refreshSvg } from './assets/refresh.svg';
|
|
|
49
51
|
export { default as shapeOutlineSvg } from './assets/shape-outline.svg';
|
|
50
52
|
export { default as StarSvg } from './assets/star.svg';
|
|
51
53
|
export { default as StarGraySvg } from './assets/star-gray.svg';
|
|
54
|
+
export { default as StarYellowSvg } from './assets/star-yellow.svg';
|
|
52
55
|
export { default as 北海道 } from './assets/prefs/北海道.svg';
|
|
53
56
|
export { default as 青森県 } from './assets/prefs/青森県.svg';
|
|
54
57
|
export { default as 岩手県 } from './assets/prefs/岩手県.svg';
|
|
@@ -245,4 +248,5 @@ export { authState } from "./auth.svelte.js";
|
|
|
245
248
|
export * as firebase from './auth';
|
|
246
249
|
export { auth } from './firebase';
|
|
247
250
|
export * as modal from "./modals-context.js";
|
|
251
|
+
export * from "./utils/index.js";
|
|
248
252
|
export { PRIORITIES, PRIORITIES_WITHOUT_LOG, PREFECTURE_SVG, POI_ICON } from "./constants/index.js";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -31,6 +31,33 @@ export interface Data<T> {
|
|
|
31
31
|
data?: T;
|
|
32
32
|
message?: string;
|
|
33
33
|
}
|
|
34
|
+
export interface ContentImage {
|
|
35
|
+
src: string;
|
|
36
|
+
credit: string;
|
|
37
|
+
link: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ContentAdditionalInfo {
|
|
40
|
+
title: string;
|
|
41
|
+
body: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ContentReview {
|
|
44
|
+
rating: number;
|
|
45
|
+
body: string;
|
|
46
|
+
photo?: string;
|
|
47
|
+
content_id: string;
|
|
48
|
+
content_name: string;
|
|
49
|
+
reviewer_nickname: string;
|
|
50
|
+
reviewer_icon?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ContentMinimal {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ContentAdmin {
|
|
57
|
+
name: string;
|
|
58
|
+
midori: boolean;
|
|
59
|
+
photo?: string;
|
|
60
|
+
}
|
|
34
61
|
export interface ContentCasual {
|
|
35
62
|
id: string;
|
|
36
63
|
name: string;
|
|
@@ -42,6 +69,34 @@ export interface ContentCasual {
|
|
|
42
69
|
rate_average: number;
|
|
43
70
|
schedules: string[];
|
|
44
71
|
}
|
|
72
|
+
export interface ContentFormal {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
latitude: number;
|
|
76
|
+
longitude: number;
|
|
77
|
+
map_zoom_level: number;
|
|
78
|
+
marker_color: string;
|
|
79
|
+
marker_icon: string;
|
|
80
|
+
address: string;
|
|
81
|
+
overview: string;
|
|
82
|
+
hp: string;
|
|
83
|
+
x: string;
|
|
84
|
+
instagram: string;
|
|
85
|
+
facebook: string;
|
|
86
|
+
images: ContentImage[];
|
|
87
|
+
parents: ContentMinimal[];
|
|
88
|
+
rate_average: number;
|
|
89
|
+
review_count: number;
|
|
90
|
+
schedules: string[];
|
|
91
|
+
fields: ContentCasual[];
|
|
92
|
+
events: ContentCasual[];
|
|
93
|
+
contents: ContentCasual[];
|
|
94
|
+
amenities: ContentCasual[];
|
|
95
|
+
logs: ContentCasual[];
|
|
96
|
+
additional_infos: ContentAdditionalInfo[];
|
|
97
|
+
reviews: ContentReview[];
|
|
98
|
+
admins: ContentAdmin[];
|
|
99
|
+
}
|
|
45
100
|
export interface ContentGeo {
|
|
46
101
|
latitude: number;
|
|
47
102
|
longitude: number;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const yen: (amount: number) => string;
|
|
2
|
+
export declare const withoutFormatText: (text?: string) => string;
|
|
3
|
+
export declare const autoFormatText: (text: string) => string;
|
|
4
|
+
export declare const date: (datetime: string) => string;
|
|
5
|
+
export declare const datetime: (datetime: string) => string;
|
|
6
|
+
export declare const datetimeForInput: (datetime: string) => string;
|
|
7
|
+
export declare const truncateWithEllipsis: (text: string, maxLength: number) => string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const yen = (amount) => `${amount.toLocaleString()}円`;
|
|
2
|
+
const processCustomMarkup = (text, type, replacer) => {
|
|
3
|
+
let pattern;
|
|
4
|
+
switch (type) {
|
|
5
|
+
case 'link':
|
|
6
|
+
pattern = /@link\[([^\]]*)\]\(([^\)]*)\)/g;
|
|
7
|
+
break;
|
|
8
|
+
case 'bold':
|
|
9
|
+
pattern = /@bold\[([^\]]*)\]/g;
|
|
10
|
+
break;
|
|
11
|
+
case 'small':
|
|
12
|
+
pattern = /@small\[([^\]]*)\]/g;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
return text.replace(pattern, (_, display, link) => replacer(display, link));
|
|
16
|
+
};
|
|
17
|
+
const stripCustomMarkup = (text, type) => {
|
|
18
|
+
return processCustomMarkup(text, type, (display) => display);
|
|
19
|
+
};
|
|
20
|
+
export const withoutFormatText = (text) => {
|
|
21
|
+
if (!text)
|
|
22
|
+
return '';
|
|
23
|
+
text = stripCustomMarkup(text, 'link');
|
|
24
|
+
text = stripCustomMarkup(text, 'bold');
|
|
25
|
+
text = stripCustomMarkup(text, 'small');
|
|
26
|
+
return text.replace(/\s+/g, '');
|
|
27
|
+
};
|
|
28
|
+
export const autoFormatText = (text) => {
|
|
29
|
+
text = escapeHtml(text);
|
|
30
|
+
text = processCustomMarkup(text, 'link', (display, link) => `<a href="${sanitizeUrl(link)}" target="_blank" rel="noopener noreferrer" class="link">${display}</a>`);
|
|
31
|
+
text = processCustomMarkup(text, 'bold', (display) => `<span class="font-bold">${display}</span>`);
|
|
32
|
+
text = processCustomMarkup(text, 'small', (display) => `<span class="text-xs">${display}</span>`);
|
|
33
|
+
return text;
|
|
34
|
+
};
|
|
35
|
+
export const date = (datetime) => {
|
|
36
|
+
return datetime.slice(0, 10).replace(/-/g, '/');
|
|
37
|
+
};
|
|
38
|
+
export const datetime = (datetime) => {
|
|
39
|
+
return datetime.slice(0, 16).replace(/-/g, '/').replace('T', ' ');
|
|
40
|
+
};
|
|
41
|
+
export const datetimeForInput = (datetime) => {
|
|
42
|
+
return datetime.slice(0, 16);
|
|
43
|
+
};
|
|
44
|
+
export const truncateWithEllipsis = (text, maxLength) => {
|
|
45
|
+
return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
|
|
46
|
+
};
|
|
47
|
+
const escapeHtml = (text) => text
|
|
48
|
+
.replace(/&/g, '&')
|
|
49
|
+
.replace(/</g, '<')
|
|
50
|
+
.replace(/>/g, '>')
|
|
51
|
+
.replace(/"/g, '"')
|
|
52
|
+
.replace(/'/g, ''');
|
|
53
|
+
const unescapeHtml = (text) => text
|
|
54
|
+
.replace(/&/g, '&')
|
|
55
|
+
.replace(/</g, '<')
|
|
56
|
+
.replace(/>/g, '>')
|
|
57
|
+
.replace(/"/g, '"')
|
|
58
|
+
.replace(/'/g, "'");
|
|
59
|
+
const sanitizeUrl = (url) => {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = new URL(unescapeHtml(url));
|
|
62
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:')
|
|
63
|
+
return parsed.href;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// 不正なURL形式
|
|
67
|
+
}
|
|
68
|
+
return '#';
|
|
69
|
+
};
|