diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/HostData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/HostData.json index e1ff1d56..8ebb70cd 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/HostData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/HostData.json @@ -12,7 +12,7 @@ "code": "App.SiteManagement.General.NewMemberNotificationEmails", "nameKey": "App.SiteManagement.General.NewMemberNotificationEmails", "descriptionKey": "App.GeneralNewMemberNotificationEmails.NewMemberNotificationEmailsDescription", - "defaultValue": "system@sozsoft.com", + "defaultValue": "destek@sozsoft.com", "isVisibleToClients": false, "providers": "T|G|D", "isInherited": false, @@ -28,7 +28,7 @@ "code": "App.SiteManagement.General.TimedLoginEmails", "nameKey": "App.SiteManagement.General.TimedLoginEmails", "descriptionKey": "App.GeneralTimedLoginEmails.TimedLoginEmailsDescription", - "defaultValue": "system@sozsoft.com", + "defaultValue": "destek@sozsoft.com", "isVisibleToClients": false, "providers": "T|G|D", "isInherited": false, diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index 3e3fcf93..113abbad 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -6846,6 +6846,12 @@ "tr": "Bültene Abone Ol", "en": "Subscribe to Newsletter" }, + { + "resourceName": "Platform", + "key": "App.PublicBlog.SearchPlaceholder", + "en": "Search blog posts...", + "tr": "Blog yazıları ara..." + }, { "resourceName": "Platform", "key": "App.BlogSubscribe.SubscribeDesc", diff --git a/ui/package-lock.json b/ui/package-lock.json index fc45f660..ba6a8f52 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -11,14 +11,9 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@floating-ui/react": "^0.27.20", - "@fullcalendar/daygrid": "^6.1.21", - "@fullcalendar/interaction": "^6.1.21", - "@fullcalendar/react": "^6.1.21", - "@fullcalendar/timegrid": "^6.1.21", "@marsidev/react-turnstile": "^1.6.0", "@microsoft/signalr": "^10.0.11", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.101.4", "@tanstack/react-table": "^8.21.3", "axios": "^1.19.0", "classnames": "^2.5.1", @@ -33,30 +28,23 @@ "formik": "^2.4.9", "framer-motion": "^13.1.0", "jspdf": "^4.2.1", - "jwt-decode": "^4.0.0", - "lodash": "^4.18.1", "react": "^19.2.8", "react-advanced-cropper": "^0.20.1", "react-arborist": "^3.16.0", "react-dom": "^19.2.8", - "react-error-boundary": "^6.1.2", - "react-highlight-words": "^0.21.0", "react-icons": "^5.7.0", "react-modal": "^3.16.3", "react-router-dom": "^7.18.2", "react-select": "^5.10.2", - "yet-another-react-lightbox": "^3.32.2", "yup": "^1.7.1" }, "devDependencies": { "@eslint/js": "^9.39.5", "@tailwindcss/typography": "^0.5.20", "@types/file-saver": "^2.0.7", - "@types/lodash": "^4.17.25", "@types/node": "^24.10.1", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@types/react-highlight-words": "^0.20.1", "@types/react-modal": "^3.16.3", "@typescript-eslint/eslint-plugin": "^8.67.0", "@typescript-eslint/parser": "^8.67.0", @@ -2182,57 +2170,6 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, - "node_modules/@fullcalendar/core": { - "version": "6.1.21", - "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.21.tgz", - "integrity": "sha512-t3u/+sqh3Iq7TWtUnVLcGDUE6OWZh0UD3c04bI/l7lSLAgAKr3kngBmhHiQD1QXpwC8ZN5iNqG7a7gOVixhSKQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "preact": "~10.12.1" - } - }, - "node_modules/@fullcalendar/daygrid": { - "version": "6.1.21", - "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.21.tgz", - "integrity": "sha512-QYb1y40RGYLlOxKpYWg8O+7njEnKnFG8Tt7qjnubJGR35s1phQg67E+81y2TyAbbm59p2JFOCXGDk9t6KDujIA==", - "license": "MIT", - "peerDependencies": { - "@fullcalendar/core": "~6.1.21" - } - }, - "node_modules/@fullcalendar/interaction": { - "version": "6.1.21", - "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.21.tgz", - "integrity": "sha512-WPYpqtljDWmU0Xm2cOtFrLlocgxv7cgkOppj34Q6OUUat8a6Cnd6kYo2JR+irP223PE5lBYHFNp1qh7SIpJc0w==", - "license": "MIT", - "peerDependencies": { - "@fullcalendar/core": "~6.1.21" - } - }, - "node_modules/@fullcalendar/react": { - "version": "6.1.21", - "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.21.tgz", - "integrity": "sha512-TLpmGUd5k/PMdCh8XbeFC9PW9wuGvMms1oCxWgXyjK3EFPXAAd0PLfcvwKdyxoAS5eK1E4RJFkjMHvsYHpimcg==", - "license": "MIT", - "peerDependencies": { - "@fullcalendar/core": "~6.1.21", - "react": "^16.7.0 || ^17 || ^18 || ^19", - "react-dom": "^16.7.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/@fullcalendar/timegrid": { - "version": "6.1.21", - "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.21.tgz", - "integrity": "sha512-2DnShx/jallGmb8QCkr6pAOu/zuPhJrP7+uTrAtSnbqsX7GF3lTxqSeNGkTQwsgF5g/ia8udhQ+JNYaE+TN1cQ==", - "license": "MIT", - "dependencies": { - "@fullcalendar/daygrid": "~6.1.21" - }, - "peerDependencies": { - "@fullcalendar/core": "~6.1.21" - } - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -3266,32 +3203,6 @@ "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" } }, - "node_modules/@tanstack/query-core": { - "version": "5.101.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", - "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.101.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", - "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.101.4" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, "node_modules/@tanstack/react-table": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", @@ -3402,13 +3313,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", - "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -3463,16 +3367,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/react-highlight-words": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@types/react-highlight-words/-/react-highlight-words-0.20.1.tgz", - "integrity": "sha512-EgF6RaoibBurIhxk3X3d5xL0uMqjD7KtShvZ8S+d7/o6xZ+mQ4rct1a2E49vWA9r63OzrHCIyghqvQHASb/ERw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-modal": { "version": "3.16.3", "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", @@ -7669,12 +7563,6 @@ "hermes-estree": "0.25.1" } }, - "node_modules/highlight-words-core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/highlight-words-core/-/highlight-words-core-1.2.3.tgz", - "integrity": "sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==", - "license": "MIT" - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -8590,15 +8478,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10383,17 +10262,6 @@ "dev": true, "license": "MIT" }, - "node_modules/preact": { - "version": "10.12.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", - "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10647,34 +10515,12 @@ "react": "^19.2.8" } }, - "node_modules/react-error-boundary": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.2.tgz", - "integrity": "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, "node_modules/react-fast-compare": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", "license": "MIT" }, - "node_modules/react-highlight-words": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/react-highlight-words/-/react-highlight-words-0.21.0.tgz", - "integrity": "sha512-SdWEeU9fIINArEPO1rO5OxPyuhdEKZQhHzZZP1ie6UeXQf+CjycT1kWaB+9bwGcVbR0NowuHK3RqgqNg6bgBDQ==", - "license": "MIT", - "dependencies": { - "highlight-words-core": "^1.2.0", - "memoize-one": "^4.0.0" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" - } - }, "node_modules/react-icons": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", @@ -13240,32 +13086,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yet-another-react-lightbox": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.32.2.tgz", - "integrity": "sha512-F4HtHQfUNpvkj+AmECgWM4XRdCqMY5gXpKgOUx39+T+FyxLe8II4SK/pwMyYj2X54KH9lFSQeHYY1/GfYf3SdA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/igordanchenko" - }, - "peerDependencies": { - "@types/react": "^16 || ^17 || ^18 || ^19", - "@types/react-dom": "^16 || ^17 || ^18 || ^19", - "react": "^16.8.0 || ^17 || ^18 || ^19", - "react-dom": "^16.8.0 || ^17 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/ui/package.json b/ui/package.json index 647de932..475392f2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -29,14 +29,9 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@floating-ui/react": "^0.27.20", - "@fullcalendar/daygrid": "^6.1.21", - "@fullcalendar/interaction": "^6.1.21", - "@fullcalendar/react": "^6.1.21", - "@fullcalendar/timegrid": "^6.1.21", "@marsidev/react-turnstile": "^1.6.0", "@microsoft/signalr": "^10.0.11", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.101.4", "@tanstack/react-table": "^8.21.3", "axios": "^1.19.0", "classnames": "^2.5.1", @@ -51,30 +46,23 @@ "formik": "^2.4.9", "framer-motion": "^13.1.0", "jspdf": "^4.2.1", - "jwt-decode": "^4.0.0", - "lodash": "^4.18.1", "react": "^19.2.8", "react-advanced-cropper": "^0.20.1", "react-arborist": "^3.16.0", "react-dom": "^19.2.8", - "react-error-boundary": "^6.1.2", - "react-highlight-words": "^0.21.0", "react-icons": "^5.7.0", "react-modal": "^3.16.3", "react-router-dom": "^7.18.2", "react-select": "^5.10.2", - "yet-another-react-lightbox": "^3.32.2", "yup": "^1.7.1" }, "devDependencies": { "@eslint/js": "^9.39.5", "@tailwindcss/typography": "^0.5.20", "@types/file-saver": "^2.0.7", - "@types/lodash": "^4.17.25", "@types/node": "^24.10.1", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@types/react-highlight-words": "^0.20.1", "@types/react-modal": "^3.16.3", "@typescript-eslint/eslint-plugin": "^8.67.0", "@typescript-eslint/parser": "^8.67.0", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7985ec86..f8d8f810 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,5 @@ import Layout from '@/components/layouts' import Theme from '@/components/template/Theme' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { StoreProvider } from 'easy-peasy' import { BrowserRouter } from 'react-router-dom' import { store } from './store' @@ -10,15 +9,6 @@ import { registerServiceWorker } from './views/version/swRegistration' import { useEffect } from 'react' import DevExtremeBoundary from './components/devExtreme/DevExtremeBoundary' -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 30_000, - refetchOnWindowFocus: false, - }, - }, -}) - function App() { useEffect(() => { registerServiceWorker() @@ -30,13 +20,11 @@ function App() { - - - - - - - + + + + + diff --git a/ui/src/components/shared/CalendarView.tsx b/ui/src/components/shared/CalendarView.tsx deleted file mode 100644 index 6ca87db3..00000000 --- a/ui/src/components/shared/CalendarView.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import classNames from 'classnames' -import Badge from '@/components/ui/Badge' -import FullCalendar from '@fullcalendar/react' -import dayGridPlugin from '@fullcalendar/daygrid' -import timeGridPlugin from '@fullcalendar/timegrid' -import interactionPlugin from '@fullcalendar/interaction' -import { CalendarOptions } from '@fullcalendar/core' - -type EventColors = Record< - string, - { - bg: string - text: string - dot: string - } -> - -interface CalendarViewProps extends CalendarOptions { - wrapperClass?: string - eventColors?: (colors: EventColors) => EventColors -} - -const defaultColorList: Record< - string, - { - bg: string - text: string - dot: string - } -> = { - red: { - bg: 'bg-red-50 dark:bg-red-500/10', - text: 'text-red-500 dark:text-red-100', - dot: 'bg-red-500', - }, - orange: { - bg: 'bg-orange-50 dark:bg-orange-500/10', - text: 'text-orange-500 dark:text-orange-100', - dot: 'bg-orange-500', - }, - amber: { - bg: 'bg-amber-50 dark:bg-amber-500/10', - text: 'text-amber-500 dark:text-amber-100', - dot: 'bg-amber-500', - }, - yellow: { - bg: 'bg-yellow-50 dark:bg-yellow-500/10', - text: 'text-yellow-500 dark:text-yellow-100', - dot: 'bg-yellow-500', - }, - lime: { - bg: 'bg-lime-50 dark:bg-lime-500/10', - text: 'text-lime-500 dark:text-lime-100', - dot: 'bg-lime-500', - }, - green: { - bg: 'bg-green-50 dark:bg-green-500/10', - text: 'text-green-500 dark:text-green-100', - dot: 'bg-green-500', - }, - emerald: { - bg: 'bg-emerald-50 dark:bg-emerald-500/10', - text: 'text-emerald-500 dark:text-emerald-100', - dot: 'bg-emerald-500', - }, - teal: { - bg: 'bg-teal-50 dark:bg-teal-500/10', - text: 'text-teal-500 dark:text-teal-100', - dot: 'bg-teal-500', - }, - cyan: { - bg: 'bg-cyan-50 dark:bg-cyan-500/10', - text: 'text-cyan-500 dark:text-cyan-100', - dot: 'bg-cyan-500', - }, - sky: { - bg: 'bg-sky-50 dark:bg-sky-500/10', - text: 'text-sky-500 dark:text-sky-100', - dot: 'bg-sky-500', - }, - blue: { - bg: 'bg-blue-50 dark:bg-blue-500/10', - text: 'text-blue-500 dark:text-blue-100', - dot: 'bg-blue-500', - }, - indigo: { - bg: 'bg-indigo-50 dark:bg-indigo-500/10', - text: 'text-indigo-500 dark:text-indigo-100', - dot: 'bg-indigo-500', - }, - purple: { - bg: 'bg-purple-50 dark:bg-purple-500/10', - text: 'text-purple-500 dark:text-purple-100', - dot: 'bg-purple-500', - }, - fuchsia: { - bg: 'bg-fuchsia-50 dark:bg-fuchsia-500/10', - text: 'text-fuchsia-500 dark:text-fuchsia-100', - dot: 'bg-fuchsia-500', - }, - pink: { - bg: 'bg-pink-50 dark:bg-pink-500/10', - text: 'text-pink-500 dark:text-pink-100', - dot: 'bg-pink-500', - }, - rose: { - bg: 'bg-rose-50 dark:bg-rose-500/10', - text: 'text-rose-500 dark:text-rose-100', - dot: 'bg-rose-500', - }, -} - -const CalendarView = (props: CalendarViewProps) => { - const { - wrapperClass, - eventColors = () => defaultColorList, - ...rest - } = props - - return ( -
- { - const { extendedProps } = arg.event - const { isEnd, isStart } = arg - return ( -
- {!(isEnd && !isStart) && ( - - )} - {!(isEnd && !isStart) && ( - {arg.timeText} - )} - - {arg.event.title} - -
- ) - }} - plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} - {...rest} - /> -
- ) -} - -export default CalendarView diff --git a/ui/src/components/shared/ErrorBoundary.tsx b/ui/src/components/shared/ErrorBoundary.tsx new file mode 100644 index 00000000..b20aca14 --- /dev/null +++ b/ui/src/components/shared/ErrorBoundary.tsx @@ -0,0 +1,62 @@ +import { Component } from 'react' +import type { ComponentType, ErrorInfo, ReactNode } from 'react' + +export interface FallbackProps { + error: unknown + resetErrorBoundary: () => void +} + +interface ErrorBoundaryProps { + FallbackComponent: ComponentType + children: ReactNode + /** Bu değerler değiştiğinde sınır kendini sıfırlar (ör. route değişimi). */ + resetKeys?: unknown[] + onError?: (error: unknown, info: ErrorInfo) => void +} + +interface ErrorBoundaryState { + error: unknown + hasError: boolean +} + +const areKeysEqual = (a: unknown[] = [], b: unknown[] = []) => + a.length === b.length && a.every((item, index) => Object.is(item, b[index])) + +/** + * react-error-boundary yerine kullanılan yerel sınır. + * Uygulamanın ihtiyaç duyduğu yüzey kadarı var: FallbackComponent, + * resetErrorBoundary ve resetKeys. + */ +class ErrorBoundary extends Component { + state: ErrorBoundaryState = { error: null, hasError: false } + + static getDerivedStateFromError(error: unknown): ErrorBoundaryState { + return { error, hasError: true } + } + + componentDidCatch(error: unknown, info: ErrorInfo) { + this.props.onError?.(error, info) + } + + componentDidUpdate(prevProps: ErrorBoundaryProps) { + if (this.state.hasError && !areKeysEqual(prevProps.resetKeys, this.props.resetKeys)) { + this.reset() + } + } + + reset = () => { + this.setState({ error: null, hasError: false }) + } + + render() { + const { FallbackComponent, children } = this.props + + if (this.state.hasError) { + return + } + + return children + } +} + +export default ErrorBoundary diff --git a/ui/src/components/shared/Highlighter.tsx b/ui/src/components/shared/Highlighter.tsx new file mode 100644 index 00000000..bf90591e --- /dev/null +++ b/ui/src/components/shared/Highlighter.tsx @@ -0,0 +1,44 @@ +import { useMemo, Fragment } from 'react' + +interface HighlighterProps { + /** Vurgulanacak kelimeler; boş/whitespace olanlar yok sayılır. */ + searchWords: string[] + textToHighlight: string + highlightClassName?: string +} + +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +/** + * react-highlight-words yerine kullanılan yerel bileşen. + * Kullanılan tek senaryo: arama kelimelerini büyük/küçük harf duyarsız + * eşleyip ile işaretlemek (paketteki `autoEscape` davranışı varsayılan). + */ +const Highlighter = ({ searchWords, textToHighlight, highlightClassName }: HighlighterProps) => { + const parts = useMemo(() => { + const words = searchWords.map((word) => word?.trim()).filter(Boolean) + if (words.length === 0 || !textToHighlight) return null + + const pattern = new RegExp(`(${words.map(escapeRegExp).join('|')})`, 'gi') + return textToHighlight.split(pattern) + }, [searchWords, textToHighlight]) + + if (!parts) return <>{textToHighlight} + + // split içindeki tek sayılı indeksler yakalanan gruplardır. + return ( + <> + {parts.map((part, index) => + index % 2 === 1 ? ( + + {part} + + ) : ( + {part} + ), + )} + + ) +} + +export default Highlighter diff --git a/ui/src/components/shared/index.ts b/ui/src/components/shared/index.ts index 826c7100..9fbfe043 100644 --- a/ui/src/components/shared/index.ts +++ b/ui/src/components/shared/index.ts @@ -3,7 +3,6 @@ export { default as AdaptableCard } from './AdaptableCard' export { default as Affix } from './Affix' export { default as AuthorityCheck } from './AuthorityCheck' export { default as PermissionCheck } from './PermissionCheck' -export { default as CalendarView } from './CalendarView' export { default as ConfirmDialog } from './ConfirmDialog' export { default as Container } from './Container' export { default as DataTable } from './DataTable' diff --git a/ui/src/components/template/HorizontalMenuContent/HorizontalMenuIcon.tsx b/ui/src/components/template/HorizontalMenuContent/HorizontalMenuIcon.tsx index 4a2f3c94..c3266377 100644 --- a/ui/src/components/template/HorizontalMenuContent/HorizontalMenuIcon.tsx +++ b/ui/src/components/template/HorizontalMenuContent/HorizontalMenuIcon.tsx @@ -1,4 +1,4 @@ -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import type { ElementType, ComponentPropsWithRef } from 'react' import React from 'react' import { FaQuestionCircle } from 'react-icons/fa' @@ -18,6 +18,8 @@ export const Icon = ({ } const HorizontalMenuIcon = ({ icon }: HorizontalMenuIconProps) => { + const navigationIcon = useNavigationIcons() + if (typeof icon !== 'string' || !icon) { return <> } diff --git a/ui/src/components/template/HorizontalMenuContent/HorizontalMenuItem.tsx b/ui/src/components/template/HorizontalMenuContent/HorizontalMenuItem.tsx index c904abd9..bb8646b8 100644 --- a/ui/src/components/template/HorizontalMenuContent/HorizontalMenuItem.tsx +++ b/ui/src/components/template/HorizontalMenuContent/HorizontalMenuItem.tsx @@ -1,4 +1,4 @@ -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import MenuItem from '@/components/ui/MenuItem' import HorizontalMenuNavLink from './HorizontalMenuNavLink' import type { NavMode } from '@/proxy/theme/models' @@ -20,6 +20,7 @@ export type HorizontalMenuItemProps = { const HorizontalMenuItem = ({ nav, isLink, manuVariant }: HorizontalMenuItemProps) => { const { title, icon, path, isExternalLink } = nav + const navigationIcon = useNavigationIcons() // Dinamik ikon render etmek için createElement kullanıyoruz const renderIcon = diff --git a/ui/src/components/template/Search.tsx b/ui/src/components/template/Search.tsx index 70eaf12f..97e752a0 100644 --- a/ui/src/components/template/Search.tsx +++ b/ui/src/components/template/Search.tsx @@ -5,7 +5,7 @@ import withHeaderItem from '@/utils/hoc/withHeaderItem' import useThemeClass from '@/utils/hooks/useThemeClass' import classNames from 'classnames' import { useEffect, useRef, useState } from 'react' -import Highlighter from 'react-highlight-words' +import Highlighter from '@/components/shared/Highlighter' import { FaChevronRight, FaSearch } from 'react-icons/fa' import { Link } from 'react-router-dom' import { PermissionCheck } from '../shared' @@ -61,7 +61,6 @@ const ListItem = (props: { */}
{ const { pathname } = useLocation() + const navigationIcon = useNavigationIcons() const { navMode, diff --git a/ui/src/components/template/VerticalMenuContent/VerticalMenuIcon.tsx b/ui/src/components/template/VerticalMenuContent/VerticalMenuIcon.tsx index 3e51b867..205d5f20 100644 --- a/ui/src/components/template/VerticalMenuContent/VerticalMenuIcon.tsx +++ b/ui/src/components/template/VerticalMenuContent/VerticalMenuIcon.tsx @@ -1,4 +1,4 @@ -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import type { ElementType, ComponentPropsWithRef } from 'react' import React from 'react' @@ -17,6 +17,8 @@ export const Icon = ({ } const VerticalMenuIcon = ({ icon }: VerticalMenuIconProps) => { + const navigationIcon = useNavigationIcons() + // Eğer icon boş veya string değilse, boş bir öğe döndür if (typeof icon !== 'string' || !icon) { return <> diff --git a/ui/src/components/ui/Alert/Alert.tsx b/ui/src/components/ui/Alert/Alert.tsx index aada3d10..fe212d66 100644 --- a/ui/src/components/ui/Alert/Alert.tsx +++ b/ui/src/components/ui/Alert/Alert.tsx @@ -1,7 +1,7 @@ import { useState, forwardRef } from 'react' import classNames from 'classnames' import useTimeout from '../hooks/useTimeout' -import { FaCheckCircle, FaInfoCircle, FaExclamation, FaTimesCircle } from 'react-icons/fa' +import { FaCheckCircle, FaInfoCircle, FaExclamation, FaTimesCircle } from '@/components/ui/icons' import { motion } from 'framer-motion' import CloseButton from '../CloseButton' import StatusIcon from '../StatusIcon' diff --git a/ui/src/components/ui/Checkbox/Group.tsx b/ui/src/components/ui/Checkbox/Group.tsx index 6c4961bd..cbfd5f6b 100644 --- a/ui/src/components/ui/Checkbox/Group.tsx +++ b/ui/src/components/ui/Checkbox/Group.tsx @@ -1,8 +1,7 @@ import { forwardRef, useState, useCallback, useMemo, useEffect } from 'react' import classNames from 'classnames' import { CheckboxGroupContextProvider } from './context' -import cloneDeep from 'lodash/cloneDeep' -import remove from 'lodash/remove' +import { cloneDeep, remove } from '@/utils/collection' import shallowEqual from '../utils/shallowEqual' import type { CommonProps } from '../@types/common' import type { CheckboxGroupValue, CheckboxValue } from './context' diff --git a/ui/src/components/ui/CloseButton/CloseButton.tsx b/ui/src/components/ui/CloseButton/CloseButton.tsx index 1493344c..65d79ae1 100644 --- a/ui/src/components/ui/CloseButton/CloseButton.tsx +++ b/ui/src/components/ui/CloseButton/CloseButton.tsx @@ -1,5 +1,5 @@ import { forwardRef } from 'react' -import { FaTimes } from 'react-icons/fa'; +import { FaTimes } from '@/components/ui/icons'; import classNames from 'classnames' import type { CommonProps } from '../@types/common' import type { MouseEvent } from 'react' diff --git a/ui/src/components/ui/DatePicker/BasePicker.tsx b/ui/src/components/ui/DatePicker/BasePicker.tsx index b4f74358..9a4551e9 100644 --- a/ui/src/components/ui/DatePicker/BasePicker.tsx +++ b/ui/src/components/ui/DatePicker/BasePicker.tsx @@ -3,7 +3,7 @@ import dayjs from 'dayjs' import localizedFormat from 'dayjs/plugin/localizedFormat' import { Input } from '../Input' import useMergedRef from '../hooks/useMergeRef' -import { FaCalendarAlt } from 'react-icons/fa'; +import { FaCalendarAlt } from '@/components/ui/icons'; import CloseButton from '../CloseButton' import type { CommonProps, TypeAttributes } from '../@types/common' import type { diff --git a/ui/src/components/ui/DatePicker/tables/Header.tsx b/ui/src/components/ui/DatePicker/tables/Header.tsx index 3657fdca..a93cee62 100644 --- a/ui/src/components/ui/DatePicker/tables/Header.tsx +++ b/ui/src/components/ui/DatePicker/tables/Header.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames' import { Button } from '../../Button' -import { FaChevronRight, FaChevronLeft } from 'react-icons/fa'; +import { FaChevronRight, FaChevronLeft } from '@/components/ui/icons'; import type { CommonProps } from '../../@types/common' export interface HeaderProps extends CommonProps { diff --git a/ui/src/components/ui/Dialog/Dialog.tsx b/ui/src/components/ui/Dialog/Dialog.tsx index dee3b953..51df263d 100644 --- a/ui/src/components/ui/Dialog/Dialog.tsx +++ b/ui/src/components/ui/Dialog/Dialog.tsx @@ -65,7 +65,7 @@ export interface DialogProps extends ReactModal.Props { contentClassName?: string draggable?: boolean height?: string | number - onClose?: (e: MouseEvent) => void + onClose?: (e: MouseEvent) => void width?: string | number showWindowControls?: boolean onMaximize?: () => void @@ -139,12 +139,12 @@ const Dialog = (props: DialogProps) => { }) }, [currentSize.width, currentSize.height]) - const onCloseClick = (e: MouseEvent) => { + const onCloseClick = (e: MouseEvent) => { onClose?.(e) } const handleMaximize = useCallback( - (e: MouseEvent) => { + (e: MouseEvent) => { e.stopPropagation() if (!isMaximized) { setIsMaximized(true) @@ -156,7 +156,7 @@ const Dialog = (props: DialogProps) => { ) const handleRestore = useCallback( - (e: MouseEvent) => { + (e: MouseEvent) => { e.stopPropagation() if (isMaximized) { setIsMaximized(false) diff --git a/ui/src/components/ui/Dropdown/DropdownItem.tsx b/ui/src/components/ui/Dropdown/DropdownItem.tsx index fa3e9240..c7cb7a84 100644 --- a/ui/src/components/ui/Dropdown/DropdownItem.tsx +++ b/ui/src/components/ui/Dropdown/DropdownItem.tsx @@ -7,7 +7,7 @@ import { isValidElement, cloneElement, } from 'react' -import isNil from 'lodash/isNil' +import { isNil } from '@/utils/collection' import chainedFunction from '../utils/chainedFunction' import DropdownContext from './context/dropdownContext' import MenuContext from './context/menuContext' @@ -19,7 +19,7 @@ import DropdownMenuContext, { DropdownMenuContextProvider, } from './context/dropdownMenuContext' import classNames from 'classnames' -import { FaChevronRight, FaChevronLeft } from 'react-icons/fa'; +import { FaChevronRight, FaChevronLeft } from '@/components/ui/icons'; import MenuItem from '../MenuItem' import { DROPDOWN_ITEM_TYPE } from '../utils/constants' import type { CommonProps } from '../@types/common' diff --git a/ui/src/components/ui/Dropdown/DropdownToggle.tsx b/ui/src/components/ui/Dropdown/DropdownToggle.tsx index 638df914..2941e0ef 100644 --- a/ui/src/components/ui/Dropdown/DropdownToggle.tsx +++ b/ui/src/components/ui/Dropdown/DropdownToggle.tsx @@ -1,6 +1,6 @@ import { forwardRef } from 'react' import classNames from 'classnames' -import { FaChevronDown, FaChevronUp, FaChevronRight, FaChevronLeft } from 'react-icons/fa'; +import { FaChevronDown, FaChevronUp, FaChevronRight, FaChevronLeft } from '@/components/ui/icons'; import type { CommonProps } from '../@types/common' import type { DropdownPlacement } from '../@types/placement' import type { ReactNode } from 'react' diff --git a/ui/src/components/ui/Dropdown/context/dropdownMenuContext.ts b/ui/src/components/ui/Dropdown/context/dropdownMenuContext.ts index 3bcba4b2..42149996 100644 --- a/ui/src/components/ui/Dropdown/context/dropdownMenuContext.ts +++ b/ui/src/components/ui/Dropdown/context/dropdownMenuContext.ts @@ -1,5 +1,5 @@ import React, { createContext, useState, useRef, useCallback } from 'react' -import isNil from 'lodash/isNil' +import { isNil } from '@/utils/collection' type Item = { element: HTMLElement | null diff --git a/ui/src/components/ui/Input/Input.tsx b/ui/src/components/ui/Input/Input.tsx index 42c14df4..a3811c0d 100644 --- a/ui/src/components/ui/Input/Input.tsx +++ b/ui/src/components/ui/Input/Input.tsx @@ -1,7 +1,5 @@ import classNames from 'classnames' -import get from 'lodash/get' -import isEmpty from 'lodash/isEmpty' -import isNil from 'lodash/isNil' +import { get, isEmpty, isNil } from '@/utils/collection' import type { ElementType, HTMLInputTypeAttribute, InputHTMLAttributes, ReactNode } from 'react' import { forwardRef, useEffect, useMemo, useRef, useState } from 'react' import type { CommonProps, TypeAttributes } from '../@types/common' @@ -75,7 +73,7 @@ const Input = forwardRef((props, ref const { touched, errors } = form const touchedField = get(touched, field.name) const errorField = get(errors, field.name) - validate = touchedField && errorField + validate = !!touchedField && !!errorField } if (typeof invalid === 'boolean') { validate = invalid diff --git a/ui/src/components/ui/Menu/MenuCollapse.tsx b/ui/src/components/ui/Menu/MenuCollapse.tsx index dce32324..d5eac015 100644 --- a/ui/src/components/ui/Menu/MenuCollapse.tsx +++ b/ui/src/components/ui/Menu/MenuCollapse.tsx @@ -4,7 +4,7 @@ import { CollapseContextProvider } from './context/collapseContext' import classNames from 'classnames' import { motion } from 'framer-motion' import MenuContext from './context/menuContext' -import { FaChevronDown } from 'react-icons/fa'; +import { FaChevronDown } from '@/components/ui/icons'; import type { CommonProps } from '../@types/common' import type { ReactNode, MouseEvent } from 'react' diff --git a/ui/src/components/ui/Pagination/Next.tsx b/ui/src/components/ui/Pagination/Next.tsx index 1eac2460..f280ed74 100644 --- a/ui/src/components/ui/Pagination/Next.tsx +++ b/ui/src/components/ui/Pagination/Next.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { FaChevronRight } from 'react-icons/fa'; +import { FaChevronRight } from '@/components/ui/icons'; import type { CommonProps } from '../@types/common' import type { MouseEvent } from 'react' diff --git a/ui/src/components/ui/Pagination/Pagers.tsx b/ui/src/components/ui/Pagination/Pagers.tsx index 2818befc..f20f845d 100644 --- a/ui/src/components/ui/Pagination/Pagers.tsx +++ b/ui/src/components/ui/Pagination/Pagers.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react' import classNames from 'classnames' -import { FaChevronCircleLeft, FaEllipsisH, FaChevronCircleRight } from 'react-icons/fa'; +import { FaChevronCircleLeft, FaEllipsisH, FaChevronCircleRight } from '@/components/ui/icons'; const PAGER_COUNT = 7 diff --git a/ui/src/components/ui/Pagination/Prev.tsx b/ui/src/components/ui/Pagination/Prev.tsx index b04f112d..9d4edf5f 100644 --- a/ui/src/components/ui/Pagination/Prev.tsx +++ b/ui/src/components/ui/Pagination/Prev.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { FaChevronLeft } from 'react-icons/fa'; +import { FaChevronLeft } from '@/components/ui/icons'; import type { CommonProps } from '../@types/common' import type { MouseEvent } from 'react' diff --git a/ui/src/components/ui/Select/Select.tsx b/ui/src/components/ui/Select/Select.tsx index 46bb8eca..f81e75e4 100644 --- a/ui/src/components/ui/Select/Select.tsx +++ b/ui/src/components/ui/Select/Select.tsx @@ -4,12 +4,11 @@ import ReactSelect, { components as ReactSelectComponents } from 'react-select' import CreatableSelect from 'react-select/creatable' import AsyncSelect from 'react-select/async' import { TW_COLORS, BORDER_RADIUS } from '@/utils/tailwind' -import isEmpty from 'lodash/isEmpty' -import get from 'lodash/get' +import { isEmpty, get } from '@/utils/collection' import { useConfig } from '../ConfigProvider' import { useForm } from '../Form/context' import { useInputGroup } from '../InputGroup/context' -import { FaCheck, FaChevronDown, FaTimes } from 'react-icons/fa' +import { FaCheck, FaChevronDown, FaTimes } from '@/components/ui/icons' import Spinner from '../Spinner/Spinner' import type { CommonProps, TypeAttributes } from '../@types/common' import type { @@ -184,7 +183,7 @@ function SelectBase< const touchedField = get(touched, field.name) const errorField = get(errors, field.name) - isInvalid = touchedField && errorField + isInvalid = !!touchedField && !!errorField } const getBoxShadow = (state: ControlProps) => { diff --git a/ui/src/components/ui/Spinner/Spinner.tsx b/ui/src/components/ui/Spinner/Spinner.tsx index d5ae2db2..27190dfd 100644 --- a/ui/src/components/ui/Spinner/Spinner.tsx +++ b/ui/src/components/ui/Spinner/Spinner.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react' import classNames from 'classnames' import { useConfig } from '../ConfigProvider' -import { FaSpinner } from 'react-icons/fa'; +import { FaSpinner } from '@/components/ui/icons'; import type { CommonProps } from '../@types/common' import type { ElementType } from 'react' diff --git a/ui/src/components/ui/StatusIcon/StatusIcon.tsx b/ui/src/components/ui/StatusIcon/StatusIcon.tsx index d3265a48..12dc41dc 100644 --- a/ui/src/components/ui/StatusIcon/StatusIcon.tsx +++ b/ui/src/components/ui/StatusIcon/StatusIcon.tsx @@ -1,4 +1,4 @@ -import { FaCheckCircle, FaInfoCircle, FaExclamation, FaTimesCircle } from 'react-icons/fa'; +import { FaCheckCircle, FaInfoCircle, FaExclamation, FaTimesCircle } from '@/components/ui/icons'; import type { TypeAttributes, CommonProps } from '../@types/common' import type { ReactElement, ReactNode } from 'react' diff --git a/ui/src/components/ui/Steps/StepItem.tsx b/ui/src/components/ui/Steps/StepItem.tsx index aa5d0d29..24ab58dc 100644 --- a/ui/src/components/ui/Steps/StepItem.tsx +++ b/ui/src/components/ui/Steps/StepItem.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react' import classNames from 'classnames' import { useConfig } from '../ConfigProvider' -import { FaCheck, FaTimes } from 'react-icons/fa'; +import { FaCheck, FaTimes } from '@/components/ui/icons'; import { STEPS_STATUS } from '../utils/constants' import type { CommonProps, StepStatus } from '../@types/common' import type { ReactNode } from 'react' diff --git a/ui/src/components/ui/Table/Sorter.tsx b/ui/src/components/ui/Table/Sorter.tsx index 52efab1b..f915bba8 100644 --- a/ui/src/components/ui/Table/Sorter.tsx +++ b/ui/src/components/ui/Table/Sorter.tsx @@ -1,5 +1,5 @@ import { useConfig } from '../ConfigProvider' -import { FaSort, FaSortUp, FaSortDown } from 'react-icons/fa' +import { FaSort, FaSortUp, FaSortDown } from '@/components/ui/icons' export type SorterProps = { sort?: boolean | 'asc' | 'desc' } diff --git a/ui/src/components/ui/TimeInput/TimeInput.tsx b/ui/src/components/ui/TimeInput/TimeInput.tsx index 2c9a66f7..930f4f97 100644 --- a/ui/src/components/ui/TimeInput/TimeInput.tsx +++ b/ui/src/components/ui/TimeInput/TimeInput.tsx @@ -12,7 +12,7 @@ import { createAmPmHandler, createTimeHandler, } from './utils' -import { FaClock } from 'react-icons/fa'; +import { FaClock } from '@/components/ui/icons'; import type { CommonProps, TypeAttributes } from '../@types/common' import type { ReactNode, RefObject, Ref } from 'react' diff --git a/ui/src/components/ui/TimeInput/TimeInputRange.tsx b/ui/src/components/ui/TimeInput/TimeInputRange.tsx index c6f2d96d..970fe001 100644 --- a/ui/src/components/ui/TimeInput/TimeInputRange.tsx +++ b/ui/src/components/ui/TimeInput/TimeInputRange.tsx @@ -4,7 +4,7 @@ import useMergedRef from '../hooks/useMergeRef' import useDidUpdate from '../hooks/useDidUpdate' import TimeInput from './TimeInput' import CloseButton from '../CloseButton' -import { FaClock } from 'react-icons/fa'; +import { FaClock } from '@/components/ui/icons'; import Input from '../Input/Input' import type { CommonProps, TypeAttributes } from '../@types/common' import type { RefObject, Ref, ReactNode } from 'react' diff --git a/ui/src/components/ui/Tooltip/Arrow.tsx b/ui/src/components/ui/Tooltip/Arrow.tsx index 9639f51a..041eba27 100644 --- a/ui/src/components/ui/Tooltip/Arrow.tsx +++ b/ui/src/components/ui/Tooltip/Arrow.tsx @@ -4,7 +4,7 @@ import { FaCaretLeft, FaCaretUp, FaCaretRight, -} from 'react-icons/fa'; +} from '@/components/ui/icons'; export type ArrowPlacement = | 'top' diff --git a/ui/src/components/ui/Upload/FileItem.tsx b/ui/src/components/ui/Upload/FileItem.tsx index 86d6b1a0..373407bb 100644 --- a/ui/src/components/ui/Upload/FileItem.tsx +++ b/ui/src/components/ui/Upload/FileItem.tsx @@ -1,4 +1,4 @@ -import { FaFilePdf, FaFileArchive, FaFile } from 'react-icons/fa'; +import { FaFilePdf, FaFileArchive, FaFile } from '@/components/ui/icons'; import type { CommonProps } from '../@types/common' const BYTE = 1000 diff --git a/ui/src/components/ui/Upload/Upload.tsx b/ui/src/components/ui/Upload/Upload.tsx index 800d395b..c062da36 100644 --- a/ui/src/components/ui/Upload/Upload.tsx +++ b/ui/src/components/ui/Upload/Upload.tsx @@ -1,7 +1,7 @@ import { forwardRef, useRef, useState, useCallback, useEffect } from 'react' import classNames from 'classnames' import { useConfig } from '../ConfigProvider' -import cloneDeep from 'lodash/cloneDeep' +import { cloneDeep } from '@/utils/collection' import FileItem from './FileItem' import Button from '../Button/Button' import CloseButton from '../CloseButton' diff --git a/ui/src/components/ui/WindowControls/WindowControls.tsx b/ui/src/components/ui/WindowControls/WindowControls.tsx index 6556a0bd..3cccca46 100644 --- a/ui/src/components/ui/WindowControls/WindowControls.tsx +++ b/ui/src/components/ui/WindowControls/WindowControls.tsx @@ -1,63 +1,87 @@ import { forwardRef } from 'react' -import { FaTimes, FaWindowMaximize, FaWindowRestore } from 'react-icons/fa' +import { FaTimes, FaWindowMaximize, FaWindowRestore } from '@/components/ui/icons' import classNames from 'classnames' import type { CommonProps } from '../@types/common' import type { MouseEvent } from 'react' export interface WindowControlsProps extends CommonProps { absolute?: boolean - defaultStyle?: boolean isMaximized?: boolean - onClose?: (e: MouseEvent) => void - onMaximize?: (e: MouseEvent) => void - onRestore?: (e: MouseEvent) => void + onClose?: (e: MouseEvent) => void + onMaximize?: (e: MouseEvent) => void + onRestore?: (e: MouseEvent) => void showMaximize?: boolean - showRestore?: boolean showClose?: boolean + maximizeTitle?: string + restoreTitle?: string + closeTitle?: string } +const buttonClass = 'window-controls-btn' + const WindowControls = forwardRef((props, ref) => { const { absolute, className, - defaultStyle, isMaximized, onClose, onMaximize, onRestore, showMaximize = true, - showRestore = true, showClose = true, + maximizeTitle = 'Maximize', + restoreTitle = 'Restore', + closeTitle = 'Close', ...rest } = props - const windowControlsAbsoluteClass = 'absolute z-10' - const windowControlsClass = classNames( 'window-controls flex items-center gap-2', - defaultStyle && 'window-controls-default', - absolute && windowControlsAbsoluteClass, + absolute && 'absolute z-10 top-3', className ) - const buttonClass = 'close-btn cursor-pointer hover:opacity-70 transition-opacity' + const stopPropagation = (e: MouseEvent) => { + e.stopPropagation() + } return (
- {showMaximize && !isMaximized && ( - - - - )} - {showRestore && isMaximized && ( - - - - )} + {showMaximize && + (isMaximized ? ( + + ) : ( + + ))} {showClose && ( - + )}
) diff --git a/ui/src/components/ui/hooks/useUniqueId.ts b/ui/src/components/ui/hooks/useUniqueId.ts index 5962d4ba..cd590665 100644 --- a/ui/src/components/ui/hooks/useUniqueId.ts +++ b/ui/src/components/ui/hooks/useUniqueId.ts @@ -1,5 +1,5 @@ import { useRef } from 'react' -import uniqueId from 'lodash/uniqueId' +import { uniqueId } from '@/utils/collection' import createUID from '../utils/createUid' export default function useUniqueId(prefix = '', len = 10) { diff --git a/ui/src/components/ui/icons.tsx b/ui/src/components/ui/icons.tsx new file mode 100644 index 00000000..c4988ac0 --- /dev/null +++ b/ui/src/components/ui/icons.tsx @@ -0,0 +1,412 @@ +import { GenIcon } from 'react-icons/lib' +import type { IconType } from 'react-icons/lib' + +/** + * Platform kabuğunun (ui/ bileşenleri) kullandığı Font Awesome ikonları. + * + * NEDEN BURADA: menü ikonları veritabanından ada göre çözüldüğü için + * `navigation-icon.config` tüm FA setini namespace olarak yüklemek zorunda. + * Bu, `react-icons/fa` modülünü tree-shake edilemez hale getiriyor; sonuçta + * TEK bir ikon import eden dosya bile ~1.28 MB'lık fa chunk'ını sürüklüyordu. + * ui/ bileşenleri uygulamanın her yerinde (ve ilk boyamada) kullanıldığından, + * kullandıkları 24 ikon buraya birebir aynı tanımlarla gömüldü (~10 KB) ve + * kabuk artık fa chunk'ına hiç dokunmuyor. + * + * Render çıktısı react-icons ile aynıdır: aynı `GenIcon` fabrikası, aynı + * viewBox/path verisi, aynı prop davranışı (size, color, title, IconContext). + * + * Yeni bir ikon gerekirse: node_modules/react-icons/fa/index.mjs içindeki + * tanımı buraya aynı biçimde kopyalayın. + */ + +export const FaCaretDown: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 320 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z', + }, + child: [], + }, + ], +}) + +export const FaCaretLeft: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 192 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M192 127.338v257.324c0 17.818-21.543 26.741-34.142 14.142L29.196 270.142c-7.81-7.81-7.81-20.474 0-28.284l128.662-128.662c12.599-12.6 34.142-3.676 34.142 14.142z', + }, + child: [], + }, + ], +}) + +export const FaCaretRight: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 192 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z', + }, + child: [], + }, + ], +}) + +export const FaCaretUp: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 320 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z', + }, + child: [], + }, + ], +}) + +export const FaCalendarAlt: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 448 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M0 464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V192H0v272zm320-196c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM192 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM64 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zM400 64h-48V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H160V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H48C21.5 64 0 85.5 0 112v48h448v-48c0-26.5-21.5-48-48-48z', + }, + child: [], + }, + ], +}) + +export const FaCheck: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z', + }, + child: [], + }, + ], +}) + +export const FaCheckCircle: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z', + }, + child: [], + }, + ], +}) + +export const FaChevronCircleLeft: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M256 504C119 504 8 393 8 256S119 8 256 8s248 111 248 248-111 248-248 248zM142.1 273l135.5 135.5c9.4 9.4 24.6 9.4 33.9 0l17-17c9.4-9.4 9.4-24.6 0-33.9L226.9 256l101.6-101.6c9.4-9.4 9.4-24.6 0-33.9l-17-17c-9.4-9.4-24.6-9.4-33.9 0L142.1 239c-9.4 9.4-9.4 24.6 0 34z', + }, + child: [], + }, + ], +}) + +export const FaChevronCircleRight: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M256 8c137 0 248 111 248 248S393 504 256 504 8 393 8 256 119 8 256 8zm113.9 231L234.4 103.5c-9.4-9.4-24.6-9.4-33.9 0l-17 17c-9.4 9.4-9.4 24.6 0 33.9L285.1 256 183.5 357.6c-9.4 9.4-9.4 24.6 0 33.9l17 17c9.4 9.4 24.6 9.4 33.9 0L369.9 273c9.4-9.4 9.4-24.6 0-34z', + }, + child: [], + }, + ], +}) + +export const FaChevronDown: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 448 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z', + }, + child: [], + }, + ], +}) + +export const FaChevronLeft: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 320 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z', + }, + child: [], + }, + ], +}) + +export const FaChevronRight: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 320 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z', + }, + child: [], + }, + ], +}) + +export const FaChevronUp: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 448 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z', + }, + child: [], + }, + ], +}) + +export const FaClock: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8Zm92.49,313h0l-20,25a16,16,0,0,1-22.49,2.5h0l-67-49.72a40,40,0,0,1-15-31.23V112a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16V256l58,42.5A16,16,0,0,1,348.49,321Z', + }, + child: [], + }, + ], +}) + +export const FaEllipsisH: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z', + }, + child: [], + }, + ], +}) + +export const FaExclamation: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 192 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z', + }, + child: [], + }, + ], +}) + +export const FaFile: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 384 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M224 136V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zm160-14.1v6.1H256V0h6.1c6.4 0 12.5 2.5 17 7l97.9 98c4.5 4.5 7 10.6 7 16.9z', + }, + child: [], + }, + ], +}) + +export const FaFileArchive: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 384 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M377 105L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128v-6.1c0-6.3-2.5-12.4-7-16.9zM128.4 336c-17.9 0-32.4 12.1-32.4 27 0 15 14.6 27 32.5 27s32.4-12.1 32.4-27-14.6-27-32.5-27zM224 136V0h-63.6v32h-32V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zM95.9 32h32v32h-32zm32.3 384c-33.2 0-58-30.4-51.4-62.9L96.4 256v-32h32v-32h-32v-32h32v-32h-32V96h32V64h32v32h-32v32h32v32h-32v32h32v32h-32v32h22.1c5.7 0 10.7 4.1 11.8 9.7l17.3 87.7c6.4 32.4-18.4 62.6-51.4 62.6z', + }, + child: [], + }, + ], +}) + +export const FaFilePdf: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 384 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M181.9 256.1c-5-16-4.9-46.9-2-46.9 8.4 0 7.6 36.9 2 46.9zm-1.7 47.2c-7.7 20.2-17.3 43.3-28.4 62.7 18.3-7 39-17.2 62.9-21.9-12.7-9.6-24.9-23.4-34.5-40.8zM86.1 428.1c0 .8 13.2-5.4 34.9-40.2-6.7 6.3-29.1 24.5-34.9 40.2zM248 160h136v328c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V24C0 10.7 10.7 0 24 0h200v136c0 13.2 10.8 24 24 24zm-8 171.8c-20-12.2-33.3-29-42.7-53.8 4.5-18.5 11.6-46.6 6.2-64.2-4.7-29.4-42.4-26.5-47.8-6.8-5 18.3-.4 44.1 8.1 77-11.6 27.6-28.7 64.6-40.8 85.8-.1 0-.1.1-.2.1-27.1 13.9-73.6 44.5-54.5 68 5.6 6.9 16 10 21.5 10 17.9 0 35.7-18 61.1-61.8 25.8-8.5 54.1-19.1 79-23.2 21.7 11.8 47.1 19.5 64 19.5 29.2 0 31.2-32 19.7-43.4-13.9-13.6-54.3-9.7-73.6-7.2zM377 105L279 7c-4.5-4.5-10.6-7-17-7h-6v128h128v-6.1c0-6.3-2.5-12.4-7-16.9zm-74.1 255.3c4.1-2.7-2.5-11.9-42.8-9 37.1 15.8 42.8 9 42.8 9z', + }, + child: [], + }, + ], +}) + +export const FaInfoCircle: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z', + }, + child: [], + }, + ], +}) + +export const FaSort: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 320 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z', + }, + child: [], + }, + ], +}) + +export const FaSortDown: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 320 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z', + }, + child: [], + }, + ], +}) + +export const FaSortUp: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 320 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z', + }, + child: [], + }, + ], +}) + +export const FaSpinner: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z', + }, + child: [], + }, + ], +}) + +export const FaTimes: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 352 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z', + }, + child: [], + }, + ], +}) + +export const FaTimesCircle: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z', + }, + child: [], + }, + ], +}) + +export const FaWindowMaximize: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M464 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-16 160H64v-84c0-6.6 5.4-12 12-12h360c6.6 0 12 5.4 12 12v84z', + }, + child: [], + }, + ], +}) + +export const FaWindowRestore: IconType = GenIcon({ + tag: 'svg', + attr: { viewBox: '0 0 512 512' }, + child: [ + { + tag: 'path', + attr: { + d: 'M512 48v288c0 26.5-21.5 48-48 48h-48V176c0-44.1-35.9-80-80-80H128V48c0-26.5 21.5-48 48-48h288c26.5 0 48 21.5 48 48zM384 176v288c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V176c0-26.5 21.5-48 48-48h288c26.5 0 48 21.5 48 48zm-68 28c0-6.6-5.4-12-12-12H76c-6.6 0-12 5.4-12 12v52h252v-52z', + }, + child: [], + }, + ], +}) diff --git a/ui/src/index.css b/ui/src/index.css index 724c9fda..61c901ab 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -151,30 +151,47 @@ div.dialog-after-open > div.dialog-content.maximized { /* React Modal prevent bg scroll */ /* Window Controls Styles */ -.window-controls { - top: 12px; -} - -.window-controls .close-btn { +.window-controls .window-controls-btn { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 4px; - transition: all 0.2s ease; + transition: + background-color 0.2s ease, + color 0.2s ease; color: #666; font-size: 14px; + outline: none; } -.window-controls .close-btn:hover { +.window-controls .window-controls-btn:hover { background-color: rgba(0, 0, 0, 0.1); color: #333; } -.window-controls .close-btn:last-child:hover { +.window-controls .window-controls-btn:focus-visible { + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.6); +} + +.window-controls .window-controls-close:hover { background-color: #e74c3c; - color: white; + color: #fff; +} + +.dark .window-controls .window-controls-btn { + color: #9ca3af; +} + +.dark .window-controls .window-controls-btn:hover { + background-color: rgba(255, 255, 255, 0.12); + color: #f3f4f6; +} + +.dark .window-controls .window-controls-close:hover { + background-color: #e74c3c; + color: #fff; } /* Maximized dialog styles */ diff --git a/ui/src/proxy/menus/navigation-icon.config.tsx b/ui/src/proxy/menus/navigation-icon.config.tsx index 6b966b58..726e6816 100644 --- a/ui/src/proxy/menus/navigation-icon.config.tsx +++ b/ui/src/proxy/menus/navigation-icon.config.tsx @@ -1,24 +1,81 @@ -import * as fc from 'react-icons/fc' -import * as fa from 'react-icons/fa' +import { useSyncExternalStore } from 'react' import type { ComponentType } from 'react' +/** + * Menü ikonları veritabanında ada göre tutulduğu için (`FaUser`, `FcFolder` …) + * tüm react-icons setinin ad→bileşen sözlüğü olarak hazır olması gerekiyor. + * + * Bu setler `import * as` ile STATİK alındığında rollup tree-shaking'i tamamen + * kapanıyordu: modül tek parça olarak korunduğu için, uygulamada tek bir `FaCog` + * kullanan dosya bile ~1.3 MB'lık fa chunk'ını ilk açılış grafiğine sokuyordu + * (ölçüm: eager JS 1756 KB'ın 1281 KB'ı sadece bu). + * + * Çözüm: setler DİNAMİK import ile yükleniyor. + * - Named import kullanan (~190) dosya artık normal şekilde tree-shake ediliyor, + * yalnızca kullandıkları ikonlar bundle'a giriyor. + * - Tam sözlük ilk boyamayı bloklamayan ayrı bir chunk'a düşüyor; ilk abone + * olan bileşen (kenar çubuğu) yüklemeyi tetikler, hazır olunca `useSyncExternalStore` + * üzerinden yeniden render edilir. Menü verisi zaten API'den asenkron geldiği + * için pratikte ikonlar menüyle birlikte görünür. + */ + export type NavigationIcons = Record> -const navigationIcon: NavigationIcons = {} +/** + * Snapshot referansı yükleme bitince DEĞİŞİR; böylece `useSyncExternalStore` + * yeniden render tetikler ve tüketicilerdeki `useMemo(..., [navigationIcon])` + * bağımlılıkları da doğru şekilde tazelenir. + */ +let snapshot: NavigationIcons = {} -function registerIcons(iconModule: Record) { - for (const [key, Icon] of Object.entries(iconModule)) { - if ( - Icon && - (typeof Icon === 'function' || - (typeof Icon === 'object' && 'render' in Icon)) - ) { - navigationIcon[key] = Icon as ComponentType +const listeners = new Set<() => void>() +let loadPromise: Promise | null = null + +function collectIcons(iconModules: Record[]): NavigationIcons { + const icons: NavigationIcons = {} + for (const iconModule of iconModules) { + for (const [key, Icon] of Object.entries(iconModule)) { + if (Icon && (typeof Icon === 'function' || (typeof Icon === 'object' && 'render' in Icon))) { + icons[key] = Icon as ComponentType + } } } + return icons +} + +/** İkon sözlüğünü (bir kez) yükler. Birden çok çağrı aynı promise'i paylaşır. */ +export function loadNavigationIcons(): Promise { + if (!loadPromise) { + loadPromise = Promise.all([import('react-icons/fc'), import('react-icons/fa')]) + .then((modules) => { + snapshot = collectIcons(modules) + listeners.forEach((listener) => listener()) + }) + .catch((error) => { + // Yükleme başarısızsa yeniden denenebilsin; ikonsuz menü çalışmaya devam eder. + loadPromise = null + console.error('Navigation icons could not be loaded', error) + }) + } + + return loadPromise +} + +function subscribe(listener: () => void) { + listeners.add(listener) + void loadNavigationIcons() + return () => { + listeners.delete(listener) + } } -registerIcons(fc) -registerIcons(fa) +const getSnapshot = () => snapshot -export default navigationIcon +/** + * Ad→ikon sözlüğünü döner ve sözlük yüklendiğinde bileşeni yeniden render eder. + * Yükleme tamamlanana kadar boş sözlük döner; çağıran taraflar zaten + * "ikon bulunamadı" durumunu (fallback ikon / null) ele alıyor. + */ +export function useNavigationIcons(): NavigationIcons { + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) +} diff --git a/ui/src/services/platformApi.service.ts b/ui/src/services/platformApi.service.ts index 5899358d..f71b2f33 100644 --- a/ui/src/services/platformApi.service.ts +++ b/ui/src/services/platformApi.service.ts @@ -1,6 +1,6 @@ import axios from 'axios' import { store } from '../store' -import { jwtDecode } from 'jwt-decode' +import { jwtDecode } from '@/utils/jwt' import appConfig from '../proxy/configs/app.config' import { refreshToken, signOut as revokeTokens } from './auth.service' import { isLoginSuccess } from '../proxy/account/models' diff --git a/ui/src/utils/collection.ts b/ui/src/utils/collection.ts new file mode 100644 index 00000000..d46cda4b --- /dev/null +++ b/ui/src/utils/collection.ts @@ -0,0 +1,148 @@ +/** + * lodash yerine kullanılan minimal yardımcılar. + * + * Uygulamada lodash'ten yalnızca aşağıdaki dokuz fonksiyon kullanılıyordu; + * bunlar için ~25 KB'lık bir bağımlılık taşımak yerine davranışları birebir + * karşılayan yerel sürümler tutuluyor. Davranış farkı olmaması önemli: + * çağrı yerleri lodash semantiğine göre yazılmıştı. + */ + +/** lodash/isNil */ +export const isNil = (value: unknown): value is null | undefined => value == null + +/** + * lodash/isEmpty — nesne, dizi, string, Map/Set ve primitive'ler için. + * Primitive (number/boolean) daima "boş" sayılır; lodash de böyle davranır. + */ +export const isEmpty = (value: unknown): boolean => { + if (value == null) return true + if (Array.isArray(value) || typeof value === 'string') return value.length === 0 + if (value instanceof Map || value instanceof Set) return value.size === 0 + if (typeof value === 'object') return Object.keys(value as object).length === 0 + return true +} + +/** lodash/isPlainObject — düz nesne mi (dizi, Date, class örneği değil). */ +export const isPlainObject = (value: unknown): boolean => { + if (typeof value !== 'object' || value === null) return false + const proto = Object.getPrototypeOf(value) + return proto === null || proto === Object.prototype +} + +/** lodash/keys */ +export const keys = (value: object | null | undefined): string[] => + value == null ? [] : Object.keys(value) + +/** + * lodash/cloneDeep — düz veri (nesne/dizi/Date/Map/Set) için derin kopya. + * structuredClone fonksiyon ve React elemanı içeren yapılarda patlar; bu + * yüzden elle dolaşılıyor, klonlanamayan değerler referansla taşınıyor. + */ +export const cloneDeep = (value: T, seen = new WeakMap()): T => { + if (value === null || typeof value !== 'object') return value + + const objectValue = value as unknown as object + const cached = seen.get(objectValue) + if (cached !== undefined) return cached as T + + if (value instanceof Date) return new Date(value.getTime()) as unknown as T + if (value instanceof RegExp) return new RegExp(value.source, value.flags) as unknown as T + + if (Array.isArray(value)) { + const copy: unknown[] = [] + seen.set(objectValue, copy) + value.forEach((item, index) => { + copy[index] = cloneDeep(item, seen) + }) + return copy as unknown as T + } + + if (value instanceof Map) { + const copy = new Map() + seen.set(objectValue, copy) + value.forEach((item, key) => copy.set(key, cloneDeep(item, seen))) + return copy as unknown as T + } + + if (value instanceof Set) { + const copy = new Set() + seen.set(objectValue, copy) + value.forEach((item) => copy.add(cloneDeep(item, seen))) + return copy as unknown as T + } + + // File/Blob gibi klonlanamayan host nesneleri referansla taşınır. + if (!isPlainObject(value)) return value + + const copy: Record = {} + seen.set(objectValue, copy) + Object.entries(value as Record).forEach(([key, item]) => { + copy[key] = cloneDeep(item, seen) + }) + return copy as unknown as T +} + +/** + * lodash/get — 'a.b[0].c' biçimli yol desteğiyle güvenli okuma. + * Formik'in iç içe alan adları (`address.city`, `rows[0].name`) bu formatta. + */ +export const get = (object: unknown, path: string, defaultValue?: T): T => { + if (object == null) return defaultValue as T + + const segments = path + .replace(/\[(\d+)\]/g, '.$1') + .split('.') + .filter((segment) => segment.length > 0) + + let current: unknown = object + for (const segment of segments) { + if (current == null) return defaultValue as T + current = (current as Record)[segment] + } + + return (current === undefined ? defaultValue : current) as T +} + +/** lodash/groupBy — anahtar üreticisi alan/fonksiyon olabilir. */ +export const groupBy = ( + collection: readonly T[] | null | undefined, + iteratee: ((item: T) => string | number) | keyof T, +): Record => { + const result: Record = {} + if (!collection) return result + + const resolveKey = + typeof iteratee === 'function' + ? (item: T) => (iteratee as (item: T) => string | number)(item) + : (item: T) => (item as Record)[iteratee as string] as string | number + + for (const item of collection) { + const key = String(resolveKey(item)) + ;(result[key] ??= []).push(item) + } + + return result +} + +/** + * lodash/remove — diziyi YERİNDE değiştirir ve çıkarılan elemanları döner. + * Çağrı yerleri bu mutasyona güveniyor. + */ +export const remove = (array: T[], predicate: (item: T) => boolean): T[] => { + const removed: T[] = [] + for (let index = array.length - 1; index >= 0; index -= 1) { + if (predicate(array[index])) { + removed.unshift(array[index]) + array.splice(index, 1) + } + } + return removed +} + +let uniqueIdCounter = 0 + +/** lodash/uniqueId */ +export const uniqueId = (prefix = ''): string => { + uniqueIdCounter += 1 + return `${prefix}${uniqueIdCounter}` +} diff --git a/ui/src/utils/hooks/useAuth.ts b/ui/src/utils/hooks/useAuth.ts index f10a44d9..53c48d74 100644 --- a/ui/src/utils/hooks/useAuth.ts +++ b/ui/src/utils/hooks/useAuth.ts @@ -1,4 +1,4 @@ -import { jwtDecode } from 'jwt-decode' +import { jwtDecode } from '@/utils/jwt' import { useNavigate } from 'react-router-dom' import useQuery from './useQuery' import { store, useStoreActions, useStoreState } from '../../store/store' diff --git a/ui/src/utils/hooks/useAuthority.ts b/ui/src/utils/hooks/useAuthority.ts index 504e3b9c..8b75a39a 100644 --- a/ui/src/utils/hooks/useAuthority.ts +++ b/ui/src/utils/hooks/useAuthority.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import isEmpty from 'lodash/isEmpty' +import { isEmpty } from '@/utils/collection' function useAuthority( userAuthority: string[] = [], diff --git a/ui/src/utils/hooks/useCurrentMenuIcon.tsx b/ui/src/utils/hooks/useCurrentMenuIcon.tsx index a91cf585..505627c2 100644 --- a/ui/src/utils/hooks/useCurrentMenuIcon.tsx +++ b/ui/src/utils/hooks/useCurrentMenuIcon.tsx @@ -1,7 +1,7 @@ import { useStoreState } from '@/store/store' import { useLocation } from 'react-router-dom' import { FaUser } from 'react-icons/fa' -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { navigationTreeToFlat } from '@/utils/navigation' import { ReactElement } from 'react' @@ -18,6 +18,7 @@ const extractListFormCode = (path: string): string | null => { export function useCurrentMenuIcon(className = 'w-6 h-6'): ReactElement { const mainMenu = useStoreState((state) => state.abpConfig.menu.mainMenu) const location = useLocation() + const navigationIcon = useNavigationIcons() const menus = navigationTreeToFlat(mainMenu) const currentMenu = menus.find((menu) => { diff --git a/ui/src/utils/hooks/useMenuActive.ts b/ui/src/utils/hooks/useMenuActive.ts index af0d19ba..818c9df7 100644 --- a/ui/src/utils/hooks/useMenuActive.ts +++ b/ui/src/utils/hooks/useMenuActive.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // eslint-disable @typescript-eslint/no-explicit-any import type { NavigationTree } from '@/proxy/menus/navigation' -import isPlainObject from 'lodash/isPlainObject' +import { isPlainObject } from '@/utils/collection' import { useMemo } from 'react' interface NavInfo extends NavigationTree { diff --git a/ui/src/utils/jwt.ts b/ui/src/utils/jwt.ts new file mode 100644 index 00000000..b2ff58d6 --- /dev/null +++ b/ui/src/utils/jwt.ts @@ -0,0 +1,27 @@ +/** + * jwt-decode yerine kullanılan minimal çözücü. + * + * Sadece payload okunuyor; imza doğrulaması zaten sunucunun işi. Token + * base64url kodlu olduğu için `-`/`_` çevrilir ve padding tamamlanır; + * ayrıca Türkçe karakter içeren claim'lerin bozulmaması için atob çıktısı + * UTF-8 olarak yeniden çözülür. + */ +export const jwtDecode = >(token: string): T => { + const payload = token?.split('.')[1] + if (!payload) { + throw new Error('Invalid token specified: missing payload') + } + + const base64 = payload.replace(/-/g, '+').replace(/_/g, '/') + const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=') + + try { + const binary = atob(padded) + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)) + return JSON.parse(new TextDecoder().decode(bytes)) as T + } catch { + throw new Error('Invalid token specified: payload could not be decoded') + } +} + +export default jwtDecode diff --git a/ui/src/views/Views.tsx b/ui/src/views/Views.tsx index 296e24dc..f19afdc6 100644 --- a/ui/src/views/Views.tsx +++ b/ui/src/views/Views.tsx @@ -3,8 +3,8 @@ import Loading from '@/components/shared/Loading' import { Alert, Button } from '@/components/ui' import { store, useStoreState } from '@/store' import { Suspense, useEffect } from 'react' -import { ErrorBoundary } from 'react-error-boundary' -import type { FallbackProps } from 'react-error-boundary' +import ErrorBoundary from '@/components/shared/ErrorBoundary' +import type { FallbackProps } from '@/components/shared/ErrorBoundary' import { FaArrowLeft } from 'react-icons/fa' import { Navigate, useLocation } from 'react-router-dom' import { useLocalization } from '@/utils/hooks/useLocalization' @@ -19,8 +19,8 @@ interface ViewsProps { layout?: LayoutType } -// react-error-boundary v6'dan itibaren `error` `unknown` tipinde geliyor; -// throw edilen her şey Error olmayabilir, o yüzden daraltıyoruz. +// `error` `unknown` tipinde geliyor; throw edilen her şey Error olmayabilir, +// o yüzden daraltıyoruz. function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { const { translate } = useLocalization() const { name, message } = diff --git a/ui/src/views/admin/activityLog/ActivityLog.tsx b/ui/src/views/admin/activityLog/ActivityLog.tsx index 3e897c98..1e2c521f 100644 --- a/ui/src/views/admin/activityLog/ActivityLog.tsx +++ b/ui/src/views/admin/activityLog/ActivityLog.tsx @@ -7,7 +7,7 @@ import { NotificationDto } from '@/proxy/notification/models' import { getList } from '@/services/notification.service' import { useLocalization } from '@/utils/hooks/useLocalization' import dayjs from 'dayjs' -import groupBy from 'lodash/groupBy' +import { groupBy } from '@/utils/collection' import { useCallback, useEffect, useState } from 'react' import Log from './components/Log' import LogFilter from './components/LogFilter' diff --git a/ui/src/views/admin/activityLog/components/Log.tsx b/ui/src/views/admin/activityLog/components/Log.tsx index 05a2eb51..9aab464f 100644 --- a/ui/src/views/admin/activityLog/components/Log.tsx +++ b/ui/src/views/admin/activityLog/components/Log.tsx @@ -5,8 +5,7 @@ import { AVATAR_URL } from '@/constants/app.constant' import { NotificationDto } from '@/proxy/notification/models' import { useLocalization } from '@/utils/hooks/useLocalization' import dayjs from 'dayjs' -import isEmpty from 'lodash/isEmpty' -import keys from 'lodash/keys' +import { isEmpty, keys } from '@/utils/collection' import Event from './Event' import TimeLineAvatar from './TimeLineAvatar' import useLocale from '@/utils/hooks/useLocale' diff --git a/ui/src/views/admin/listForm/edit/ChartTabSeries.tsx b/ui/src/views/admin/listForm/edit/ChartTabSeries.tsx index 68788d11..bf29db02 100644 --- a/ui/src/views/admin/listForm/edit/ChartTabSeries.tsx +++ b/ui/src/views/admin/listForm/edit/ChartTabSeries.tsx @@ -39,7 +39,7 @@ import { JsonRowDialogData } from './json-row-operations/types' import JsonRowOpDialogSeries from './json-row-operations/JsonRowOpDialogSeries' import CreatableSelect from 'react-select/creatable' import { getListFormFields } from '@/services/admin/list-form-field.service' -import groupBy from 'lodash/groupBy' +import { groupBy } from '@/utils/collection' // Bu sekme ListForm nesnesinin tamamını düzenler; `name` / `title` / `height` // gibi alanların girdisi burada yok. Şema bu alanları zorunlu tutuyordu ve diff --git a/ui/src/views/admin/listForm/edit/FormTabGantt.tsx b/ui/src/views/admin/listForm/edit/FormTabGantt.tsx index e601a5d5..a7764970 100644 --- a/ui/src/views/admin/listForm/edit/FormTabGantt.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabGantt.tsx @@ -18,7 +18,7 @@ import { FormEditProps } from './FormEdit' import { SelectBoxOption } from '@/types/shared' import { useCallback, useEffect, useState } from 'react' import { getListFormFields } from '@/services/admin/list-form-field.service' -import groupBy from 'lodash/groupBy' +import { groupBy } from '@/utils/collection' import { useParams } from 'react-router-dom' import type { GanttScaleType } from 'devextreme/ui/gantt' import { gantScaleOptions } from './options' diff --git a/ui/src/views/admin/listForm/edit/FormTabScheduler.tsx b/ui/src/views/admin/listForm/edit/FormTabScheduler.tsx index 49d18f03..259fc7dd 100644 --- a/ui/src/views/admin/listForm/edit/FormTabScheduler.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabScheduler.tsx @@ -18,7 +18,7 @@ import { FormEditProps } from './FormEdit' import { SelectBoxOption } from '@/types/shared' import { useCallback, useEffect, useState } from 'react' import { getListFormFields } from '@/services/admin/list-form-field.service' -import groupBy from 'lodash/groupBy' +import { groupBy } from '@/utils/collection' import { useParams } from 'react-router-dom' import { firstDayOfWeekOptions, schedulerViewOptions } from './options' diff --git a/ui/src/views/admin/listForm/edit/FormTabTodo.tsx b/ui/src/views/admin/listForm/edit/FormTabTodo.tsx index edf87df2..a6c45fb7 100644 --- a/ui/src/views/admin/listForm/edit/FormTabTodo.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabTodo.tsx @@ -14,7 +14,7 @@ import { getListFormFields } from '@/services/admin/list-form-field.service' import { useStoreState } from '@/store' import type { SelectBoxOption } from '@/types/shared' import { Field, FieldProps, Form, Formik } from 'formik' -import groupBy from 'lodash/groupBy' +import { groupBy } from '@/utils/collection' import { useCallback, useEffect, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import type { FormEditProps } from './FormEdit' diff --git a/ui/src/views/admin/listForm/edit/FormTabTree.tsx b/ui/src/views/admin/listForm/edit/FormTabTree.tsx index 5972af3f..4f7228ae 100644 --- a/ui/src/views/admin/listForm/edit/FormTabTree.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabTree.tsx @@ -17,7 +17,7 @@ import { FormEditProps } from './FormEdit' import { SelectBoxOption } from '@/types/shared' import { useCallback, useEffect, useState } from 'react' import { getListFormFields } from '@/services/admin/list-form-field.service' -import groupBy from 'lodash/groupBy' +import { groupBy } from '@/utils/collection' import { useParams } from 'react-router-dom' const validationSchema = Yup.object().shape({}) diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogExtraFilter.tsx b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogExtraFilter.tsx index 104df72c..5b6eedb4 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogExtraFilter.tsx +++ b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogExtraFilter.tsx @@ -25,7 +25,7 @@ import { import { cascadeFilterOperator } from '../options' import CreatableSelect from 'react-select/creatable' import { getListFormFields } from '@/services/admin/list-form-field.service' -import groupBy from 'lodash/groupBy' +import { groupBy } from '@/utils/collection' import { extraFilterControlTypeOptions } from '@/proxy/admin/list-form/options' const schema = object().shape({ diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx index ae7f798b..e2e0443f 100644 --- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx @@ -20,7 +20,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization' import { useStoreState } from '@/store/store' import { ROUTES_ENUM } from '@/routes/route.constant' import { WizardFileInfoDto } from '@/proxy/admin/wizard/models' -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' interface ConfirmState { fileName: string @@ -32,6 +32,7 @@ const WizardFileManager = () => { const { translate } = useLocalization() const mode = useStoreState((state) => state.theme.mode) const MenuIcon = useCurrentMenuIcon('w-5 h-5') + const navigationIcon = useNavigationIcons() const [files, setFiles] = useState([]) const [loading, setLoading] = useState(false) diff --git a/ui/src/views/admin/listForm/wizard/WizardStep1.tsx b/ui/src/views/admin/listForm/wizard/WizardStep1.tsx index 7974d57f..7765fb22 100644 --- a/ui/src/views/admin/listForm/wizard/WizardStep1.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStep1.tsx @@ -1,7 +1,7 @@ import { Button, FormItem, Input, Notification, Select, toast } from '@/components/ui' import { MenuDto } from '@/proxy/menus/models' import { SelectBoxOption } from '@/types/shared' -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { MenuItem } from '@/proxy/menus/menu' import { MenuService } from '@/services/menu.service' import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik' @@ -132,6 +132,7 @@ function TreeNode({ const isExpanded = expanded.has(node.code) const isSelected = node.code === selectedCode const isEditing = editingCode === node.code + const navigationIcon = useNavigationIcons() const NodeIcon = node.icon ? navigationIcon[node.icon] : null const { translate } = useLocalization() diff --git a/ui/src/views/admin/organization-unit/OrganizationUnits.tsx b/ui/src/views/admin/organization-unit/OrganizationUnits.tsx index 1783a682..2f0c6674 100644 --- a/ui/src/views/admin/organization-unit/OrganizationUnits.tsx +++ b/ui/src/views/admin/organization-unit/OrganizationUnits.tsx @@ -47,7 +47,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization' import TableNoRecords from '@/views/shared/TableNoRecords' import classNames from 'classnames' import { Field, FieldProps, Form, Formik, FormikHelpers } from 'formik' -import isEmpty from 'lodash/isEmpty' +import { isEmpty } from '@/utils/collection' import { memo, Suspense, useEffect, useMemo, useState } from 'react' import { NodeApi, NodeRendererProps } from 'react-arborist' import { Tree } from 'react-arborist/dist/module/components/tree' diff --git a/ui/src/views/admin/role-management/Roles.tsx b/ui/src/views/admin/role-management/Roles.tsx index b8a1f4c3..18901165 100644 --- a/ui/src/views/admin/role-management/Roles.tsx +++ b/ui/src/views/admin/role-management/Roles.tsx @@ -3,7 +3,7 @@ import Container from '@/components/shared/Container' import { Button, Table, toast } from '@/components/ui' import { useLocalization } from '@/utils/hooks/useLocalization' import useThemeClass from '@/utils/hooks/useThemeClass' -import isEmpty from 'lodash/isEmpty' +import { isEmpty } from '@/utils/collection' import { useEffect, useState } from 'react' import PageTitle from '@/components/shared/PageTitle' import RolesPermission from './RolesPermission' diff --git a/ui/src/views/admin/tenant-management/Tenants.tsx b/ui/src/views/admin/tenant-management/Tenants.tsx index 52cbe3c7..64ca0f75 100644 --- a/ui/src/views/admin/tenant-management/Tenants.tsx +++ b/ui/src/views/admin/tenant-management/Tenants.tsx @@ -5,7 +5,7 @@ import { Table } from '@/components/ui' import { TenantDto } from '@/proxy/config/models' import { useLocalization } from '@/utils/hooks/useLocalization' import useThemeClass from '@/utils/hooks/useThemeClass' -import isEmpty from 'lodash/isEmpty' +import { isEmpty } from '@/utils/collection' import { useEffect, useState } from 'react' import PageTitle from '@/components/shared/PageTitle' import TenantsConnectionString from './TenantsConnectionString' diff --git a/ui/src/views/admin/user-management/Users.tsx b/ui/src/views/admin/user-management/Users.tsx index 185dc0fb..b051f3a6 100644 --- a/ui/src/views/admin/user-management/Users.tsx +++ b/ui/src/views/admin/user-management/Users.tsx @@ -4,7 +4,7 @@ import { Table } from '@/components/ui' import { getUsers } from '@/services/identity.service' import { useLocalization } from '@/utils/hooks/useLocalization' import useThemeClass from '@/utils/hooks/useThemeClass' -import isEmpty from 'lodash/isEmpty' +import { isEmpty } from '@/utils/collection' import { useEffect, useState } from 'react' import PageTitle from '@/components/shared/PageTitle' import UsersPermission from './UsersPermission' diff --git a/ui/src/views/auth/VerifyConfirmationCode.tsx b/ui/src/views/auth/VerifyConfirmationCode.tsx index e735b0d5..7f4afc58 100644 --- a/ui/src/views/auth/VerifyConfirmationCode.tsx +++ b/ui/src/views/auth/VerifyConfirmationCode.tsx @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query' +import { useEffect, useRef } from 'react' import { Alert, Button } from '@/components/ui' import { useNavigate, useParams } from 'react-router-dom' import useAccount from '@/utils/hooks/useAccount' @@ -12,12 +12,17 @@ const VerifyConfirmationCode = () => { const navigate = useNavigate() const { translate } = useLocalization() - useQuery({ - queryKey: ['verifyConfirmationCode', userId, token], - queryFn: () => verifyConfirmationCode(userId!, token!), - retry: false, - refetchOnWindowFocus: false, - }) + // Doğrulama tek seferlik bir yan etki; StrictMode'un çift render'ında + // isteğin iki kez gitmemesi için ref ile bir kereye kilitleniyor. + const verifiedKey = useRef(null) + + useEffect(() => { + if (!userId || !token) return + const key = `${userId}|${token}` + if (verifiedKey.current === key) return + verifiedKey.current = key + verifyConfirmationCode(userId, token) + }, [userId, token, verifyConfirmationCode]) return ( <> diff --git a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx index f0c9fc2a..1401503f 100644 --- a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx +++ b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx @@ -21,7 +21,7 @@ import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { getMenus } from '@/services/menu.service' import { useLocalization } from '@/utils/hooks/useLocalization' import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models' -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { MenuItem } from '@/proxy/menus/menu' import { MenuTreeNode, @@ -1122,6 +1122,7 @@ function SimpleMenuTreeNode({ const hasChildren = node.children.length > 0 const isExpanded = expanded.has(node.code) const isSelected = node.code === selectedCode + const navigationIcon = useNavigationIcons() const NodeIcon = node.icon ? navigationIcon[node.icon] : null const { translate } = useLocalization() diff --git a/ui/src/views/form/FormButtons.tsx b/ui/src/views/form/FormButtons.tsx index 246eb0e4..72133aea 100644 --- a/ui/src/views/form/FormButtons.tsx +++ b/ui/src/views/form/FormButtons.tsx @@ -1,5 +1,5 @@ import { Badge, Button, Dialog, Notification, toast } from '@/components/ui' -import navigationIcon from '@/proxy/menus/navigation-icon.config' +import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { useLocalization } from '@/utils/hooks/useLocalization' import CustomStore from 'devextreme/data/custom_store' import { useState } from 'react' @@ -162,6 +162,8 @@ const FormButtons = (props: { } } + const navigationIcon = useNavigationIcons() + const toolbarExcludedNames = ['deleteAllRecords', 'deleteSelectedRecords', 'refreshButton'] const hasVisibleToolbarButtons = toolbarData?.some(isToolbarButtonVisible) diff --git a/ui/src/views/intranet/SocialWall/MediaLightbox.tsx b/ui/src/views/intranet/SocialWall/MediaLightbox.tsx index a6be736d..2fc8f50f 100644 --- a/ui/src/views/intranet/SocialWall/MediaLightbox.tsx +++ b/ui/src/views/intranet/SocialWall/MediaLightbox.tsx @@ -1,10 +1,8 @@ import React from 'react' -import Lightbox from 'yet-another-react-lightbox' -import 'yet-another-react-lightbox/styles.css' -import Video from 'yet-another-react-lightbox/plugins/video' -import Zoom from 'yet-another-react-lightbox/plugins/zoom' -import Counter from 'yet-another-react-lightbox/plugins/counter' -import 'yet-another-react-lightbox/plugins/counter.css' +import { createPortal } from 'react-dom' +import { AnimatePresence, motion } from 'framer-motion' +import { FaChevronLeft, FaChevronRight, FaTimes } from 'react-icons/fa' +import { Button } from '@/components/ui' import { SocialMediaDto } from '@/proxy/intranet/models' interface MediaLightboxProps { @@ -14,53 +12,153 @@ interface MediaLightboxProps { startIndex?: number } +const MIN_ZOOM = 1 +const MAX_ZOOM = 3 +const ZOOM_STEP = 0.25 + +/** + * Sosyal duvar medya görüntüleyici. + * + * Daha önce `yet-another-react-lightbox` kullanıyordu; ihtiyaç duyulan yüzey + * (görsel gezinme + sayaç + tekerlek ile yakınlaştırma + video oynatma) burada + * zaten bağımlılık olan framer-motion ile karşılanıyor, böylece paket ve ona + * ait üç eklenti + CSS bundle'dan düşüyor. + */ const MediaLightbox: React.FC = ({ isOpen, onClose, media, - startIndex = 0 + startIndex = 0, }) => { - const slides = React.useMemo(() => { - if (media.type === 'video' && media.urls && media.urls.length > 0) { - return [ - { - type: 'video' as const, - sources: [ - { - src: media.urls[0], - type: 'video/mp4' - } - ] - } - ] + const isVideo = media.type === 'video' + const urls = React.useMemo(() => media.urls ?? [], [media]) + const [index, setIndex] = React.useState(startIndex) + const [zoom, setZoom] = React.useState(MIN_ZOOM) + + // Aynı bileşen farklı medya/indeks ile yeniden açılabiliyor. + React.useEffect(() => { + if (isOpen) { + setIndex(startIndex) + setZoom(MIN_ZOOM) + } + }, [isOpen, startIndex, media]) + + const step = React.useCallback( + (delta: number) => { + if (urls.length < 2) return + setIndex((current) => (current + delta + urls.length) % urls.length) + setZoom(MIN_ZOOM) + }, + [urls.length], + ) + + // Klavye ile gezinme ve kapatma; lightbox açıkken sayfa kaydırması kilitlenir. + React.useEffect(() => { + if (!isOpen) return + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose() + if (event.key === 'ArrowLeft') step(-1) + if (event.key === 'ArrowRight') step(1) } - const urls = media.urls || [] - return urls.map((url) => ({ - src: url - })) - }, [media]) + const previousOverflow = document.body.style.overflow + document.body.style.overflow = 'hidden' + window.addEventListener('keydown', onKeyDown) - return ( - + return () => { + document.body.style.overflow = previousOverflow + window.removeEventListener('keydown', onKeyDown) + } + }, [isOpen, onClose, step]) + + const onWheel = (event: React.WheelEvent) => { + if (isVideo) return + setZoom((current) => + Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, current - Math.sign(event.deltaY) * ZOOM_STEP)), + ) + } + + const stop = (event: React.MouseEvent) => event.stopPropagation() + + return createPortal( + + {isOpen && ( + +