SocialPost sayfalama özelliği ve showUpdateOverlay
This commit is contained in:
parent
871ee34536
commit
e9d8f5ebac
11 changed files with 167 additions and 19 deletions
|
|
@ -8,6 +8,7 @@ namespace Sozsoft.Platform.Intranet;
|
||||||
public interface IIntranetAppService : IApplicationService
|
public interface IIntranetAppService : IApplicationService
|
||||||
{
|
{
|
||||||
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
|
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
|
||||||
|
Task<List<SocialPostDto>> GetIntranetSocialPostsAsync(int skipCount, int maxResultCount);
|
||||||
Task CreateSurveyResponseAsync(SubmitSurveyInput input);
|
Task CreateSurveyResponseAsync(SubmitSurveyInput input);
|
||||||
Task<SocialPostDto> CreateSocialPostAsync(CreateSocialPostInput input);
|
Task<SocialPostDto> CreateSocialPostAsync(CreateSocialPostInput input);
|
||||||
Task DeleteSocialPostAsync(System.Guid id);
|
Task DeleteSocialPostAsync(System.Guid id);
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,4 @@ public class IntranetDashboardDto
|
||||||
public List<FileItemDto> Documents { get; set; } = [];
|
public List<FileItemDto> Documents { get; set; } = [];
|
||||||
public List<AnnouncementDto> Announcements { get; set; } = [];
|
public List<AnnouncementDto> Announcements { get; set; } = [];
|
||||||
public List<SurveyDto> Surveys { get; set; } = [];
|
public List<SurveyDto> Surveys { get; set; } = [];
|
||||||
public List<SocialPostDto> SocialPosts { get; set; } = [];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
Documents = await GetIntranetDocumentsAsync(BlobContainerNames.Intranet), //2
|
Documents = await GetIntranetDocumentsAsync(BlobContainerNames.Intranet), //2
|
||||||
Announcements = await GetAnnouncementsAsync(), //3
|
Announcements = await GetAnnouncementsAsync(), //3
|
||||||
Surveys = await GetSurveysAsync(), //4
|
Surveys = await GetSurveysAsync(), //4
|
||||||
SocialPosts = await GetSocialPostsAsync(), //5
|
Events = await GetUpcomingEventsAsync(), //5
|
||||||
Events = await GetUpcomingEventsAsync(), //6
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,12 +405,28 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
return dtos;
|
return dtos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<SocialPostDto>> GetSocialPostsAsync()
|
[UnitOfWork]
|
||||||
|
public async Task<List<SocialPostDto>> GetIntranetSocialPostsAsync(int skipCount, int maxResultCount)
|
||||||
{
|
{
|
||||||
|
// Önce sadece ID'leri sayfalayarak çek (collection include'lar olmadan)
|
||||||
|
var baseQueryable = await _socialPostRepository.GetQueryableAsync();
|
||||||
|
var pagedIds = await AsyncExecuter.ToListAsync(
|
||||||
|
baseQueryable
|
||||||
|
.OrderByDescending(p => p.CreationTime)
|
||||||
|
.Skip(skipCount)
|
||||||
|
.Take(maxResultCount)
|
||||||
|
.Select(p => p.Id));
|
||||||
|
|
||||||
|
if (pagedIds.Count == 0) return [];
|
||||||
|
|
||||||
|
// Sonra sadece bu ID'ler için detayları yükle
|
||||||
var queryable = await _socialPostRepository
|
var queryable = await _socialPostRepository
|
||||||
.WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes);
|
.WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes);
|
||||||
|
|
||||||
var socialPosts = await AsyncExecuter.ToListAsync(queryable.OrderByDescending(p => p.CreationTime));
|
var socialPosts = await AsyncExecuter.ToListAsync(
|
||||||
|
queryable
|
||||||
|
.Where(p => pagedIds.Contains(p.Id))
|
||||||
|
.OrderByDescending(p => p.CreationTime));
|
||||||
|
|
||||||
var dtos = ObjectMapper.Map<List<SocialPost>, List<SocialPostDto>>(socialPosts);
|
var dtos = ObjectMapper.Map<List<SocialPost>, List<SocialPostDto>>(socialPosts);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -538,7 +538,7 @@ public class ListFormWizardAppService(
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DbMigrator projesinin Seeds/WizardData dizinini ContentRootPath'ten yukarı traversal ile bulur.
|
/// DbMigrator projesinin Seeds/WizardData dizinini ContentRootPath'ten yukarı traversal ile bulur.
|
||||||
/// Tüm işletim sistemlerinde Path.Combine kullanır, separator karakteri içermez.
|
/// Tüm işletim sistemlerinde Path.Combine kullanır, separator karakteri içermez.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string ResolveWizardSeedOutputPath()
|
private string ResolveWizardSeedOutputPath()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12288,6 +12288,12 @@
|
||||||
"tr": "Anketi Güncelle",
|
"tr": "Anketi Güncelle",
|
||||||
"en": "Update Survey"
|
"en": "Update Survey"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.Intranet.SocialWall.AllPostsLoaded",
|
||||||
|
"tr": "Tüm gönderiler yüklendi",
|
||||||
|
"en": "All posts loaded"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps",
|
"key": "App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"RedirectAllowedUrls": "http://localhost:4200,http://localhost:4200/authentication/callback",
|
"RedirectAllowedUrls": "http://localhost:4200,http://localhost:4200/authentication/callback",
|
||||||
"AttachmentsPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\mail-queue\\attachments",
|
"AttachmentsPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\mail-queue\\attachments",
|
||||||
"CdnPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\docker\\cdn",
|
"CdnPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\docker\\cdn",
|
||||||
"Version": "1.0.1"
|
"Version": "1.0.5"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"SqlServer": "Server=localhost;Database=Sozsoft;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
"SqlServer": "Server=localhost;Database=Sozsoft;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
||||||
|
|
|
||||||
2
ui/.env
2
ui/.env
|
|
@ -19,4 +19,4 @@ VITE_GOOGLE_MAPS_API_KEY='AIzaSyAefS2rvF-xwq7OHpZ27UYxXPbMo6OwACc'
|
||||||
# Eğer Restrict key seçiliyse, izinli API listesine şunları ekle:
|
# Eğer Restrict key seçiliyse, izinli API listesine şunları ekle:
|
||||||
|
|
||||||
# Maps JavaScript API
|
# Maps JavaScript API
|
||||||
# Places API
|
# Places API
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,16 @@ export class IntranetService {
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
getIntranetSocialPosts = (skipCount: number, maxResultCount: number, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<SocialPostDto[]>(
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/app/intranet/intranet-social-posts',
|
||||||
|
params: { skipCount, maxResultCount },
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
|
|
||||||
createSurveyResponse = (
|
createSurveyResponse = (
|
||||||
surveyId: string,
|
surveyId: string,
|
||||||
answers: { questionId: string; questionType: string; value: string }[],
|
answers: { questionId: string; questionType: string; value: string }[],
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ const IntranetDashboard: React.FC = () => {
|
||||||
<Surveys surveys={intranetDashboard?.surveys || []} onTakeSurvey={handleTakeSurvey} />
|
<Surveys surveys={intranetDashboard?.surveys || []} onTakeSurvey={handleTakeSurvey} />
|
||||||
)
|
)
|
||||||
case 'social-wall':
|
case 'social-wall':
|
||||||
return <SocialWall posts={intranetDashboard?.socialPosts || []} />
|
return <SocialWall />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,58 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { AnimatePresence } from 'framer-motion'
|
import { AnimatePresence } from 'framer-motion'
|
||||||
import PostItem from './PostItem'
|
import PostItem from './PostItem'
|
||||||
import CreatePost from './CreatePost'
|
import CreatePost from './CreatePost'
|
||||||
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
|
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { intranetService } from '@/services/intranet.service'
|
import { intranetService } from '@/services/intranet.service'
|
||||||
|
|
||||||
const SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: SocialPostDto[]) => void }> = ({ posts: initialPosts, onPostsChange }) => {
|
const PAGE_SIZE = 10
|
||||||
const [posts, setPosts] = useState<SocialPostDto[]>(initialPosts)
|
|
||||||
|
|
||||||
|
const SocialWall: React.FC = () => {
|
||||||
|
const [posts, setPosts] = useState<SocialPostDto[]>([])
|
||||||
|
const [skipCount, setSkipCount] = useState(0)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
const sentinelRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (loadingMore || !hasMore) return
|
||||||
|
setLoadingMore(true)
|
||||||
|
try {
|
||||||
|
const res = await intranetService.getIntranetSocialPosts(skipCount, PAGE_SIZE)
|
||||||
|
const newPosts = res.data ?? []
|
||||||
|
setPosts((prev) => {
|
||||||
|
const existingIds = new Set(prev.map((p) => p.id))
|
||||||
|
const unique = newPosts.filter((p) => !existingIds.has(p.id))
|
||||||
|
return [...prev, ...unique]
|
||||||
|
})
|
||||||
|
setSkipCount((s) => s + newPosts.length)
|
||||||
|
setHasMore(newPosts.length === PAGE_SIZE)
|
||||||
|
} catch {
|
||||||
|
// error handled by apiService
|
||||||
|
} finally {
|
||||||
|
setLoadingMore(false)
|
||||||
|
}
|
||||||
|
}, [loadingMore, hasMore, skipCount])
|
||||||
|
|
||||||
|
// Sentinel observer — ilk yükleme de dahil tüm sayfaları bu tetikler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPosts(initialPosts)
|
const sentinel = sentinelRef.current
|
||||||
}, [initialPosts])
|
if (!sentinel) return
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) loadMore()
|
||||||
|
},
|
||||||
|
{ rootMargin: '200px' },
|
||||||
|
)
|
||||||
|
observer.observe(sentinel)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [loadMore])
|
||||||
const [filter, setFilter] = useState<'all' | 'mine'>('all')
|
const [filter, setFilter] = useState<'all' | 'mine'>('all')
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
const { user } = useStoreState((state) => state.auth)
|
|
||||||
|
|
||||||
const updatePosts = (updated: SocialPostDto[]) => {
|
const updatePosts = (updated: SocialPostDto[]) => {
|
||||||
setPosts(updated)
|
setPosts(updated)
|
||||||
onPostsChange?.(updated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreatePost = async (postData: {
|
const handleCreatePost = async (postData: {
|
||||||
|
|
@ -190,6 +222,23 @@ const SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: Soc
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Infinite scroll sentinel */}
|
||||||
|
{filter === 'all' && (
|
||||||
|
<>
|
||||||
|
<div ref={sentinelRef} className="h-1" />
|
||||||
|
{loadingMore && (
|
||||||
|
<div className="flex justify-center py-6">
|
||||||
|
<div className="w-8 h-8 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasMore && posts.length > 0 && (
|
||||||
|
<p className="text-center text-sm text-gray-400 dark:text-gray-600 py-6">
|
||||||
|
{translate('::App.Platform.Intranet.SocialWall.AllPostsLoaded') || 'Tüm gönderiler yüklendi'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,78 @@
|
||||||
import { registerSW } from 'virtual:pwa-register'
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
|
||||||
|
function showUpdateOverlay() {
|
||||||
|
if (document.getElementById('sw-update-overlay')) return
|
||||||
|
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.id = 'sw-update-overlay-style'
|
||||||
|
style.textContent = `
|
||||||
|
#sw-update-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
#sw-update-overlay .sw-update-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px 56px;
|
||||||
|
box-shadow: 0 8px 40px rgba(0,0,0,0.25);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 360px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
#sw-update-overlay .sw-update-spinner {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border: 5px solid #e5e7eb;
|
||||||
|
border-top-color: #6366f1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: sw-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes sw-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
#sw-update-overlay .sw-update-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#sw-update-overlay .sw-update-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.id = 'sw-update-overlay'
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="sw-update-card">
|
||||||
|
<div class="sw-update-spinner"></div>
|
||||||
|
<p class="sw-update-title">System Updating</p>
|
||||||
|
<p class="sw-update-desc">Loading new version, please wait…<br/>The page will refresh automatically.</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
document.body.appendChild(overlay)
|
||||||
|
}
|
||||||
|
|
||||||
export const registerServiceWorker = () => {
|
export const registerServiceWorker = () => {
|
||||||
registerSW({
|
registerSW({
|
||||||
immediate: true,
|
immediate: true,
|
||||||
onNeedRefresh() {
|
onNeedRefresh() {
|
||||||
console.log('🔔 New version available, please refresh.')
|
console.log('🔔 New version available, refreshing…')
|
||||||
|
showUpdateOverlay()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
},
|
},
|
||||||
onOfflineReady() {
|
onOfflineReady() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue