diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs index d528286..99c22e5 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs @@ -8,6 +8,7 @@ namespace Sozsoft.Platform.Intranet; public interface IIntranetAppService : IApplicationService { Task GetIntranetDashboardAsync(); + Task> GetIntranetSocialPostsAsync(int skipCount, int maxResultCount); Task CreateSurveyResponseAsync(SubmitSurveyInput input); Task CreateSocialPostAsync(CreateSocialPostInput input); Task DeleteSocialPostAsync(System.Guid id); diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IntranetDashboardDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IntranetDashboardDto.cs index 4cd669d..07f714d 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IntranetDashboardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IntranetDashboardDto.cs @@ -11,5 +11,4 @@ public class IntranetDashboardDto public List Documents { get; set; } = []; public List Announcements { get; set; } = []; public List Surveys { get; set; } = []; - public List SocialPosts { get; set; } = []; } diff --git a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs index 50bdb7a..06e2034 100644 --- a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs @@ -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> GetSocialPostsAsync() + [UnitOfWork] + public async Task> 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>(socialPosts); diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index 3aa520a..b4d0161 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -538,7 +538,7 @@ public class ListFormWizardAppService( /// /// 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. /// private string ResolveWizardSeedOutputPath() { diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 0c1b25d..58ebe45 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -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", diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json index 949160f..9a72c6d 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json +++ b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json @@ -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;", diff --git a/ui/.env b/ui/.env index c37b558..7126d16 100644 --- a/ui/.env +++ b/ui/.env @@ -19,4 +19,4 @@ VITE_GOOGLE_MAPS_API_KEY='AIzaSyAefS2rvF-xwq7OHpZ27UYxXPbMo6OwACc' # Eğer Restrict key seçiliyse, izinli API listesine şunları ekle: # Maps JavaScript API -# Places API \ No newline at end of file +# Places API diff --git a/ui/src/services/intranet.service.ts b/ui/src/services/intranet.service.ts index 77ce538..93df248 100644 --- a/ui/src/services/intranet.service.ts +++ b/ui/src/services/intranet.service.ts @@ -24,6 +24,16 @@ export class IntranetService { { apiName: this.apiName, ...config }, ) + getIntranetSocialPosts = (skipCount: number, maxResultCount: number, config?: Partial) => + apiService.fetchData( + { + 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 }[], diff --git a/ui/src/views/intranet/Dashboard.tsx b/ui/src/views/intranet/Dashboard.tsx index eeb1c71..6357acb 100644 --- a/ui/src/views/intranet/Dashboard.tsx +++ b/ui/src/views/intranet/Dashboard.tsx @@ -262,7 +262,7 @@ const IntranetDashboard: React.FC = () => { ) case 'social-wall': - return + return default: return null } diff --git a/ui/src/views/intranet/SocialWall/index.tsx b/ui/src/views/intranet/SocialWall/index.tsx index 9e08407..daf6bd6 100644 --- a/ui/src/views/intranet/SocialWall/index.tsx +++ b/ui/src/views/intranet/SocialWall/index.tsx @@ -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(initialPosts) +const PAGE_SIZE = 10 +const SocialWall: React.FC = () => { + const [posts, setPosts] = useState([]) + const [skipCount, setSkipCount] = useState(0) + const [hasMore, setHasMore] = useState(true) + const [loadingMore, setLoadingMore] = useState(false) + const sentinelRef = useRef(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 )} + + {/* Infinite scroll sentinel */} + {filter === 'all' && ( + <> +
+ {loadingMore && ( +
+
+
+ )} + {!hasMore && posts.length > 0 && ( +

+ {translate('::App.Platform.Intranet.SocialWall.AllPostsLoaded') || 'Tüm gönderiler yüklendi'} +

+ )} + + )}
) } diff --git a/ui/src/views/version/swRegistration.ts b/ui/src/views/version/swRegistration.ts index 77ac567..fc0e644 100644 --- a/ui/src/views/version/swRegistration.ts +++ b/ui/src/views/version/swRegistration.ts @@ -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 = ` +
+
+

System Updating

+

Loading new version, please wait…
The page will refresh automatically.

+
+ ` + 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() {