mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Add link support to resource creation and editing, including validation
This commit is contained in:
@@ -128,7 +128,7 @@ export function UploadClipboardImageButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageDrapArea({
|
export function ImageDropArea({
|
||||||
children,
|
children,
|
||||||
onUploaded,
|
onUploaded,
|
||||||
}: {
|
}: {
|
||||||
|
@@ -42,6 +42,7 @@ export interface TagWithCount extends Tag {
|
|||||||
export interface CreateResourceParams {
|
export interface CreateResourceParams {
|
||||||
title: string;
|
title: string;
|
||||||
alternative_titles: string[];
|
alternative_titles: string[];
|
||||||
|
links: RLink[];
|
||||||
tags: number[];
|
tags: number[];
|
||||||
article: string;
|
article: string;
|
||||||
images: number[];
|
images: number[];
|
||||||
@@ -53,6 +54,11 @@ export interface Image {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RLink {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -66,6 +72,7 @@ export interface ResourceDetails {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
alternativeTitles: string[];
|
alternativeTitles: string[];
|
||||||
|
links: RLink[];
|
||||||
article: string;
|
article: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
|
@@ -16,7 +16,7 @@ import { ErrorAlert } from "../components/alert.tsx";
|
|||||||
import Loading from "../components/loading.tsx";
|
import Loading from "../components/loading.tsx";
|
||||||
import TagInput, { QuickAddTagDialog } from "../components/tag_input.tsx";
|
import TagInput, { QuickAddTagDialog } from "../components/tag_input.tsx";
|
||||||
import {
|
import {
|
||||||
ImageDrapArea,
|
ImageDropArea,
|
||||||
SelectAndUploadImageButton,
|
SelectAndUploadImageButton,
|
||||||
UploadClipboardImageButton,
|
UploadClipboardImageButton,
|
||||||
} from "../components/image_selector.tsx";
|
} from "../components/image_selector.tsx";
|
||||||
@@ -27,6 +27,7 @@ export default function EditResourcePage() {
|
|||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [article, setArticle] = useState<string>("");
|
const [article, setArticle] = useState<string>("");
|
||||||
const [images, setImages] = useState<number[]>([]);
|
const [images, setImages] = useState<number[]>([]);
|
||||||
|
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
@@ -53,6 +54,7 @@ export default function EditResourcePage() {
|
|||||||
setTags(data.tags);
|
setTags(data.tags);
|
||||||
setArticle(data.article);
|
setArticle(data.article);
|
||||||
setImages(data.images.map((i) => i.id));
|
setImages(data.images.map((i) => i.id));
|
||||||
|
setLinks(data.links);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
showToast({ message: t("Failed to load resource"), type: "error" });
|
showToast({ message: t("Failed to load resource"), type: "error" });
|
||||||
@@ -74,6 +76,12 @@ export default function EditResourcePage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
if (!links[i].label || !links[i].url) {
|
||||||
|
setError(t("Link cannot be empty"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!tags || tags.length === 0) {
|
if (!tags || tags.length === 0) {
|
||||||
setError(t("At least one tag required"));
|
setError(t("At least one tag required"));
|
||||||
return;
|
return;
|
||||||
@@ -89,6 +97,7 @@ export default function EditResourcePage() {
|
|||||||
tags: tags.map((tag) => tag.id),
|
tags: tags.map((tag) => tag.id),
|
||||||
article: article,
|
article: article,
|
||||||
images: images,
|
images: images,
|
||||||
|
links: links,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@@ -117,7 +126,7 @@ export default function EditResourcePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageDrapArea
|
<ImageDropArea
|
||||||
onUploaded={(images) => {
|
onUploaded={(images) => {
|
||||||
setImages((prev) => [...prev, ...images]);
|
setImages((prev) => [...prev, ...images]);
|
||||||
}}
|
}}
|
||||||
@@ -175,6 +184,61 @@ export default function EditResourcePage() {
|
|||||||
{t("Add Alternative Title")}
|
{t("Add Alternative Title")}
|
||||||
</button>
|
</button>
|
||||||
<div className={"h-2"}></div>
|
<div className={"h-2"}></div>
|
||||||
|
<p className={"my-1"}>{t("Links")}</p>
|
||||||
|
<div className={"flex flex-col"}>
|
||||||
|
{links.map((link, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className={"flex items-center my-2"}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
placeholder={t("Label")}
|
||||||
|
value={link.label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLinks = [...links];
|
||||||
|
newLinks[index].label = e.target.value;
|
||||||
|
setLinks(newLinks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input w-full ml-2"
|
||||||
|
placeholder={t("URL")}
|
||||||
|
value={link.url}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLinks = [...links];
|
||||||
|
newLinks[index].url = e.target.value;
|
||||||
|
setLinks(newLinks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={"btn btn-square btn-error ml-2"}
|
||||||
|
type={"button"}
|
||||||
|
onClick={() => {
|
||||||
|
const newLinks = [...links];
|
||||||
|
newLinks.splice(index, 1);
|
||||||
|
setLinks(newLinks);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdDelete size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className={"flex"}>
|
||||||
|
<button
|
||||||
|
className={"btn my-2"}
|
||||||
|
type={"button"}
|
||||||
|
onClick={() => {
|
||||||
|
setLinks([...links, { label: "", url: "" }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdAdd />
|
||||||
|
{t("Add Link")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"h-2"}></div>
|
||||||
<p className={"my-1"}>{t("Tags")}</p>
|
<p className={"my-1"}>{t("Tags")}</p>
|
||||||
<p className={"my-1 pb-1"}>
|
<p className={"my-1 pb-1"}>
|
||||||
{tags.map((tag, index) => {
|
{tags.map((tag, index) => {
|
||||||
@@ -343,6 +407,6 @@ export default function EditResourcePage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ImageDrapArea>
|
</ImageDropArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ import { ErrorAlert } from "../components/alert.tsx";
|
|||||||
import { useAppContext } from "../components/AppContext.tsx";
|
import { useAppContext } from "../components/AppContext.tsx";
|
||||||
import TagInput, { QuickAddTagDialog } from "../components/tag_input.tsx";
|
import TagInput, { QuickAddTagDialog } from "../components/tag_input.tsx";
|
||||||
import {
|
import {
|
||||||
ImageDrapArea,
|
ImageDropArea,
|
||||||
SelectAndUploadImageButton,
|
SelectAndUploadImageButton,
|
||||||
UploadClipboardImageButton,
|
UploadClipboardImageButton,
|
||||||
} from "../components/image_selector.tsx";
|
} from "../components/image_selector.tsx";
|
||||||
@@ -26,6 +26,7 @@ export default function PublishPage() {
|
|||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [article, setArticle] = useState<string>("");
|
const [article, setArticle] = useState<string>("");
|
||||||
const [images, setImages] = useState<number[]>([]);
|
const [images, setImages] = useState<number[]>([]);
|
||||||
|
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
@@ -83,6 +84,12 @@ export default function PublishPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
if (!links[i].label || !links[i].url) {
|
||||||
|
setError(t("Link cannot be empty"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!tags || tags.length === 0) {
|
if (!tags || tags.length === 0) {
|
||||||
setError(t("At least one tag required"));
|
setError(t("At least one tag required"));
|
||||||
return;
|
return;
|
||||||
@@ -98,6 +105,7 @@ export default function PublishPage() {
|
|||||||
tags: tags.map((tag) => tag.id),
|
tags: tags.map((tag) => tag.id),
|
||||||
article: article,
|
article: article,
|
||||||
images: images,
|
images: images,
|
||||||
|
links: links,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
localStorage.removeItem("publish_data");
|
localStorage.removeItem("publish_data");
|
||||||
@@ -129,7 +137,7 @@ export default function PublishPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageDrapArea
|
<ImageDropArea
|
||||||
onUploaded={(images) => {
|
onUploaded={(images) => {
|
||||||
setImages((prev) => [...prev, ...images]);
|
setImages((prev) => [...prev, ...images]);
|
||||||
}}
|
}}
|
||||||
@@ -187,6 +195,61 @@ export default function PublishPage() {
|
|||||||
{t("Add Alternative Title")}
|
{t("Add Alternative Title")}
|
||||||
</button>
|
</button>
|
||||||
<div className={"h-2"}></div>
|
<div className={"h-2"}></div>
|
||||||
|
<p className={"my-1"}>{t("Links")}</p>
|
||||||
|
<div className={"flex flex-col"}>
|
||||||
|
{links.map((link, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className={"flex items-center my-2"}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
placeholder={t("Label")}
|
||||||
|
value={link.label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLinks = [...links];
|
||||||
|
newLinks[index].label = e.target.value;
|
||||||
|
setLinks(newLinks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input w-full ml-2"
|
||||||
|
placeholder={t("URL")}
|
||||||
|
value={link.url}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLinks = [...links];
|
||||||
|
newLinks[index].url = e.target.value;
|
||||||
|
setLinks(newLinks);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={"btn btn-square btn-error ml-2"}
|
||||||
|
type={"button"}
|
||||||
|
onClick={() => {
|
||||||
|
const newLinks = [...links];
|
||||||
|
newLinks.splice(index, 1);
|
||||||
|
setLinks(newLinks);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdDelete size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className={"flex"}>
|
||||||
|
<button
|
||||||
|
className={"btn my-2"}
|
||||||
|
type={"button"}
|
||||||
|
onClick={() => {
|
||||||
|
setLinks([...links, { label: "", url: "" }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdAdd />
|
||||||
|
{t("Add Link")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"h-2"}></div>
|
||||||
<p className={"my-1"}>{t("Tags")}</p>
|
<p className={"my-1"}>{t("Tags")}</p>
|
||||||
<p className={"my-1 pb-1"}>
|
<p className={"my-1 pb-1"}>
|
||||||
{tags.map((tag, index) => {
|
{tags.map((tag, index) => {
|
||||||
@@ -355,6 +418,6 @@ export default function PublishPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ImageDrapArea>
|
</ImageDropArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,7 @@ import {
|
|||||||
MdOutlineDownload,
|
MdOutlineDownload,
|
||||||
MdOutlineEdit,
|
MdOutlineEdit,
|
||||||
MdOutlineImage,
|
MdOutlineImage,
|
||||||
|
MdOutlineLink,
|
||||||
MdOutlineOpenInNew,
|
MdOutlineOpenInNew,
|
||||||
} from "react-icons/md";
|
} from "react-icons/md";
|
||||||
import { app } from "../app.ts";
|
import { app } from "../app.ts";
|
||||||
@@ -48,6 +49,7 @@ import Badge, { BadgeAccent } from "../components/badge.tsx";
|
|||||||
import Input, { TextArea } from "../components/input.tsx";
|
import Input, { TextArea } from "../components/input.tsx";
|
||||||
import { useAppContext } from "../components/AppContext.tsx";
|
import { useAppContext } from "../components/AppContext.tsx";
|
||||||
import { ImageGrid, SquareImage } from "../components/image.tsx";
|
import { ImageGrid, SquareImage } from "../components/image.tsx";
|
||||||
|
import { BiLogoSteam } from "react-icons/bi";
|
||||||
|
|
||||||
export default function ResourcePage() {
|
export default function ResourcePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -182,6 +184,27 @@ export default function ResourcePage() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<Tags tags={resource.tags} />
|
<Tags tags={resource.tags} />
|
||||||
|
{resource.links && (
|
||||||
|
<p className={"px-3 mt-2"}>
|
||||||
|
{resource.links.map((l) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"py-1 px-3 inline-flex items-center m-1 border border-base-300 rounded-2xl hover:bg-base-200 transition-colors cursor-pointer select-none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{l.url.includes("steampowered.com") ? (
|
||||||
|
<BiLogoSteam size={20} />
|
||||||
|
) : (
|
||||||
|
<MdOutlineLink size={20} />
|
||||||
|
)}
|
||||||
|
<span className={"ml-2 text-sm"}>{l.label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="tabs tabs-box my-4 mx-2 p-4">
|
<div className="tabs tabs-box my-4 mx-2 p-4">
|
||||||
<label className="tab transition-all">
|
<label className="tab transition-all">
|
||||||
<input
|
<input
|
||||||
|
@@ -24,7 +24,7 @@ func updateSiteMapAndRss(baseURL string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleCreateResource(c fiber.Ctx) error {
|
func handleCreateResource(c fiber.Ctx) error {
|
||||||
var params service.ResourceCreateParams
|
var params service.ResourceParams
|
||||||
body := c.Body()
|
body := c.Body()
|
||||||
err := json.Unmarshal(body, ¶ms)
|
err := json.Unmarshal(body, ¶ms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -229,7 +229,7 @@ func handleUpdateResource(c fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return model.NewRequestError("Invalid resource ID")
|
return model.NewRequestError("Invalid resource ID")
|
||||||
}
|
}
|
||||||
var params service.ResourceCreateParams
|
var params service.ResourceParams
|
||||||
body := c.Body()
|
body := c.Body()
|
||||||
err = json.Unmarshal(body, ¶ms)
|
err = json.Unmarshal(body, ¶ms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -10,6 +10,7 @@ type Resource struct {
|
|||||||
gorm.Model
|
gorm.Model
|
||||||
Title string
|
Title string
|
||||||
AlternativeTitles []string `gorm:"serializer:json"`
|
AlternativeTitles []string `gorm:"serializer:json"`
|
||||||
|
Links []Link `gorm:"serializer:json"`
|
||||||
Article string
|
Article string
|
||||||
Images []Image `gorm:"many2many:resource_images;"`
|
Images []Image `gorm:"many2many:resource_images;"`
|
||||||
Tags []Tag `gorm:"many2many:resource_tags;"`
|
Tags []Tag `gorm:"many2many:resource_tags;"`
|
||||||
@@ -20,6 +21,11 @@ type Resource struct {
|
|||||||
Downloads uint
|
Downloads uint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
type ResourceView struct {
|
type ResourceView struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -33,6 +39,7 @@ type ResourceDetailView struct {
|
|||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
AlternativeTitles []string `json:"alternativeTitles"`
|
AlternativeTitles []string `json:"alternativeTitles"`
|
||||||
|
Links []Link `json:"links"`
|
||||||
Article string `json:"article"`
|
Article string `json:"article"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Tags []TagView `json:"tags"`
|
Tags []TagView `json:"tags"`
|
||||||
@@ -84,6 +91,7 @@ func (r *Resource) ToDetailView() ResourceDetailView {
|
|||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
AlternativeTitles: r.AlternativeTitles,
|
AlternativeTitles: r.AlternativeTitles,
|
||||||
|
Links: r.Links,
|
||||||
Article: r.Article,
|
Article: r.Article,
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
@@ -12,15 +12,16 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceCreateParams struct {
|
type ResourceParams struct {
|
||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required"`
|
||||||
AlternativeTitles []string `json:"alternative_titles"`
|
AlternativeTitles []string `json:"alternative_titles"`
|
||||||
|
Links []model.Link `json:"links"`
|
||||||
Tags []uint `json:"tags"`
|
Tags []uint `json:"tags"`
|
||||||
Article string `json:"article"`
|
Article string `json:"article"`
|
||||||
Images []uint `json:"images"`
|
Images []uint `json:"images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateResource(uid uint, params *ResourceCreateParams) (uint, error) {
|
func CreateResource(uid uint, params *ResourceParams) (uint, error) {
|
||||||
canUpload, err := checkUserCanUpload(uid)
|
canUpload, err := checkUserCanUpload(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -49,6 +50,7 @@ func CreateResource(uid uint, params *ResourceCreateParams) (uint, error) {
|
|||||||
Title: params.Title,
|
Title: params.Title,
|
||||||
AlternativeTitles: params.AlternativeTitles,
|
AlternativeTitles: params.AlternativeTitles,
|
||||||
Article: params.Article,
|
Article: params.Article,
|
||||||
|
Links: params.Links,
|
||||||
Images: images,
|
Images: images,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
UserID: uid,
|
UserID: uid,
|
||||||
@@ -213,7 +215,7 @@ func GetResourcesWithUser(username string, page int) ([]model.ResourceView, int,
|
|||||||
return views, totalPages, nil
|
return views, totalPages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EditResource(uid, rid uint, params *ResourceCreateParams) error {
|
func EditResource(uid, rid uint, params *ResourceParams) error {
|
||||||
isAdmin, err := checkUserCanUpload(uid)
|
isAdmin, err := checkUserCanUpload(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("checkUserCanUpload error: ", err)
|
log.Error("checkUserCanUpload error: ", err)
|
||||||
@@ -230,6 +232,7 @@ func EditResource(uid, rid uint, params *ResourceCreateParams) error {
|
|||||||
r.Title = params.Title
|
r.Title = params.Title
|
||||||
r.AlternativeTitles = params.AlternativeTitles
|
r.AlternativeTitles = params.AlternativeTitles
|
||||||
r.Article = params.Article
|
r.Article = params.Article
|
||||||
|
r.Links = params.Links
|
||||||
|
|
||||||
images := make([]model.Image, len(params.Images))
|
images := make([]model.Image, len(params.Images))
|
||||||
for i, id := range params.Images {
|
for i, id := range params.Images {
|
||||||
|
Reference in New Issue
Block a user