Versiyon güncelleme
This commit is contained in:
parent
6766d1129d
commit
9e85780623
14 changed files with 371 additions and 250 deletions
|
|
@ -7528,7 +7528,7 @@
|
||||||
{
|
{
|
||||||
"key": "admin.changeLog",
|
"key": "admin.changeLog",
|
||||||
"path": "/admin/changeLog",
|
"path": "/admin/changeLog",
|
||||||
"componentPath": "@/views/docs/ChangeLog",
|
"componentPath": "@/views/version/ChangeLog",
|
||||||
"routeType": "protected",
|
"routeType": "protected",
|
||||||
"authority": []
|
"authority": []
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"AttachmentsPath": "C:\\Private\\Projects\\sozsoft\\configs\\mail-queue\\attachments",
|
"AttachmentsPath": "C:\\Private\\Projects\\sozsoft\\configs\\mail-queue\\attachments",
|
||||||
"CdnPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\cdn",
|
"CdnPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\cdn",
|
||||||
"ImportPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\import",
|
"ImportPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\import",
|
||||||
"Version": "1.0.4"
|
"Version": "1.0.1"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"SqlServer": "Server=localhost;Database=KURS;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;TrustServerCertificate=True;",
|
"SqlServer": "Server=localhost;Database=KURS;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;TrustServerCertificate=True;",
|
||||||
|
|
|
||||||
1
ui/.gitignore
vendored
1
ui/.gitignore
vendored
|
|
@ -9,6 +9,7 @@ lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
dev-dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
build
|
build
|
||||||
*.local
|
*.local
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ define(['./workbox-a959eb95'], (function (workbox) { 'use strict';
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "/index.html",
|
"url": "/index.html",
|
||||||
"revision": "0.agtbclbpej8"
|
"revision": "0.jq60tbu0hgg"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "kurs-platform-ui",
|
"name": "kurs-platform-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.4",
|
"version": "1.0.1",
|
||||||
"elstarVersion": "2.1.6",
|
"elstarVersion": "2.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build && node scripts/write-version.js",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.json",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.json",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
|
|
||||||
35
ui/public/version.json
Normal file
35
ui/public/version.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"commit": "6766d11",
|
||||||
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "1.0.5",
|
||||||
|
"buildDate": "2025-09-19",
|
||||||
|
"changeLog": [
|
||||||
|
"Form ekranındaki Butonlar güncellemeleri yapıldı",
|
||||||
|
"Edit Form ekranındaki Info butonu eklendi.",
|
||||||
|
"New Form ekranındaki Geri butonu eklendi."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.4",
|
||||||
|
"buildDate": "2025-09-19",
|
||||||
|
"changeLog": [
|
||||||
|
"Subformlar üzerinde extra filters ve Widget çalışmaları yapıldı."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.3",
|
||||||
|
"buildDate": "2025-09-19",
|
||||||
|
"changeLog": [
|
||||||
|
"Manage Grid üzerinde Extra filtre tanımlaması yapıldı."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.2",
|
||||||
|
"buildDate": "2025-09-16",
|
||||||
|
"changeLog": [
|
||||||
|
"Genel Static olan Url bilgileri kaldırıldı."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
46
ui/scripts/write-version.js
Normal file
46
ui/scripts/write-version.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import fs from "fs"
|
||||||
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
|
function safeExec(cmd) {
|
||||||
|
try {
|
||||||
|
return execSync(cmd, { stdio: ["pipe", "pipe", "ignore"] }).toString().trim()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tüm tag isimlerini al
|
||||||
|
const rawTags = safeExec("git tag --list --sort=creatordate")
|
||||||
|
|
||||||
|
if (!rawTags) {
|
||||||
|
console.log("> No git tags found, skipping version.json")
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = rawTags
|
||||||
|
.split("\n")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((tag) => {
|
||||||
|
const date = safeExec(`git log -1 --format=%ad --date=short ${tag}`)
|
||||||
|
const messageRaw = safeExec(`git tag -l --format="%(contents)" ${tag}`)
|
||||||
|
|
||||||
|
const changeLog = messageRaw
|
||||||
|
? messageRaw.split("\n").map((s) => s.trim()).filter(Boolean)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: tag.replace(/^v/, ""), // v1.0.5 → 1.0.5
|
||||||
|
buildDate: date,
|
||||||
|
changeLog
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const commit = safeExec("git rev-parse --short HEAD")
|
||||||
|
|
||||||
|
const versionInfo = {
|
||||||
|
commit,
|
||||||
|
releases: tags.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync("public/version.json", JSON.stringify(versionInfo, null, 2))
|
||||||
|
console.log("> Version file written to public/version.json:", versionInfo)
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
// src/components/UpdateNotifier.jsx
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
const UpdateNotifier = () => {
|
|
||||||
const [updateAvailable, setUpdateAvailable] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
const checkUpdate = () => {
|
|
||||||
navigator.serviceWorker.getRegistration().then(registration => {
|
|
||||||
if (registration) {
|
|
||||||
registration.addEventListener('updatefound', () => {
|
|
||||||
setUpdateAvailable(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
checkUpdate()
|
|
||||||
const interval = setInterval(checkUpdate, 30000) // 30 saniyede bir kontrol
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!updateAvailable) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '20px',
|
|
||||||
right: '20px',
|
|
||||||
padding: '12px',
|
|
||||||
background: '#FF99C8',
|
|
||||||
color: 'white',
|
|
||||||
borderRadius: '8px',
|
|
||||||
zIndex: 1000
|
|
||||||
}}>
|
|
||||||
<p>Yeni güncelleme mevcut!</p>
|
|
||||||
<button onClick={handleUpdate} style={{
|
|
||||||
background: 'white',
|
|
||||||
color: '#FF99C8',
|
|
||||||
border: 'none',
|
|
||||||
padding: '8px 16px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}>
|
|
||||||
Yenile
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UpdateNotifier
|
|
||||||
|
|
@ -15,7 +15,7 @@ import useLocale from '@/utils/hooks/useLocale'
|
||||||
import { useDynamicRoutes } from '@/routes/dynamicRoutesContext'
|
import { useDynamicRoutes } from '@/routes/dynamicRoutesContext'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { hasSubdomain } from '@/utils/subdomain'
|
import { hasSubdomain } from '@/utils/subdomain'
|
||||||
import UpdateNotifier from '../UpdateNotifier'
|
import UpdateNotifier from '../../views/version/UpdateNotifier'
|
||||||
|
|
||||||
export type LayoutType =
|
export type LayoutType =
|
||||||
| typeof LAYOUT_TYPE_CLASSIC
|
| typeof LAYOUT_TYPE_CLASSIC
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import classNames from 'classnames'
|
import { useEffect, useState } from "react"
|
||||||
import Container from '@/components/shared/Container'
|
import classNames from "classnames"
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import Container from "@/components/shared/Container"
|
||||||
import { PAGE_CONTAINER_GUTTER_X } from '@/constants/theme.constant'
|
import { APP_NAME } from "@/constants/app.constant"
|
||||||
import { useStoreActions, useStoreState } from '@/store'
|
import { PAGE_CONTAINER_GUTTER_X } from "@/constants/theme.constant"
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { useStoreActions, useStoreState } from "@/store"
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
import UiDialog from '@/views/shared/UiDialog'
|
import { ROUTES_ENUM } from "@/routes/route.constant"
|
||||||
|
import UiDialog from "@/views/shared/UiDialog"
|
||||||
|
|
||||||
export type FooterPageContainerType = 'gutterless' | 'contained'
|
export type FooterPageContainerType = "gutterless" | "contained"
|
||||||
|
|
||||||
type FooterProps = {
|
type FooterProps = {
|
||||||
pageContainerType: FooterPageContainerType
|
pageContainerType: FooterPageContainerType
|
||||||
|
|
@ -21,7 +22,20 @@ const FooterContent = () => {
|
||||||
const apiConfig = useStoreState((state) => state.abpConfig.config?.extraProperties)
|
const apiConfig = useStoreState((state) => state.abpConfig.config?.extraProperties)
|
||||||
|
|
||||||
const uiMode = import.meta.env.MODE
|
const uiMode = import.meta.env.MODE
|
||||||
const reactAppVersion = import.meta.env.VITE_REACT_APP_VERSION
|
|
||||||
|
const [latestVersion, setLatestVersion] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// version.json'dan en güncel UI versiyonunu al
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/version.json?ts=" + Date.now())
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data?.releases?.length > 0) {
|
||||||
|
setLatestVersion(data.releases[0].version) // en güncel hep en üstte
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setLatestVersion(null))
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -40,24 +54,25 @@ const FooterContent = () => {
|
||||||
{apiConfig && (
|
{apiConfig && (
|
||||||
<span>
|
<span>
|
||||||
<b>API: </b>
|
<b>API: </b>
|
||||||
{apiConfig['environment'].toString()}:{apiConfig['version'].toString()}
|
{apiConfig["environment"].toString()}:{apiConfig["version"].toString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{reactAppVersion != currentUiVersion && (
|
|
||||||
|
{latestVersion && latestVersion !== currentUiVersion && (
|
||||||
<UiDialog
|
<UiDialog
|
||||||
key={`version-${reactAppVersion }`}
|
key={`version-${latestVersion}`}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
type="info"
|
type="info"
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
setUiVersion(reactAppVersion )
|
setUiVersion(latestVersion)
|
||||||
navigate(ROUTES_ENUM.protected.admin.changeLog)
|
navigate(ROUTES_ENUM.protected.admin.changeLog)
|
||||||
}}
|
}}
|
||||||
title="🎉 Yeni Güncelleme"
|
title="🎉 Yeni Güncelleme"
|
||||||
>
|
>
|
||||||
Sözsoft Kurs Platform Sistemi güncellendi.
|
Sözsoft Kurs Platform Sistemi <b>v{latestVersion}</b> sürümüne güncellendi.
|
||||||
<p>Detayları, "Güncelleme Günlüğü" ekranında görebilirsiniz.</p>
|
<p>Detayları, "Güncelleme Günlüğü" ekranında görebilirsiniz.</p>
|
||||||
</UiDialog>
|
</UiDialog>
|
||||||
)}
|
)}
|
||||||
|
|
@ -65,12 +80,14 @@ const FooterContent = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Footer({ pageContainerType = 'contained' }: FooterProps) {
|
export default function Footer({ pageContainerType = "contained" }: FooterProps) {
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className={classNames(`print:hidden footer flex flex-auto items-center h-6 ${PAGE_CONTAINER_GUTTER_X}`)}
|
className={classNames(
|
||||||
|
`print:hidden footer flex flex-auto items-center h-6 ${PAGE_CONTAINER_GUTTER_X}`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{pageContainerType === 'contained' ? (
|
{pageContainerType === "contained" ? (
|
||||||
<Container>
|
<Container>
|
||||||
<FooterContent />
|
<FooterContent />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import AdaptableCard from '@/components/shared/AdaptableCard'
|
|
||||||
import Container from '@/components/shared/Container'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import { Helmet } from 'react-helmet'
|
|
||||||
|
|
||||||
type Log = {
|
|
||||||
version: string
|
|
||||||
date: string
|
|
||||||
updateContent: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogProps = Omit<Log, 'updateContent'> & {
|
|
||||||
border?: boolean
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const logData: Log[] = [
|
|
||||||
{
|
|
||||||
version: '1.0.3',
|
|
||||||
date: '04 Şubat 2025',
|
|
||||||
updateContent: ['[Fix] GridBoxEditorComponent bug', '[Fix] TagBoxEditorComponent bug'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: '1.0.2',
|
|
||||||
date: '27 Ocak 2025',
|
|
||||||
updateContent: ['[Add] jspdf kurulumu', '[Add] exceljs kurulumu', '[Add] file-saver kurulumu'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: '1.0.1',
|
|
||||||
date: '23 Ocak 2025',
|
|
||||||
updateContent: ['[Fix] Bağımlılık güvenlik açığı'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: '1.0.0',
|
|
||||||
date: '20 Ocak 2025',
|
|
||||||
updateContent: ['[Update] İlk yeni sürüm'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const Log = (props: LogProps) => {
|
|
||||||
return (
|
|
||||||
<div className={`py-4 ${props.border && 'border-bottom'}`}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h5 className="font-weight-normal mb-0 mr-3">{props.version}</h5>
|
|
||||||
<code>{props.date}</code>
|
|
||||||
</div>
|
|
||||||
<div className="api-container p-0 border-0 mt-3">{props.children}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Changelog = () => {
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Helmet
|
|
||||||
titleTemplate="%s | Sözsoft Kurs Platform"
|
|
||||||
title={translate('::' + 'App.ChangeLog')}
|
|
||||||
defaultTitle="Sözsoft Kurs Platform"
|
|
||||||
></Helmet>
|
|
||||||
<AdaptableCard>
|
|
||||||
<h4>Platform Güncelleme Günlüğü</h4>
|
|
||||||
{logData.map((elm) => (
|
|
||||||
<Log key={elm.version} version={`v${elm.version}`} date={elm.date}>
|
|
||||||
{elm.updateContent.length > 0 ? (
|
|
||||||
<ul>
|
|
||||||
{elm.updateContent.map((item, i) => (
|
|
||||||
<li key={i}>- {item}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : null}
|
|
||||||
</Log>
|
|
||||||
))}
|
|
||||||
</AdaptableCard>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Changelog
|
|
||||||
77
ui/src/views/version/ChangeLog.tsx
Normal file
77
ui/src/views/version/ChangeLog.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import AdaptableCard from "@/components/shared/AdaptableCard"
|
||||||
|
import Container from "@/components/shared/Container"
|
||||||
|
import { HiTag, HiCheckCircle } from "react-icons/hi"
|
||||||
|
import { useLocalization } from "@/utils/hooks/useLocalization"
|
||||||
|
import { Helmet } from "react-helmet"
|
||||||
|
|
||||||
|
type Release = {
|
||||||
|
version: string
|
||||||
|
buildDate: string
|
||||||
|
changeLog: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Log = ({ version, date, children }: { version: string; date: string; children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="relative pl-4 sm:pl-32 py-4 group">
|
||||||
|
<div className="flex flex-col sm:flex-row items-start mb-1 group-last:before:hidden before:absolute before:left-2 sm:before:left-0 before:h-full before:px-px before:bg-slate-200 sm:before:ml-[6.5rem] before:self-start before:-translate-x-1/2 before:translate-y-3 after:absolute after:left-2 sm:after:left-0 after:w-2 after:h-2 after:bg-indigo-600 after:border-4 after:box-content after:border-slate-50 after:rounded-full sm:after:ml-[6.5rem] after:-translate-x-1/2 after:translate-y-1.5">
|
||||||
|
<time className="sm:absolute left-0 translate-y-0.5 inline-flex items-center justify-center text-xs font-semibold uppercase w-20 h-6 mb-3 sm:mb-0 text-emerald-600 bg-emerald-100 rounded-full">
|
||||||
|
{date}
|
||||||
|
</time>
|
||||||
|
<div className="flex items-center text-xl font-bold text-gray-900">
|
||||||
|
<HiTag className="mr-2 text-indigo-500" />
|
||||||
|
v{version}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-500">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Changelog = () => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
const [releases, setReleases] = useState<Release[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/version.json?ts=" + Date.now())
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data?.releases) {
|
||||||
|
setReleases(data.releases)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setReleases([]))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Helmet
|
||||||
|
titleTemplate="%s | Sözsoft Kurs Platform"
|
||||||
|
title={translate("::App.ChangeLog")}
|
||||||
|
defaultTitle="Sözsoft Kurs Platform"
|
||||||
|
/>
|
||||||
|
<AdaptableCard>
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{releases.map((rel) => (
|
||||||
|
<Log key={rel.version} version={rel.version} date={rel.buildDate}>
|
||||||
|
{rel.changeLog?.length > 0 && (
|
||||||
|
<ul className="list-none mt-2 space-y-2">
|
||||||
|
{rel.changeLog.map((item, i) => (
|
||||||
|
<li key={i} className="flex items-start">
|
||||||
|
<HiCheckCircle className="w-4 h-4 text-emerald-500 mr-2 flex-shrink-0" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Log>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdaptableCard>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Changelog
|
||||||
71
ui/src/views/version/UpdateNotifier.tsx
Normal file
71
ui/src/views/version/UpdateNotifier.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { HiOutlineRefresh, HiX } from 'react-icons/hi'
|
||||||
|
import { useStoreState, useStoreActions } from '@/store'
|
||||||
|
|
||||||
|
const UpdateNotifier = () => {
|
||||||
|
const [lastUiVersion, setLastUiVersion] = useState('')
|
||||||
|
const [updateAvailable, setUpdateAvailable] = useState(false)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { currentUiVersion } = useStoreState((s) => s.locale)
|
||||||
|
const { setUiVersion } = useStoreActions((s) => s.locale)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkVersion = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/version.json?ts=' + Date.now())
|
||||||
|
const data = await res.json()
|
||||||
|
const latestVersion = data?.releases?.[0]?.version
|
||||||
|
if (latestVersion && latestVersion !== currentUiVersion) {
|
||||||
|
setUpdateAvailable(true)
|
||||||
|
setLastUiVersion(latestVersion)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Version check failed', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkVersion()
|
||||||
|
const interval = setInterval(checkVersion, 30000) // 30s’de bir kontrol
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [currentUiVersion])
|
||||||
|
|
||||||
|
if (!updateAvailable) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-5 right-5 z-50 flex items-center justify-between gap-4 rounded-lg bg-sky-500 p-4 text-white shadow-lg animate-bounce"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<HiOutlineRefresh className="h-8 w-8 animate-spin" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Yeni güncelleme mevcut!</p>
|
||||||
|
<p className="text-sm">En son özellikler için sayfayı yenileyin.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setUiVersion(lastUiVersion)
|
||||||
|
navigate(ROUTES_ENUM.protected.admin.changeLog)
|
||||||
|
setUpdateAvailable(false)
|
||||||
|
}}
|
||||||
|
className="rounded bg-white px-4 py-2 text-sm font-bold text-sky-600 transition hover:bg-sky-100"
|
||||||
|
>
|
||||||
|
Yenile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setUpdateAvailable(false)}
|
||||||
|
className="rounded-full p-1 transition hover:bg-sky-600"
|
||||||
|
aria-label="Kapat"
|
||||||
|
>
|
||||||
|
<HiX className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateNotifier
|
||||||
|
|
@ -19,7 +19,8 @@ export default defineConfig(async ({ mode }) => {
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
VitePWA({
|
mode === 'production'
|
||||||
|
? VitePWA({
|
||||||
// Deploy'dan sonra otomatik güncelle
|
// Deploy'dan sonra otomatik güncelle
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
// Kayıt kodunu otomatik enjekte et (virtual:pwa-register yazmadan da çalışır)
|
// Kayıt kodunu otomatik enjekte et (virtual:pwa-register yazmadan da çalışır)
|
||||||
|
|
@ -27,7 +28,7 @@ export default defineConfig(async ({ mode }) => {
|
||||||
// Dev ortamında SW'yi aç, prod'da kapalı tut (build edilmiş SW prod'da zaten aktif olur)
|
// Dev ortamında SW'yi aç, prod'da kapalı tut (build edilmiş SW prod'da zaten aktif olur)
|
||||||
devOptions: {
|
devOptions: {
|
||||||
enabled: mode !== 'production',
|
enabled: mode !== 'production',
|
||||||
type: 'module' // Modern module worker kullan
|
type: 'module', // Modern module worker kullan
|
||||||
},
|
},
|
||||||
|
|
||||||
workbox: {
|
workbox: {
|
||||||
|
|
@ -39,13 +40,15 @@ export default defineConfig(async ({ mode }) => {
|
||||||
skipWaiting: true,
|
skipWaiting: true,
|
||||||
|
|
||||||
// Eski workbox cache'lerini temizle
|
// Eski workbox cache'lerini temizle
|
||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: mode === 'production',
|
||||||
|
|
||||||
// SPA fallback'i API çağrılarına uygulama
|
// SPA fallback'i API çağrılarına uygulama
|
||||||
navigateFallbackDenylist: [/^\/api\//],
|
navigateFallbackDenylist: [/^\/api\//],
|
||||||
|
|
||||||
// ⭐⭐ BU KISMI EKLEYİN: Cache sorununu çözecek runtime caching
|
// ⭐⭐ BU KISMI EKLEYİN: Cache sorununu çözecek runtime caching
|
||||||
runtimeCaching: [
|
runtimeCaching:
|
||||||
|
mode === 'production'
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
urlPattern: /\.(?:js|css|html|json)$/,
|
urlPattern: /\.(?:js|css|html|json)$/,
|
||||||
handler: 'NetworkFirst',
|
handler: 'NetworkFirst',
|
||||||
|
|
@ -53,12 +56,12 @@ export default defineConfig(async ({ mode }) => {
|
||||||
cacheName: 'static-resources',
|
cacheName: 'static-resources',
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 50,
|
maxEntries: 50,
|
||||||
maxAgeSeconds: 24 * 60 * 60 // 24 saat
|
maxAgeSeconds: 24 * 60 * 60, // 24 saat
|
||||||
},
|
},
|
||||||
cacheableResponse: {
|
cacheableResponse: {
|
||||||
statuses: [0, 200]
|
statuses: [0, 200],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/,
|
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/,
|
||||||
|
|
@ -67,15 +70,16 @@ export default defineConfig(async ({ mode }) => {
|
||||||
cacheName: 'images',
|
cacheName: 'images',
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 100,
|
maxEntries: 100,
|
||||||
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 gün
|
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 gün
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
]
|
||||||
|
: [],
|
||||||
|
|
||||||
// ⭐ YENİ EKLENEN: Additional navigation route for SPA
|
// ⭐ YENİ EKLENEN: Additional navigation route for SPA
|
||||||
navigateFallback: '/index.html',
|
navigateFallback: '/index.html',
|
||||||
navigateFallbackAllowlist: [/^(?!\/__).*/]
|
navigateFallbackAllowlist: [/^(?!\/__).*/],
|
||||||
},
|
},
|
||||||
|
|
||||||
manifest: {
|
manifest: {
|
||||||
|
|
@ -89,25 +93,26 @@ export default defineConfig(async ({ mode }) => {
|
||||||
src: '/img/logo/logo-400.png',
|
src: '/img/logo/logo-400.png',
|
||||||
sizes: '400x400',
|
sizes: '400x400',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any maskable'
|
purpose: 'any maskable',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/img/logo/logo-192.png',
|
src: '/img/logo/logo-192.png',
|
||||||
sizes: '192x192',
|
sizes: '192x192',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any maskable'
|
purpose: 'any maskable',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/img/logo/logo-512.png',
|
src: '/img/logo/logo-512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any maskable'
|
purpose: 'any maskable',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
categories: ['business', 'productivity'],
|
categories: ['business', 'productivity'],
|
||||||
description: 'Sözsoft Kurs Platform Application'
|
description: 'Sözsoft Kurs Platform Application',
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
|
: null,
|
||||||
],
|
],
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
|
|
@ -116,8 +121,8 @@ export default defineConfig(async ({ mode }) => {
|
||||||
// ⭐ YENİ EKLENEN: Hot reload için polling
|
// ⭐ YENİ EKLENEN: Hot reload için polling
|
||||||
watch: {
|
watch: {
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
interval: 1000
|
interval: 1000,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
assetsInclude: ['**/*.md'],
|
assetsInclude: ['**/*.md'],
|
||||||
|
|
@ -133,7 +138,14 @@ export default defineConfig(async ({ mode }) => {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
emptyOutDir: true, // ✅ Build öncesi otomatik temizlik
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: `assets/[name].[hash].js`,
|
||||||
|
chunkFileNames: `assets/[name].[hash].js`,
|
||||||
|
assetFileNames: `assets/[name].[hash].[ext]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
preview: {
|
preview: {
|
||||||
|
|
@ -147,9 +159,9 @@ export default defineConfig(async ({ mode }) => {
|
||||||
define: {
|
define: {
|
||||||
'process.env': {},
|
'process.env': {},
|
||||||
// ⭐ YENİ EKLENEN: Version tracking için global değişkenler
|
// ⭐ YENİ EKLENEN: Version tracking için global değişkenler
|
||||||
__APP_VERSION__: JSON.stringify(process.env.npm_package_version || '1.0.0'),
|
__APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || '1.0.0'),
|
||||||
__BUILD_DATE__: JSON.stringify(new Date().toISOString()),
|
__BUILD_DATE__: JSON.stringify(new Date().toISOString()),
|
||||||
__APP_MODE__: JSON.stringify(mode)
|
__APP_MODE__: JSON.stringify(mode),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Loading…
Reference in a new issue