diff --git a/client/src/components/LinkListItem/components/ActionButtons/DeleteLinkActionButton.tsx b/client/src/components/LinkListItem/ActionButtons/DeleteLinkActionButton.tsx
similarity index 67%
rename from client/src/components/LinkListItem/components/ActionButtons/DeleteLinkActionButton.tsx
rename to client/src/components/LinkListItem/ActionButtons/DeleteLinkActionButton.tsx
index 1c77ad0..1fe066c 100644
--- a/client/src/components/LinkListItem/components/ActionButtons/DeleteLinkActionButton.tsx
+++ b/client/src/components/LinkListItem/ActionButtons/DeleteLinkActionButton.tsx
@@ -1,9 +1,9 @@
-import Link from '../../../../models/Link.ts';
-import {useLinkStore} from '../../../../contexts/AppContext.tsx';
-import useAsyncAction from '../../../../hooks/useAsyncAction.ts';
-import Button from '../../../Button.tsx';
+import Link from '../../../models/Link.ts';
+import {useLinkStore} from '../../../contexts/AppContext.tsx';
+import useAsyncAction from '../../../hooks/useAsyncAction.ts';
+import Button from '../../Button.tsx';
import {useState} from 'react';
-import Modal from '../../../Modal.tsx';
+import Modal from '../../Modal.tsx';
const DeleteLinkActionButton = ({ id }: Pick ) => {
const { deleteLink } = useLinkStore();
@@ -16,12 +16,10 @@ const DeleteLinkActionButton = ({ id }: Pick ) => {
return (
<>
>({ title: link.title, tags: link.tags });
+ const {loading, execute} = useAsyncAction(() => updateLink(link.id, updateState));
+
+ const handleOnEditClick = () => {
+ setUpdateState({ title: link.title, tags: link.tags });
+ setUpdating(true);
+ };
+
+ const handleOnApplyClick = async () => {
+ await execute();
+ setUpdating(false);
+ };
+
+ const handleOnRemoveTag = (tag: string) => {
+ setUpdateState((prevState) => ({ ...prevState, tags: prevState.tags.filter((t) => t !== tag) }));
+ };
+
+ const handleOnAddTag = (tag: string) => {
+ setUpdateState((prevState) => ({ ...prevState, tags: [...prevState.tags, tag] }));
+ };
+
+ const handleOnTitleUpdate = (title: string) => {
+ setUpdateState((prevState) => ({ ...prevState, title }));
+ }
+
return (
<>
-
+ setUpdating(false)}
+ onEditClick={handleOnEditClick} />
-
-
+
+
>
);
diff --git a/client/src/components/LinkListItem/LinkListItemActionPanel.tsx b/client/src/components/LinkListItem/LinkListItemActionPanel.tsx
new file mode 100644
index 0000000..20207ca
--- /dev/null
+++ b/client/src/components/LinkListItem/LinkListItemActionPanel.tsx
@@ -0,0 +1,52 @@
+import LikeLinkActionButton from './ActionButtons/LikeLinkActionButton.tsx';
+import SaveLinkActionButton from './ActionButtons/SaveLinkActionButton.tsx';
+import DeleteLinkActionButton from './ActionButtons/DeleteLinkActionButton.tsx';
+import Link from '../../models/Link.ts';
+import Button from '../Button.tsx';
+
+type LinkListItemActionPanelProps = {
+ disabled?: boolean;
+ onEditClick: () => void;
+ onCancelClick: () => void;
+ onApplyClick: () => void;
+ link: Link;
+ updating: boolean;
+};
+
+const LinkListItemActionPanel = ({ link, disabled, updating, onApplyClick, onCancelClick, onEditClick}: LinkListItemActionPanelProps) => {
+ return (
+
+
+
+
+
+ { !updating && link.editable && (
+
+ )}
+ { updating && (
+
+ )}
+
+ );
+};
+
+export default LinkListItemActionPanel;
\ No newline at end of file
diff --git a/client/src/components/LinkListItem/components/LinkListItemAuthor.tsx b/client/src/components/LinkListItem/LinkListItemAuthor.tsx
similarity index 88%
rename from client/src/components/LinkListItem/components/LinkListItemAuthor.tsx
rename to client/src/components/LinkListItem/LinkListItemAuthor.tsx
index d69e8ac..bdead66 100644
--- a/client/src/components/LinkListItem/components/LinkListItemAuthor.tsx
+++ b/client/src/components/LinkListItem/LinkListItemAuthor.tsx
@@ -1,4 +1,4 @@
-import Link from '../../../models/Link.ts';
+import Link from '../../models/Link.ts';
const LinkListItemAuthor = ({ user, createdAt }: Pick ) => (
diff --git a/client/src/components/LinkListItem/components/LinkListItemContent.tsx b/client/src/components/LinkListItem/LinkListItemContent.tsx
similarity index 76%
rename from client/src/components/LinkListItem/components/LinkListItemContent.tsx
rename to client/src/components/LinkListItem/LinkListItemContent.tsx
index 3828809..6cfbf69 100644
--- a/client/src/components/LinkListItem/components/LinkListItemContent.tsx
+++ b/client/src/components/LinkListItem/LinkListItemContent.tsx
@@ -1,6 +1,6 @@
import YoutubeVideoContent from './Contents/YoutubeVideoContent.tsx';
-import Link from '../../../models/Link.ts';
-import LinkType from '../../../models/LinkType.ts';
+import Link from '../../models/Link.ts';
+import LinkType from '../../models/LinkType.ts';
const LinkListItemContent = ({ type, youtube }: Pick ) => {
if (type === LinkType.Youtube) {
diff --git a/client/src/components/LinkListItem/LinkListItemTags.tsx b/client/src/components/LinkListItem/LinkListItemTags.tsx
new file mode 100644
index 0000000..a1498be
--- /dev/null
+++ b/client/src/components/LinkListItem/LinkListItemTags.tsx
@@ -0,0 +1,39 @@
+import {useStore} from '../../contexts/AppContext.tsx';
+import LinkStore from '../../stores/LinkStore.ts';
+import {observer} from 'mobx-react-lite';
+import TagBadge from '../TagBadge.tsx';
+import {MaxTags} from '../../constants/preferences.ts';
+import AddTagButton from './AddTagButton.tsx';
+
+type LinkListItemTagsProps = {
+ tags: string[];
+ editable?: boolean;
+ error?: string;
+ onAdd?: (tag: string) => void;
+ onRemove?: (tag: string) => void;
+};
+
+const LinkListItemTags = observer(({ tags, editable, error, onAdd, onRemove }: LinkListItemTagsProps) => {
+ const { toggleTagFilter } = useStore(LinkStore);
+ const handleOnClick = (tag: string) => {
+ if (!editable) {
+ toggleTagFilter(tag);
+ } else if (onRemove) {
+ onRemove(tag);
+ }
+ };
+
+ return (
+ <>
+
+ {tags.map((tag) => (
+
handleOnClick(tag)} name={tag} removable={editable} active />
+ ))}
+ { editable && tags.length < MaxTags && onAdd && onAdd(tag)} />}
+
+ { editable && error && ({error} ) }
+ >
+ );
+});
+
+export default LinkListItemTags;
\ No newline at end of file
diff --git a/client/src/components/LinkListItem/LinkListItemTitle.tsx b/client/src/components/LinkListItem/LinkListItemTitle.tsx
new file mode 100644
index 0000000..e03274b
--- /dev/null
+++ b/client/src/components/LinkListItem/LinkListItemTitle.tsx
@@ -0,0 +1,47 @@
+import { ChangeEvent, useState } from 'react';
+import {MaxTitleLength} from '../../constants/preferences.ts';
+
+type LinkListItemTitleProps = {
+ editable?: boolean;
+ title: string;
+ onUpdate?: (title: string) => void;
+ error?: string;
+};
+
+const LinkListItemTitle = ({ title, onUpdate, editable, error }: LinkListItemTitleProps) => {
+ const [value, setValue] = useState(title);
+
+ const handleInputChange = (event: ChangeEvent) => {
+ setValue(event.target.value);
+ };
+
+ const handleEditEnd = () => {
+ if (value !== title && onUpdate) {
+ onUpdate(value);
+ }
+ };
+
+ return (
+ <>
+ { editable && (
+
+ )}
+ { !editable && (
+
+ { value.length > MaxTitleLength ? `${value.slice(0, MaxTitleLength)}` : value }
+
+ )}
+ { error && ({error} ) }
+ >
+ );
+}
+
+export default LinkListItemTitle;
\ No newline at end of file
diff --git a/client/src/components/TagSearchInput.tsx b/client/src/components/LinkListItem/TagSearchInput.tsx
similarity index 93%
rename from client/src/components/TagSearchInput.tsx
rename to client/src/components/LinkListItem/TagSearchInput.tsx
index 6f40632..6b1ecd5 100644
--- a/client/src/components/TagSearchInput.tsx
+++ b/client/src/components/LinkListItem/TagSearchInput.tsx
@@ -1,10 +1,10 @@
import React, {useEffect, useState} from 'react';
-import useClickOutsideHandler from '../hooks/useClickOutsideHandler.ts';
+import useClickOutsideHandler from '../../hooks/useClickOutsideHandler.ts';
import {observer} from 'mobx-react-lite';
-import {useLinkStore} from '../contexts/AppContext.tsx';
-import TagBadge from '../components/TagBadge.tsx';
-import SearchIcon from '../components/SearchIcon.tsx';
-import Button from './Button.tsx';
+import {useLinkStore} from '../../contexts/AppContext.tsx';
+import TagBadge from '../TagBadge.tsx';
+import SearchIcon from '../SearchIcon.tsx';
+import Button from '../Button.tsx';
type TagSearchProps = {
showSuggestionsInitial?: boolean;
diff --git a/client/src/components/LinkListItem/components/LinkListItemActionButtons.tsx b/client/src/components/LinkListItem/components/LinkListItemActionButtons.tsx
deleted file mode 100644
index 91a702e..0000000
--- a/client/src/components/LinkListItem/components/LinkListItemActionButtons.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import LikeLinkActionButton from './ActionButtons/LikeLinkActionButton.tsx';
-import SaveLinkActionButton from './ActionButtons/SaveLinkActionButton.tsx';
-import DeleteLinkActionButton from './ActionButtons/DeleteLinkActionButton.tsx';
-import Link from '../../../models/Link.ts';
-
-const LinkListItemActionButtons = (link: Link) => {
- return (
-
-
-
-
-
- { link.editable && (
-
-
-
- )}
-
- );
-};
-
-export default LinkListItemActionButtons;
\ No newline at end of file
diff --git a/client/src/components/LinkListItem/components/LinkListItemTags.tsx b/client/src/components/LinkListItem/components/LinkListItemTags.tsx
deleted file mode 100644
index 5907778..0000000
--- a/client/src/components/LinkListItem/components/LinkListItemTags.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import {useStore} from '../../../contexts/AppContext.tsx';
-import LinkStore from '../../../stores/LinkStore.ts';
-import {observer} from 'mobx-react-lite';
-import TagBadge from '../../TagBadge.tsx';
-
-const LinkListItemTags = observer(({ tags }: { tags: string[]}) => {
- const { toggleTagFilter } = useStore(LinkStore);
-
- return (
-
- {tags.map((tag) => (
- toggleTagFilter(tag)} active />
- ))}
-
- );
-});
-
-export default LinkListItemTags;
\ No newline at end of file
diff --git a/client/src/components/LinkListItem/components/LinkListItemTitle.tsx b/client/src/components/LinkListItem/components/LinkListItemTitle.tsx
deleted file mode 100644
index 8a43aa6..0000000
--- a/client/src/components/LinkListItem/components/LinkListItemTitle.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import {MaxTitleLength} from '../../../constants/preferences.ts';
-
-const LinkListItemTitle = ({ title }: { title: string}) => {
- return (
-
- { title?.length > MaxTitleLength ? `${title?.slice(0, MaxTitleLength - 3)}...` : title }
-
- );
-}
-
-export default LinkListItemTitle;
\ No newline at end of file
diff --git a/client/src/components/LinkListToolbar/LinkListToolbar.tsx b/client/src/components/LinkListToolbar/LinkListToolbar.tsx
index 18ea23e..3c30eea 100644
--- a/client/src/components/LinkListToolbar/LinkListToolbar.tsx
+++ b/client/src/components/LinkListToolbar/LinkListToolbar.tsx
@@ -1,14 +1,16 @@
import TopTagsList from './components/TopTagsList.tsx';
import SearchByTitleInput from './components/SearchByTitleInput.tsx';
-import TagSearch from './components/TagSearch.tsx';
import UserInteractionsFilter from './components/UserInteractionsFilter.tsx';
+import {useLinkStore} from '../../contexts/AppContext.tsx';
+import TagSearchInput from '../LinkListItem/TagSearchInput.tsx';
const LinkListToolbar = () => {
+ const { toggleTagFilter } = useLinkStore();
return (
-
+
diff --git a/client/src/components/LinkListToolbar/components/TagSearch.tsx b/client/src/components/LinkListToolbar/components/TagSearch.tsx
deleted file mode 100644
index 1fa6c2d..0000000
--- a/client/src/components/LinkListToolbar/components/TagSearch.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import {useLinkStore} from '../../../contexts/AppContext.tsx';
-import TagSearchInput from '../../TagSearchInput.tsx';
-
-const TagSearch = () => {
- const { toggleTagFilter } = useLinkStore();
- return
;
-};
-
-export default TagSearch;
\ No newline at end of file
diff --git a/client/src/screens/AddLink/components/AddLinkForm.tsx b/client/src/screens/AddLink/components/AddLinkForm.tsx
index d8ec3e9..f47fd08 100644
--- a/client/src/screens/AddLink/components/AddLinkForm.tsx
+++ b/client/src/screens/AddLink/components/AddLinkForm.tsx
@@ -1,14 +1,12 @@
import {useLinkStore} from '../../../contexts/AppContext.tsx';
-import TagBadge from '../../../components/TagBadge.tsx';
-import AddTagButton from './AddTagButton.tsx';
-import {MaxTags} from '../../../constants/preferences.ts';
-import TitleInput from './TitleInput.tsx';
import SubmitButton from '../../../components/SubmitButton.tsx';
import ErrorAlert from '../../../components/ErrorAlert.tsx';
import useSimpleReducer from '../../../hooks/useSimpleReducer.ts';
import {handleError} from '../../../utils/errors.ts';
import PreviewLink from '../../../models/PreviewLink.ts';
-import LinkListItemContent from '../../../components/LinkListItem/components/LinkListItemContent.tsx';
+import LinkListItemContent from '../../../components/LinkListItem/LinkListItemContent.tsx';
+import LinkListItemTitle from '../../../components/LinkListItem/LinkListItemTitle.tsx';
+import LinkListItemTags from '../../../components/LinkListItem/LinkListItemTags.tsx';
type LocalState = {
isSubmitting: boolean;
@@ -78,15 +76,8 @@ const AddLinkForm = ({ onSuccess, link }: AddLinkFormProps) => {
-
- { state.titleError && (
{state.titleError} ) }
-
- {link.tags.map((tag) => (
-
removeTag(tag)} name={tag} removable active />
- ))}
- { link.tags.length < MaxTags && }
-
- { state.tagsError && (
{state.tagsError} ) }
+
+
Submit
diff --git a/client/src/screens/AddLink/components/TitleInput.tsx b/client/src/screens/AddLink/components/TitleInput.tsx
deleted file mode 100644
index 6c798da..0000000
--- a/client/src/screens/AddLink/components/TitleInput.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { ChangeEvent, useState } from 'react';
-import {MaxTitleLength} from '../../../constants/preferences.ts';
-
-type TitleInputProps = {
- initialTitle: string;
- onUpdate: (title: string) => void;
-};
-
-const TitleInput = ({ initialTitle, onUpdate }: TitleInputProps) => {
- const [isEditing, setIsEditing] = useState(false);
- const [title, setTitle] = useState(initialTitle);
-
- const handleEditStart = () => {
- setIsEditing(true);
- };
-
- const handleInputChange = (event: ChangeEvent) => {
- setTitle(event.target.value);
- };
-
- const handleEditEnd = () => {
- setIsEditing(false);
- if (title !== initialTitle) {
- onUpdate(title);
- }
- };
-
- return isEditing || initialTitle.length === 0 ? (
-
- ) : (
-
- { title.length > MaxTitleLength ? `${title.slice(0, MaxTitleLength)}...` : title }
-
- );
-}
-
-export default TitleInput;
\ No newline at end of file
diff --git a/client/src/stores/LinkStore.ts b/client/src/stores/LinkStore.ts
index 2aa0b90..d89ede6 100644
--- a/client/src/stores/LinkStore.ts
+++ b/client/src/stores/LinkStore.ts
@@ -91,9 +91,7 @@ class LinkStore {
};
public loadMore = async () => {
- console.log('loadMore')
if (this.state.paginationState.hasNextPage) {
- console.log('loadMore2')
const response = await this.linkService.getList({
...this.paginationParams,
...this.state.filter,
@@ -212,7 +210,7 @@ class LinkStore {
return {};
};
- public updatePreviewLink = (updates: Partial) => {
+ public updatePreviewLink = (updates: Partial>) => {
if (!this.state.preview.link) {
throw new Error('Preview Link not found');
}
@@ -223,6 +221,44 @@ class LinkStore {
};
};
+ public updateLink = async (linkId: string, update: Pick ) => {
+ await this.linkService.update({ linkId, ...update });
+
+ const linkIndex = this.state.links.findIndex(link => link.id === linkId);
+ if (linkIndex === -1) {
+ throw new Error('Link not found');
+ }
+
+ const link = this.state.links[linkIndex];
+ const removedTags = link.tags.filter(tag => !update.tags.includes(tag));
+ const addedTags = update.tags.filter(tag => !link.tags.includes(tag));
+ const updatedTags = this.state.tags.map(tag => {
+ if (removedTags.includes(tag.name)) {
+ return {
+ ...tag,
+ count: tag.count - 1
+ };
+ }
+
+ if (addedTags.includes(tag.name)) {
+ return {
+ ...tag,
+ count: tag.count + 1
+ };
+ }
+
+ return tag;
+ }).filter(x => x.count > 0);
+ runInAction(() => {
+ this.state.links[linkIndex] = {
+ ...link,
+ ...update,
+ };
+ this.state.tags = updatedTags;
+ });
+
+ }
+
public submitLink = async (): Promise => {
if (!this.state.preview.link) {
throw new Error('Preview Link not found');
diff --git a/client/src/utils/errors.ts b/client/src/utils/errors.ts
index bce293d..1a78618 100644
--- a/client/src/utils/errors.ts
+++ b/client/src/utils/errors.ts
@@ -3,7 +3,6 @@ import FetchHttpResponseError from '../services/HttpClient/errors/FetchHttpRespo
import FetchHttpResponseValidationError from '../services/HttpClient/errors/FetchHttpResponseValidationError.ts';
export const handleError = (error: unknown): string | undefined => {
- console.log(error)
if (error instanceof FetchHttpResponseBusinessError) {
return error.message;
}
diff --git a/server/src/ShareLink.Application/UpdateLinkHandler/UpdateLinkHandler.cs b/server/src/ShareLink.Application/UpdateLinkHandler/UpdateLinkHandler.cs
index 822291b..7d458d8 100644
--- a/server/src/ShareLink.Application/UpdateLinkHandler/UpdateLinkHandler.cs
+++ b/server/src/ShareLink.Application/UpdateLinkHandler/UpdateLinkHandler.cs
@@ -17,7 +17,9 @@ public async Task Handle(UpdateLinkRequest request, CancellationToken cancellati
throw new UserUnauthorizedException();
}
- var link = await context.Links.SingleOrDefaultAsync(x => x.Id == request.LinkId && x.UserId == userId, cancellationToken);
+ var link = await context.Links
+ .Include(x => x.Tags)
+ .SingleOrDefaultAsync(x => x.Id == request.LinkId && x.UserId == userId, cancellationToken);
if (link is null)
{
throw new BusinessException(ErrorCodes.LinkNotFound);