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
{
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
Task<List<SocialPostDto>> GetIntranetSocialPostsAsync(int skipCount, int maxResultCount);
Task CreateSurveyResponseAsync(SubmitSurveyInput input);
Task<SocialPostDto> CreateSocialPostAsync(CreateSocialPostInput input);
Task DeleteSocialPostAsync(System.Guid id);

View file

@ -11,5 +11,4 @@ public class IntranetDashboardDto
public List<FileItemDto> Documents { get; set; } = [];
public List<AnnouncementDto> Announcements { 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
Announcements = await GetAnnouncementsAsync(), //3
Surveys = await GetSurveysAsync(), //4
SocialPosts = await GetSocialPostsAsync(), //5
Events = await GetUpcomingEventsAsync(), //6
Events = await GetUpcomingEventsAsync(), //5
};
}
@ -406,12 +405,28 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
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
.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);

View file

@ -12288,6 +12288,12 @@
"tr": "Anketi Güncelle",
"en": "Update Survey"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.SocialWall.AllPostsLoaded",
"tr": "Tüm gönderiler yüklendi",
"en": "All posts loaded"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps",

View file

@ -6,7 +6,7 @@
"RedirectAllowedUrls": "http://localhost:4200,http://localhost:4200/authentication/callback",
"AttachmentsPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\mail-queue\\attachments",
"CdnPath": "C:\\Private\\Projects\\sozsoft-platform\\configs\\docker\\cdn",
"Version": "1.0.1"
"Version": "1.0.5"
},
"ConnectionStrings": {
"SqlServer": "Server=localhost;Database=Sozsoft;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",

View file

@ -24,6 +24,16 @@ export class IntranetService {
{ 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 = (
surveyId: string,
answers: { questionId: string; questionType: string; value: string }[],

View file

@ -262,7 +262,7 @@ const IntranetDashboard: React.FC = () => {
<Surveys surveys={intranetDashboard?.surveys || []} onTakeSurvey={handleTakeSurvey} />
)
case 'social-wall':
return <SocialWall posts={intranetDashboard?.socialPosts || []} />
return <SocialWall />
default:
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 { AnimatePresence } from 'framer-motion'
import PostItem from './PostItem'
import CreatePost from './CreatePost'
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
import { useStoreState } from '@/store/store'
import { intranetService } from '@/services/intranet.service'
const SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: SocialPostDto[]) => void }> = ({ posts: initialPosts, onPostsChange }) => {
const [posts, setPosts] = useState<SocialPostDto[]>(initialPosts)
const PAGE_SIZE = 10
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(() => {
setPosts(initialPosts)
}, [initialPosts])
const sentinel = sentinelRef.current
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 { translate } = useLocalization()
const { user } = useStoreState((state) => state.auth)
const updatePosts = (updated: SocialPostDto[]) => {
setPosts(updated)
onPostsChange?.(updated)
}
const handleCreatePost = async (postData: {
@ -190,6 +222,23 @@ const SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: Soc
</div>
)}
</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>
)
}

View file

@ -1,10 +1,78 @@
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 = () => {
registerSW({
immediate: true,
onNeedRefresh() {
console.log('🔔 New version available, please refresh.')
console.log('🔔 New version available, refreshing…')
showUpdateOverlay()
window.location.reload()
},
onOfflineReady() {