Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
723e6bcd
Commit
723e6bcd
authored
Sep 15, 2023
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: update resources page
parent
d1156aa7
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
71 additions
and
400 deletions
+71
-400
ResourceCard.tsx
web/src/components/ResourceCard.tsx
+1
-7
ResourceItemDropdown.tsx
web/src/components/ResourceItemDropdown.tsx
+0
-93
ResourceSearchBar.tsx
web/src/components/ResourceSearchBar.tsx
+0
-44
Resources.tsx
web/src/pages/Resources.tsx
+67
-0
ResourcesDashboard.tsx
web/src/pages/ResourcesDashboard.tsx
+0
-242
index.tsx
web/src/router/index.tsx
+2
-2
resource.ts
web/src/store/module/resource.ts
+1
-12
No files found.
web/src/components/ResourceCard.tsx
View file @
723e6bcd
import
{
getDateTimeString
}
from
"@/helpers/datetime"
;
import
ResourceIcon
from
"./ResourceIcon"
;
import
ResourceItemDropdown
from
"./ResourceItemDropdown"
;
import
"@/less/resource-card.less"
;
interface
Props
{
...
...
@@ -10,13 +9,8 @@ interface Props {
const
ResourceCard
=
({
resource
}:
Props
)
=>
{
return
(
<
div
className=
"resource-card"
>
<
div
className=
"w-full p-2 flex flex-row justify-end items-center absolute top-0 left-0"
>
<
div
className=
"more-action-btn"
>
<
ResourceItemDropdown
resource=
{
resource
}
/>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-center items-center pb-2 pt-4 px-2"
>
<
ResourceIcon
resource=
{
resource
}
strokeWidth=
{
1
}
/>
<
ResourceIcon
resource=
{
resource
}
strokeWidth=
{
0.5
}
/>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-center px-1 select-none"
>
<
div
className=
"w-full text-base text-center text-ellipsis overflow-hidden line-clamp-3"
>
{
resource
.
filename
}
</
div
>
...
...
web/src/components/ResourceItemDropdown.tsx
deleted
100644 → 0
View file @
d1156aa7
import
copy
from
"copy-to-clipboard"
;
import
React
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
{
useResourceStore
}
from
"@/store/module"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
getResourceType
,
getResourceUrl
}
from
"@/utils/resource"
;
import
showChangeResourceFilenameDialog
from
"./ChangeResourceFilenameDialog"
;
import
{
showCommonDialog
}
from
"./Dialog/CommonDialog"
;
import
Icon
from
"./Icon"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
Dropdown
from
"./kit/Dropdown"
;
interface
Props
{
resource
:
Resource
;
}
const
ResourceItemDropdown
=
({
resource
}:
Props
)
=>
{
const
t
=
useTranslate
();
const
resourceStore
=
useResourceStore
();
const
handlePreviewBtnClick
=
(
resource
:
Resource
)
=>
{
const
resourceUrl
=
getResourceUrl
(
resource
);
if
(
getResourceType
(
resource
).
startsWith
(
"image"
))
{
showPreviewImageDialog
([
getResourceUrl
(
resource
)],
0
);
}
else
{
window
.
open
(
resourceUrl
);
}
};
const
handleCopyResourceLinkBtnClick
=
(
resource
:
Resource
)
=>
{
const
url
=
getResourceUrl
(
resource
);
copy
(
url
);
toast
.
success
(
t
(
"message.succeed-copy-resource-link"
));
};
const
handleRenameBtnClick
=
(
resource
:
Resource
)
=>
{
showChangeResourceFilenameDialog
(
resource
.
id
,
resource
.
filename
);
};
const
handleDeleteResourceBtnClick
=
(
resource
:
Resource
)
=>
{
let
warningText
=
t
(
"resource.warning-text"
);
if
(
resource
.
linkedMemoAmount
>
0
)
{
warningText
=
warningText
+
`\n
${
t
(
"resource.linked-amount"
)}
:
${
resource
.
linkedMemoAmount
}
`
;
}
showCommonDialog
({
title
:
t
(
"resource.delete-resource"
),
content
:
warningText
,
style
:
"warning"
,
dialogName
:
"delete-resource-dialog"
,
onConfirm
:
async
()
=>
{
await
resourceStore
.
deleteResourceById
(
resource
.
id
);
},
});
};
return
(
<
Dropdown
actionsClassName=
"!w-auto min-w-[8rem]"
trigger=
{
<
Icon
.
MoreVertical
className=
"w-4 h-auto hover:opacity-80 cursor-pointer"
/>
}
actions=
{
<>
<
button
className=
"w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick=
{
()
=>
handlePreviewBtnClick
(
resource
)
}
>
{
t
(
"common.preview"
)
}
</
button
>
<
button
className=
"w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick=
{
()
=>
handleCopyResourceLinkBtnClick
(
resource
)
}
>
{
t
(
"resource.copy-link"
)
}
</
button
>
<
button
className=
"w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick=
{
()
=>
handleRenameBtnClick
(
resource
)
}
>
{
t
(
"common.rename"
)
}
</
button
>
<
button
className=
"w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick=
{
()
=>
handleDeleteResourceBtnClick
(
resource
)
}
>
{
t
(
"common.delete"
)
}
</
button
>
</>
}
/>
);
};
export
default
React
.
memo
(
ResourceItemDropdown
);
web/src/components/ResourceSearchBar.tsx
deleted
100644 → 0
View file @
d1156aa7
import
{
useRef
,
useState
}
from
"react"
;
import
useDebounce
from
"react-use/lib/useDebounce"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
Icon
from
"./Icon"
;
interface
ResourceSearchBarProps
{
setQuery
:
(
queryText
:
string
)
=>
void
;
}
const
ResourceSearchBar
=
({
setQuery
}:
ResourceSearchBarProps
)
=>
{
const
t
=
useTranslate
();
const
[
queryText
,
setQueryText
]
=
useState
(
""
);
const
inputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
handleTextQueryInput
=
(
event
:
React
.
FormEvent
<
HTMLInputElement
>
)
=>
{
const
text
=
event
.
currentTarget
.
value
;
setQueryText
(
text
);
};
useDebounce
(
()
=>
{
setQuery
(
queryText
);
},
200
,
[
queryText
]
);
return
(
<
div
className=
"w-44 sm:w-52"
>
<
div
className=
"w-full h-9 flex flex-row justify-start items-center py-2 px-3 rounded-md bg-gray-200 dark:bg-zinc-800"
>
<
Icon
.
Search
className=
"w-4 h-auto opacity-30 dark:text-gray-200"
/>
<
input
className=
"flex ml-2 w-24 grow text-sm outline-none bg-transparent dark:text-gray-200"
type=
"text"
placeholder=
{
t
(
"resource.search-bar-placeholder"
)
}
ref=
{
inputRef
}
value=
{
queryText
}
onChange=
{
handleTextQueryInput
}
/>
</
div
>
</
div
>
);
};
export
default
ResourceSearchBar
;
web/src/pages/Resources.tsx
0 → 100644
View file @
723e6bcd
import
{
useEffect
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
Empty
from
"@/components/Empty"
;
import
Icon
from
"@/components/Icon"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
ResourceCard
from
"@/components/ResourceCard"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useResourceStore
}
from
"@/store/module"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
const
Resources
=
()
=>
{
const
t
=
useTranslate
();
const
loadingState
=
useLoading
();
const
resourceStore
=
useResourceStore
();
const
resources
=
resourceStore
.
state
.
resources
;
useEffect
(()
=>
{
resourceStore
.
fetchResourceList
()
.
then
(()
=>
{
loadingState
.
setFinish
();
})
.
catch
((
error
)
=>
{
console
.
error
(
error
);
toast
.
error
(
error
.
response
.
data
.
message
);
});
},
[]);
return
(
<
section
className=
"w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"
>
<
MobileHeader
showSearch=
{
false
}
/>
<
div
className=
"w-full flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300"
>
<
div
className=
"relative w-full flex flex-row justify-between items-center"
>
<
p
className=
"px-2 py-1 flex flex-row justify-start items-center cursor-pointer select-none rounded opacity-80 hover:bg-gray-100 dark:hover:bg-zinc-700"
>
<
Icon
.
Paperclip
className=
"w-5 h-auto mr-1"
/>
{
t
(
"common.resources"
)
}
</
p
>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-start mt-4 mb-6"
>
{
loadingState
.
isLoading
?
(
<
div
className=
"w-full h-32 flex flex-col justify-center items-center"
>
<
p
className=
"w-full text-center text-base my-6 mt-8"
>
{
t
(
"resource.fetching-data"
)
}
</
p
>
</
div
>
)
:
(
<
div
className=
{
resources
.
length
===
0
?
"flex flex-col justify-start items-start w-full"
:
"w-full h-auto grid grid-cols-2 md:grid-cols-4 gap-6"
}
>
{
resources
.
length
===
0
?
(
<
div
className=
"w-full mt-8 mb-8 flex flex-col justify-center items-center italic"
>
<
Empty
/>
<
p
className=
"mt-4 text-gray-600 dark:text-gray-400"
>
{
t
(
"message.no-data"
)
}
</
p
>
</
div
>
)
:
(
resources
.
map
((
resource
)
=>
<
ResourceCard
key=
{
resource
.
id
}
resource=
{
resource
}
></
ResourceCard
>)
)
}
</
div
>
)
}
</
div
>
</
div
>
</
section
>
);
};
export
default
Resources
;
web/src/pages/ResourcesDashboard.tsx
deleted
100644 → 0
View file @
d1156aa7
import
{
Button
}
from
"@mui/joy"
;
import
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
showCreateResourceDialog
from
"@/components/CreateResourceDialog"
;
import
{
showCommonDialog
}
from
"@/components/Dialog/CommonDialog"
;
import
Empty
from
"@/components/Empty"
;
import
Icon
from
"@/components/Icon"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
ResourceCard
from
"@/components/ResourceCard"
;
import
ResourceSearchBar
from
"@/components/ResourceSearchBar"
;
import
Dropdown
from
"@/components/kit/Dropdown"
;
import
{
DEFAULT_MEMO_LIMIT
}
from
"@/helpers/consts"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useResourceStore
}
from
"@/store/module"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
const
ResourcesDashboard
=
()
=>
{
const
t
=
useTranslate
();
const
loadingState
=
useLoading
();
const
resourceStore
=
useResourceStore
();
const
resources
=
resourceStore
.
state
.
resources
;
const
[
queryText
,
setQueryText
]
=
useState
<
string
>
(
""
);
const
[
dragActive
,
setDragActive
]
=
useState
(
false
);
const
[
isComplete
,
setIsComplete
]
=
useState
<
boolean
>
(
false
);
useEffect
(()
=>
{
resourceStore
.
fetchResourceListWithLimit
(
DEFAULT_MEMO_LIMIT
)
.
then
((
fetchedResource
)
=>
{
if
(
fetchedResource
.
length
<
DEFAULT_MEMO_LIMIT
)
{
setIsComplete
(
true
);
}
loadingState
.
setFinish
();
})
.
catch
((
error
)
=>
{
console
.
error
(
error
);
toast
.
error
(
error
.
response
.
data
.
message
);
});
},
[]);
const
handleDeleteUnusedResourcesBtnClick
=
async
()
=>
{
let
warningText
=
t
(
"resource.warning-text-unused"
);
const
allResources
=
await
fetchAllResources
();
const
unusedResources
=
allResources
.
filter
((
resource
)
=>
{
if
(
resource
.
linkedMemoAmount
===
0
)
{
warningText
=
warningText
+
`\n-
${
resource
.
filename
}
`
;
return
true
;
}
return
false
;
});
if
(
unusedResources
.
length
===
0
)
{
toast
.
success
(
t
(
"resource.no-unused-resources"
));
return
;
}
showCommonDialog
({
title
:
t
(
"resource.delete-resource"
),
content
:
warningText
,
style
:
"warning"
,
dialogName
:
"delete-unused-resources"
,
onConfirm
:
async
()
=>
{
for
(
const
resource
of
unusedResources
)
{
await
resourceStore
.
deleteResourceById
(
resource
.
id
);
}
},
});
};
const
handleFetchMoreResourceBtnClick
=
async
()
=>
{
try
{
const
fetchedResource
=
await
resourceStore
.
fetchResourceListWithLimit
(
DEFAULT_MEMO_LIMIT
,
resources
.
length
);
if
(
fetchedResource
.
length
<
DEFAULT_MEMO_LIMIT
)
{
setIsComplete
(
true
);
}
else
{
setIsComplete
(
false
);
}
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
response
.
data
.
message
);
}
};
const
fetchAllResources
=
async
()
=>
{
if
(
isComplete
)
{
return
resources
;
}
loadingState
.
setLoading
();
try
{
const
allResources
=
await
resourceStore
.
fetchResourceList
();
loadingState
.
setFinish
();
setIsComplete
(
true
);
return
allResources
;
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
response
.
data
.
message
);
return
resources
;
}
};
const
handleSearchResourceInputChange
=
async
(
query
:
string
)
=>
{
// to prevent first tiger when page is loaded
if
(
query
===
queryText
)
return
;
await
fetchAllResources
();
setQueryText
(
query
);
};
const
handleDrag
=
(
e
:
React
.
DragEvent
<
HTMLDivElement
>
)
=>
{
e
.
preventDefault
();
e
.
stopPropagation
();
if
(
e
.
type
===
"dragenter"
||
e
.
type
===
"dragover"
)
{
setDragActive
(
true
);
}
else
if
(
e
.
type
===
"dragleave"
)
{
setDragActive
(
false
);
}
};
const
resourceList
=
useMemo
(
()
=>
resources
.
filter
((
res
:
Resource
)
=>
(
queryText
===
""
?
true
:
res
.
filename
.
toLowerCase
().
includes
(
queryText
.
toLowerCase
())))
.
map
((
resource
)
=>
<
ResourceCard
key=
{
resource
.
id
}
resource=
{
resource
}
></
ResourceCard
>),
[
resources
,
queryText
]
);
const
handleDrop
=
async
(
e
:
React
.
DragEvent
<
HTMLDivElement
>
)
=>
{
e
.
preventDefault
();
e
.
stopPropagation
();
setDragActive
(
false
);
if
(
e
.
dataTransfer
.
files
&&
e
.
dataTransfer
.
files
[
0
])
{
await
resourceStore
.
createResourcesWithBlob
(
e
.
dataTransfer
.
files
).
then
(
(
res
)
=>
{
for
(
const
resource
of
res
)
{
toast
.
success
(
`
${
resource
.
filename
}
${
t
(
"resource.upload-successfully"
)}
`
);
}
},
(
reason
)
=>
{
toast
.
error
(
reason
);
}
);
}
};
return
(
<
section
className=
"w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"
>
<
MobileHeader
showSearch=
{
false
}
/>
<
div
className=
"w-full relative"
onDragEnter=
{
handleDrag
}
>
{
dragActive
&&
(
<
div
className=
"absolute h-full w-full rounded-xl bg-zinc-800 dark:bg-white opacity-60 z-10"
onDragEnter=
{
handleDrag
}
onDragLeave=
{
handleDrag
}
onDragOver=
{
handleDrag
}
onDrop=
{
handleDrop
}
>
<
div
className=
"flex h-full w-full"
>
<
p
className=
"m-auto text-2xl text-white dark:text-black"
>
{
t
(
"resource.file-drag-drop-prompt"
)
}
</
p
>
</
div
>
</
div
>
)
}
<
div
className=
"w-full flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300"
>
<
div
className=
"relative w-full flex flex-row justify-between items-center"
>
<
p
className=
"flex flex-row justify-start items-center select-none rounded"
>
<
Icon
.
Paperclip
className=
"w-5 h-auto mr-1 ml-2"
/>
{
t
(
"common.resources"
)
}
</
p
>
<
ResourceSearchBar
setQuery=
{
handleSearchResourceInputChange
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2 mt-3 z-1"
>
<
Button
onClick=
{
()
=>
showCreateResourceDialog
({
onConfirm
:
()
=>
{
resourceStore
.
fetchResourceList
();
},
})
}
>
<
Icon
.
Plus
className=
"w-4 h-auto"
/>
</
Button
>
<
Dropdown
className=
"drop-shadow-none"
actionsClassName=
"!w-28 rounded-lg drop-shadow-md dark:bg-zinc-800"
positionClassName=
"mt-2 top-full right-0"
trigger=
{
<
Button
variant=
"outlined"
>
<
Icon
.
MoreVertical
className=
"w-4 h-auto"
/>
</
Button
>
}
actions=
{
<>
<
button
className=
"w-full flex flex-row justify-start items-center content-center text-sm whitespace-nowrap leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick=
{
handleDeleteUnusedResourcesBtnClick
}
>
<
Icon
.
Trash2
className=
"w-4 h-auto mr-2"
/>
{
t
(
"resource.clear"
)
}
</
button
>
</>
}
/>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-start mt-4 mb-6"
>
{
loadingState
.
isLoading
?
(
<
div
className=
"w-full h-32 flex flex-col justify-center items-center"
>
<
p
className=
"w-full text-center text-base my-6 mt-8"
>
{
t
(
"resource.fetching-data"
)
}
</
p
>
</
div
>
)
:
(
<
div
className=
{
resourceList
.
length
===
0
?
"flex flex-col justify-start items-start w-full"
:
"w-full h-auto grid grid-cols-2 md:grid-cols-4 md:px-6 gap-6"
}
>
{
resourceList
.
length
===
0
?
(
<
div
className=
"w-full mt-8 mb-8 flex flex-col justify-center items-center italic"
>
<
Empty
/>
<
p
className=
"mt-4 text-gray-600 dark:text-gray-400"
>
{
t
(
"message.no-data"
)
}
</
p
>
</
div
>
)
:
(
resourceList
)
}
</
div
>
)
}
</
div
>
<
div
className=
"flex flex-col justify-start items-center w-full"
>
<
p
className=
"text-sm text-gray-400 italic"
>
{
!
isComplete
&&
(
<
span
className=
"cursor-pointer my-6 hover:text-green-600"
onClick=
{
handleFetchMoreResourceBtnClick
}
>
{
t
(
"memo.fetch-more"
)
}
</
span
>
)
}
</
p
>
</
div
>
</
div
>
</
div
>
</
section
>
);
};
export
default
ResourcesDashboard
;
web/src/router/index.tsx
View file @
723e6bcd
...
...
@@ -3,7 +3,7 @@ import { createBrowserRouter, redirect } from "react-router-dom";
import
App
from
"@/App"
;
import
Archived
from
"@/pages/Archived"
;
import
DailyReview
from
"@/pages/DailyReview"
;
import
Resources
Dashboard
from
"@/pages/ResourcesDashboard
"
;
import
Resources
from
"@/pages/Resources
"
;
import
Setting
from
"@/pages/Setting"
;
import
{
initialGlobalState
,
initialUserState
}
from
"@/store/module"
;
...
...
@@ -92,7 +92,7 @@ const router = createBrowserRouter([
},
{
path
:
"resources"
,
element
:
<
Resources
Dashboard
/>,
element
:
<
Resources
/>,
loader
:
userStateLoader
,
},
{
...
...
web/src/store/module/resource.ts
View file @
723e6bcd
import
*
as
api
from
"@/helpers/api"
;
import
{
DEFAULT_MEMO_LIMIT
}
from
"@/helpers/consts"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
store
,
{
useAppSelector
}
from
"../"
;
import
{
deleteResource
,
patchResource
,
setResources
,
upsertResources
}
from
"../reducer/resource"
;
import
{
deleteResource
,
patchResource
,
setResources
}
from
"../reducer/resource"
;
import
{
useGlobalStore
}
from
"./global"
;
const
convertResponseModelResource
=
(
resource
:
Resource
):
Resource
=>
{
...
...
@@ -30,16 +29,6 @@ export const useResourceStore = () => {
store
.
dispatch
(
setResources
(
resourceList
));
return
resourceList
;
},
async
fetchResourceListWithLimit
(
limit
=
DEFAULT_MEMO_LIMIT
,
offset
?:
number
):
Promise
<
Resource
[]
>
{
const
resourceFind
:
ResourceFind
=
{
limit
,
offset
,
};
const
{
data
}
=
await
api
.
getResourceListWithLimit
(
resourceFind
);
const
resourceList
=
data
.
map
((
m
)
=>
convertResponseModelResource
(
m
));
store
.
dispatch
(
upsertResources
(
resourceList
));
return
resourceList
;
},
async
createResource
(
resourceCreate
:
ResourceCreate
):
Promise
<
Resource
>
{
const
{
data
}
=
await
api
.
createResource
(
resourceCreate
);
const
resource
=
convertResponseModelResource
(
data
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment