SocialPost sayfalama özelliği ve showUpdateOverlay

This commit is contained in:
Sedat Öztürk 2026-05-09 00:48:23 +03:00
parent 871ee34536
commit e9d8f5ebac
11 changed files with 167 additions and 19 deletions

View file

@ -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);

View file

@ -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; } = [];
} }

View file

@ -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);

View file

@ -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()
{ {

View file

@ -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",

View file

@ -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;",

View file

@ -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

View file

@ -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 }[],

View file

@ -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
} }

View file

@ -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>
) )
} }

View file

@ -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() {