update
@@ -6,14 +6,22 @@ import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import CaseStudyScrollProgressButton from "@/components/pages/case-study/CaseStudyScrollProgressButton";
|
||||
import CaseStudyInitAnimations from "@/components/pages/case-study/CaseStudyInitAnimations";
|
||||
|
||||
const page = () => {
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const page = async ({ params }: PageProps) => {
|
||||
const { slug } = await params;
|
||||
|
||||
return (
|
||||
<div className="tp-app">
|
||||
<Header />
|
||||
<main>
|
||||
<CaseSingle />
|
||||
<Process />
|
||||
<RelatedCase />
|
||||
<CaseSingle slug={slug} />
|
||||
<Process slug={slug} />
|
||||
<RelatedCase slug={slug} />
|
||||
</main>
|
||||
<Footer />
|
||||
<CaseStudyScrollProgressButton />
|
||||
|
||||
213
gnx-react/backend/blog/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Blog API Documentation
|
||||
|
||||
This document provides information about the Blog API implementation for the GNX Software Solutions website.
|
||||
|
||||
## Overview
|
||||
|
||||
The Blog API is a RESTful API built with Django REST Framework that provides endpoints for managing blog posts, categories, authors, tags, and comments.
|
||||
|
||||
## Backend Setup
|
||||
|
||||
### Models
|
||||
|
||||
The blog app includes the following models:
|
||||
|
||||
1. **BlogAuthor** - Stores information about blog post authors
|
||||
2. **BlogCategory** - Categories for organizing blog posts
|
||||
3. **BlogTag** - Tags for additional post categorization
|
||||
4. **BlogPost** - The main blog post model
|
||||
5. **BlogComment** - Comments on blog posts (with moderation support)
|
||||
|
||||
### API Endpoints
|
||||
|
||||
Base URL: `/api/blog/`
|
||||
|
||||
#### Blog Posts
|
||||
|
||||
- `GET /api/blog/posts/` - List all blog posts (with pagination and filtering)
|
||||
- Query parameters:
|
||||
- `category` - Filter by category slug
|
||||
- `tag` - Filter by tag slug
|
||||
- `author` - Filter by author ID
|
||||
- `search` - Search in title, content, and excerpt
|
||||
- `featured` - Filter featured posts
|
||||
- `ordering` - Sort by fields (e.g., `-published_at`, `views_count`)
|
||||
- `page` - Page number
|
||||
- `page_size` - Items per page (default: 9)
|
||||
|
||||
- `GET /api/blog/posts/{slug}/` - Get a single blog post by slug
|
||||
- `GET /api/blog/posts/featured/` - Get featured blog posts
|
||||
- `GET /api/blog/posts/latest/?limit=5` - Get latest blog posts
|
||||
- `GET /api/blog/posts/popular/?limit=5` - Get popular posts by views
|
||||
- `GET /api/blog/posts/{id}/related/` - Get related posts
|
||||
|
||||
#### Categories
|
||||
|
||||
- `GET /api/blog/categories/` - List all categories
|
||||
- `GET /api/blog/categories/{slug}/` - Get a single category by slug
|
||||
- `GET /api/blog/categories/with_posts/` - Get categories that have published posts
|
||||
|
||||
#### Authors
|
||||
|
||||
- `GET /api/blog/authors/` - List all authors
|
||||
- `GET /api/blog/authors/{id}/` - Get a single author
|
||||
- `GET /api/blog/authors/{id}/posts/` - Get all posts by an author
|
||||
|
||||
#### Tags
|
||||
|
||||
- `GET /api/blog/tags/` - List all tags
|
||||
- `GET /api/blog/tags/{slug}/` - Get a single tag by slug
|
||||
- `GET /api/blog/tags/{slug}/posts/` - Get all posts with a specific tag
|
||||
|
||||
#### Comments
|
||||
|
||||
- `GET /api/blog/comments/?post={post_id}` - Get comments for a post
|
||||
- `POST /api/blog/comments/` - Create a new comment (requires moderation)
|
||||
|
||||
### Management Commands
|
||||
|
||||
#### Populate Blog Data
|
||||
|
||||
To populate the database with sample blog data:
|
||||
|
||||
```bash
|
||||
python manage.py populate_blog
|
||||
```
|
||||
|
||||
This command creates:
|
||||
- 4 sample authors
|
||||
- 6 blog categories
|
||||
- 16 tags
|
||||
- 8 sample blog posts with full content
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### API Service
|
||||
|
||||
Location: `/lib/api/blogService.ts`
|
||||
|
||||
The service provides functions for:
|
||||
- Fetching blog posts (with filtering)
|
||||
- Getting single posts by slug
|
||||
- Fetching categories, tags, and authors
|
||||
- Getting featured, latest, and popular posts
|
||||
- Managing comments
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
Location: `/lib/hooks/useBlog.ts`
|
||||
|
||||
Available hooks:
|
||||
- `useBlogPosts(params)` - Fetch paginated list of posts
|
||||
- `useBlogPost(slug)` - Fetch a single post
|
||||
- `useFeaturedPosts()` - Fetch featured posts
|
||||
- `useLatestPosts(limit)` - Fetch latest posts
|
||||
- `usePopularPosts(limit)` - Fetch popular posts
|
||||
- `useBlogCategories()` - Fetch all categories
|
||||
- `useBlogTags()` - Fetch all tags
|
||||
- `useBlogAuthors()` - Fetch all authors
|
||||
|
||||
### Components
|
||||
|
||||
The following components have been updated to use the API:
|
||||
|
||||
1. **PostFilterButtons** - Fetches categories from API
|
||||
2. **PostFilterItems** - Fetches and filters blog posts by category
|
||||
3. **BlogSingle** - Fetches individual post by slug from URL params
|
||||
4. **LatestPost** - Fetches latest posts for the slider
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Fetching All Posts
|
||||
|
||||
```typescript
|
||||
const { posts, loading, error, pagination } = useBlogPosts({
|
||||
category: 'enterprise-software',
|
||||
page_size: 9
|
||||
});
|
||||
```
|
||||
|
||||
#### Fetching a Single Post
|
||||
|
||||
```typescript
|
||||
const { post, loading, error } = useBlogPost('api-first-approach-to-system-integration');
|
||||
```
|
||||
|
||||
#### Fetching Latest Posts
|
||||
|
||||
```typescript
|
||||
const { posts, loading, error } = useLatestPosts(5);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Backend Features
|
||||
|
||||
1. **Pagination** - All list endpoints support pagination
|
||||
2. **Filtering** - Filter posts by category, tag, author, and search
|
||||
3. **Ordering** - Sort posts by date, views, etc.
|
||||
4. **View Tracking** - Automatically increment view count when a post is viewed
|
||||
5. **Related Posts** - Automatically suggest related posts from the same category
|
||||
6. **Comment Moderation** - Comments require approval before being visible
|
||||
7. **SEO Support** - Meta descriptions and keywords for each post
|
||||
8. **Reading Time** - Estimated reading time for posts
|
||||
9. **Tags System** - Flexible tagging for better categorization
|
||||
|
||||
### Frontend Features
|
||||
|
||||
1. **Real-time Data** - All data fetched from API
|
||||
2. **Loading States** - Proper loading indicators
|
||||
3. **Error Handling** - Graceful error handling with fallbacks
|
||||
4. **Image Optimization** - Uses Next.js Image component
|
||||
5. **Responsive Design** - Mobile-friendly layouts
|
||||
6. **Category Filtering** - Filter posts by category with smooth animations
|
||||
7. **Social Sharing** - Share posts on social media
|
||||
8. **Related Posts** - Automatically shows related posts
|
||||
9. **SEO Friendly** - Proper meta tags and structured data
|
||||
|
||||
## Sample Data
|
||||
|
||||
The populated sample data includes:
|
||||
|
||||
### Categories
|
||||
- Enterprise Software
|
||||
- Digital Transformation
|
||||
- System Integration
|
||||
- Cloud Solutions
|
||||
- Security
|
||||
- API Development
|
||||
|
||||
### Sample Post Topics
|
||||
1. The Future of Enterprise Software Architecture
|
||||
2. Digital Transformation Strategies for Large Enterprises
|
||||
3. API-First Approach to System Integration
|
||||
4. Cloud Migration Best Practices for Enterprise
|
||||
5. Enterprise Security in the Digital Age
|
||||
6. Building Scalable API Architectures
|
||||
7. Microservices Architecture for Enterprise Applications
|
||||
8. Data Analytics and Business Intelligence Solutions
|
||||
|
||||
## Notes
|
||||
|
||||
- All blog posts support HTML content
|
||||
- Images can be uploaded or linked via URL
|
||||
- Posts can be marked as featured
|
||||
- Comments require moderation before being visible
|
||||
- The API respects published status (unpublished posts are not returned)
|
||||
- View counts are automatically tracked
|
||||
- Related posts are determined by category
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
1. Add full-text search using Elasticsearch
|
||||
2. Implement post series/collections
|
||||
3. Add newsletter subscription functionality
|
||||
4. Implement post scheduling
|
||||
5. Add analytics dashboard
|
||||
6. Support for multiple authors per post
|
||||
7. Rich text editor in admin
|
||||
8. Image upload and management
|
||||
9. Post translations/i18n support
|
||||
10. RSS feed generation
|
||||
|
||||
0
gnx-react/backend/blog/__init__.py
Normal file
BIN
gnx-react/backend/blog/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
gnx-react/backend/blog/__pycache__/admin.cpython-312.pyc
Normal file
BIN
gnx-react/backend/blog/__pycache__/apps.cpython-312.pyc
Normal file
BIN
gnx-react/backend/blog/__pycache__/models.cpython-312.pyc
Normal file
BIN
gnx-react/backend/blog/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
gnx-react/backend/blog/__pycache__/urls.cpython-312.pyc
Normal file
BIN
gnx-react/backend/blog/__pycache__/views.cpython-312.pyc
Normal file
98
gnx-react/backend/blog/admin.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from django.contrib import admin
|
||||
from .models import BlogPost, BlogCategory, BlogAuthor, BlogTag, BlogComment
|
||||
|
||||
|
||||
@admin.register(BlogAuthor)
|
||||
class BlogAuthorAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'email', 'is_active', 'created_at']
|
||||
list_filter = ['is_active', 'created_at']
|
||||
search_fields = ['name', 'email']
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
@admin.register(BlogCategory)
|
||||
class BlogCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['title', 'slug', 'display_order', 'is_active', 'posts_count']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['title', 'slug']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
ordering = ['display_order', 'title']
|
||||
|
||||
def posts_count(self, obj):
|
||||
return obj.posts.count()
|
||||
posts_count.short_description = 'Posts Count'
|
||||
|
||||
|
||||
@admin.register(BlogTag)
|
||||
class BlogTagAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'is_active', 'posts_count']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'slug']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ['name']
|
||||
|
||||
def posts_count(self, obj):
|
||||
return obj.posts.count()
|
||||
posts_count.short_description = 'Posts Count'
|
||||
|
||||
|
||||
@admin.register(BlogPost)
|
||||
class BlogPostAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'title', 'author', 'category', 'published',
|
||||
'featured', 'views_count', 'published_at'
|
||||
]
|
||||
list_filter = [
|
||||
'published', 'featured', 'category',
|
||||
'author', 'published_at', 'created_at'
|
||||
]
|
||||
search_fields = ['title', 'content', 'excerpt']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
filter_horizontal = ['tags']
|
||||
date_hierarchy = 'published_at'
|
||||
ordering = ['-published_at', '-created_at']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('title', 'slug', 'author', 'category')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('excerpt', 'content')
|
||||
}),
|
||||
('Images', {
|
||||
'fields': ('thumbnail', 'thumbnail_url', 'featured_image', 'featured_image_url')
|
||||
}),
|
||||
('Categorization', {
|
||||
'fields': ('tags',)
|
||||
}),
|
||||
('SEO', {
|
||||
'fields': ('meta_description', 'meta_keywords'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Status & Visibility', {
|
||||
'fields': ('published', 'featured', 'reading_time', 'published_at')
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('views_count',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['views_count']
|
||||
|
||||
|
||||
@admin.register(BlogComment)
|
||||
class BlogCommentAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'post', 'is_approved', 'created_at']
|
||||
list_filter = ['is_approved', 'created_at']
|
||||
search_fields = ['name', 'email', 'content', 'post__title']
|
||||
ordering = ['-created_at']
|
||||
actions = ['approve_comments', 'disapprove_comments']
|
||||
|
||||
def approve_comments(self, request, queryset):
|
||||
queryset.update(is_approved=True)
|
||||
approve_comments.short_description = "Approve selected comments"
|
||||
|
||||
def disapprove_comments(self, request, queryset):
|
||||
queryset.update(is_approved=False)
|
||||
disapprove_comments.short_description = "Disapprove selected comments"
|
||||
7
gnx-react/backend/blog/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog'
|
||||
verbose_name = 'Blog Management'
|
||||
0
gnx-react/backend/blog/management/__init__.py
Normal file
414
gnx-react/backend/blog/management/commands/populate_blog.py
Normal file
@@ -0,0 +1,414 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from blog.models import BlogAuthor, BlogCategory, BlogTag, BlogPost
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Populate database with sample blog data'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('Starting to populate blog data...'))
|
||||
|
||||
# Clear existing data
|
||||
BlogPost.objects.all().delete()
|
||||
BlogTag.objects.all().delete()
|
||||
BlogCategory.objects.all().delete()
|
||||
BlogAuthor.objects.all().delete()
|
||||
|
||||
# Create Authors
|
||||
authors_data = [
|
||||
{
|
||||
'name': 'Sarah Johnson',
|
||||
'email': 'sarah@gnxsoft.com',
|
||||
'bio': 'Senior Technology Consultant with 15+ years in enterprise solutions'
|
||||
},
|
||||
{
|
||||
'name': 'Michael Chen',
|
||||
'email': 'michael@gnxsoft.com',
|
||||
'bio': 'Cloud Architecture Specialist and DevOps Expert'
|
||||
},
|
||||
{
|
||||
'name': 'Emily Rodriguez',
|
||||
'email': 'emily@gnxsoft.com',
|
||||
'bio': 'API Integration and System Integration Lead'
|
||||
},
|
||||
{
|
||||
'name': 'David Thompson',
|
||||
'email': 'david@gnxsoft.com',
|
||||
'bio': 'Digital Transformation and Business Intelligence Consultant'
|
||||
}
|
||||
]
|
||||
|
||||
authors = {}
|
||||
for author_data in authors_data:
|
||||
author = BlogAuthor.objects.create(**author_data)
|
||||
authors[author.name] = author
|
||||
self.stdout.write(f'Created author: {author.name}')
|
||||
|
||||
# Create Categories
|
||||
categories_data = [
|
||||
{
|
||||
'title': 'Enterprise Software',
|
||||
'slug': 'enterprise-software',
|
||||
'description': 'Articles about enterprise software development and architecture',
|
||||
'display_order': 1
|
||||
},
|
||||
{
|
||||
'title': 'Digital Transformation',
|
||||
'slug': 'digital-transformation',
|
||||
'description': 'Insights on digital transformation strategies',
|
||||
'display_order': 2
|
||||
},
|
||||
{
|
||||
'title': 'System Integration',
|
||||
'slug': 'system-integration',
|
||||
'description': 'Best practices for system and API integration',
|
||||
'display_order': 3
|
||||
},
|
||||
{
|
||||
'title': 'Cloud Solutions',
|
||||
'slug': 'cloud-solutions',
|
||||
'description': 'Cloud computing and migration strategies',
|
||||
'display_order': 4
|
||||
},
|
||||
{
|
||||
'title': 'Security',
|
||||
'slug': 'security',
|
||||
'description': 'Enterprise security and cybersecurity topics',
|
||||
'display_order': 5
|
||||
},
|
||||
{
|
||||
'title': 'API Development',
|
||||
'slug': 'api-development',
|
||||
'description': 'API design, development, and management',
|
||||
'display_order': 6
|
||||
}
|
||||
]
|
||||
|
||||
categories = {}
|
||||
for cat_data in categories_data:
|
||||
category = BlogCategory.objects.create(**cat_data)
|
||||
categories[category.slug] = category
|
||||
self.stdout.write(f'Created category: {category.title}')
|
||||
|
||||
# Create Tags
|
||||
tags_data = [
|
||||
'API', 'REST', 'GraphQL', 'Microservices', 'Cloud', 'AWS', 'Azure',
|
||||
'Security', 'DevOps', 'CI/CD', 'Docker', 'Kubernetes',
|
||||
'Enterprise', 'Integration', 'Architecture', 'Best Practices'
|
||||
]
|
||||
|
||||
tags = {}
|
||||
for tag_name in tags_data:
|
||||
tag = BlogTag.objects.create(name=tag_name)
|
||||
tags[tag_name] = tag
|
||||
self.stdout.write(f'Created tag: {tag.name}')
|
||||
|
||||
# Create Blog Posts
|
||||
posts_data = [
|
||||
{
|
||||
'title': 'The Future of Enterprise Software Architecture',
|
||||
'content': '''
|
||||
<h2>Introduction</h2>
|
||||
<p>Enterprise software architecture is evolving rapidly. In this comprehensive guide, we explore the latest trends and best practices that are shaping the future of enterprise systems.</p>
|
||||
|
||||
<h3>Microservices and Modularity</h3>
|
||||
<p>The shift towards microservices architecture has revolutionized how we build enterprise applications. By breaking down monolithic applications into smaller, independent services, organizations can achieve greater flexibility, scalability, and maintainability.</p>
|
||||
|
||||
<h3>Cloud-Native Development</h3>
|
||||
<p>Cloud-native architecture is becoming the standard for modern enterprise applications. This approach leverages cloud computing capabilities to build and run scalable applications in dynamic environments.</p>
|
||||
|
||||
<h3>API-First Design</h3>
|
||||
<p>API-first development ensures that applications are built with integration in mind from the start. This approach facilitates better communication between services and makes it easier to integrate with third-party systems.</p>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
<ul>
|
||||
<li>Design for scalability from the ground up</li>
|
||||
<li>Implement robust security measures at every layer</li>
|
||||
<li>Use containerization for consistent deployment</li>
|
||||
<li>Adopt continuous integration and deployment practices</li>
|
||||
<li>Monitor and optimize performance continuously</li>
|
||||
</ul>
|
||||
|
||||
<h2>Conclusion</h2>
|
||||
<p>The future of enterprise software architecture lies in flexibility, scalability, and cloud-native approaches. Organizations that embrace these principles will be better positioned to adapt to changing business needs.</p>
|
||||
''',
|
||||
'excerpt': 'Exploring the latest trends in enterprise software architecture, from microservices to cloud-native development.',
|
||||
'author': authors['Sarah Johnson'],
|
||||
'category': categories['enterprise-software'],
|
||||
'tags': ['Enterprise', 'Architecture', 'Microservices', 'Cloud'],
|
||||
'thumbnail_url': '/images/blog/one.png',
|
||||
'featured': True,
|
||||
'reading_time': 8,
|
||||
'days_ago': 5
|
||||
},
|
||||
{
|
||||
'title': 'Digital Transformation Strategies for Large Enterprises',
|
||||
'content': '''
|
||||
<h2>Understanding Digital Transformation</h2>
|
||||
<p>Digital transformation is more than just adopting new technologies—it's about fundamentally changing how your organization operates and delivers value to customers.</p>
|
||||
|
||||
<h3>Key Components of Successful Transformation</h3>
|
||||
<p>A successful digital transformation strategy must address technology, processes, and culture simultaneously. Organizations that focus on only one aspect often struggle to achieve their goals.</p>
|
||||
|
||||
<h3>Technology Stack Modernization</h3>
|
||||
<p>Legacy systems can hold organizations back. Modernizing your technology stack is often the first step in digital transformation, enabling new capabilities and improving efficiency.</p>
|
||||
|
||||
<h2>Implementation Strategy</h2>
|
||||
<ol>
|
||||
<li>Assess current state and define vision</li>
|
||||
<li>Identify quick wins and long-term goals</li>
|
||||
<li>Build cross-functional transformation teams</li>
|
||||
<li>Implement in phases with measurable milestones</li>
|
||||
<li>Continuously gather feedback and adjust</li>
|
||||
</ol>
|
||||
|
||||
<h2>Overcoming Challenges</h2>
|
||||
<p>Common challenges include resistance to change, budget constraints, and technical debt. Success requires strong leadership commitment and clear communication throughout the organization.</p>
|
||||
''',
|
||||
'excerpt': 'A comprehensive guide to digital transformation strategies tailored for large enterprise organizations.',
|
||||
'author': authors['David Thompson'],
|
||||
'category': categories['digital-transformation'],
|
||||
'tags': ['Digital Transformation', 'Enterprise', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/two.png',
|
||||
'featured': True,
|
||||
'reading_time': 10,
|
||||
'days_ago': 8
|
||||
},
|
||||
{
|
||||
'title': 'API-First Approach to System Integration',
|
||||
'content': '''
|
||||
<h2>What is API-First Development?</h2>
|
||||
<p>API-first development is a strategy where APIs are treated as first-class citizens in the development process. Instead of being an afterthought, APIs are designed and built before any other code.</p>
|
||||
|
||||
<h3>Benefits of API-First Approach</h3>
|
||||
<p>This approach offers numerous advantages including better developer experience, faster time to market, and improved system integration capabilities.</p>
|
||||
|
||||
<h3>RESTful API Design Principles</h3>
|
||||
<p>RESTful APIs follow specific architectural principles that make them scalable, maintainable, and easy to understand. Key principles include statelessness, resource-based URLs, and standard HTTP methods.</p>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
<ul>
|
||||
<li>Design APIs with clear and consistent naming conventions</li>
|
||||
<li>Version your APIs from the start</li>
|
||||
<li>Implement proper authentication and authorization</li>
|
||||
<li>Provide comprehensive documentation</li>
|
||||
<li>Use appropriate HTTP status codes</li>
|
||||
<li>Implement rate limiting and throttling</li>
|
||||
</ul>
|
||||
|
||||
<h3>GraphQL vs REST</h3>
|
||||
<p>While REST has been the standard for many years, GraphQL offers an alternative approach that can be more efficient for certain use cases. Understanding when to use each is crucial for effective API design.</p>
|
||||
''',
|
||||
'excerpt': 'Learn how an API-first approach can streamline system integration and improve your software architecture.',
|
||||
'author': authors['Emily Rodriguez'],
|
||||
'category': categories['system-integration'],
|
||||
'tags': ['API', 'REST', 'GraphQL', 'Integration'],
|
||||
'thumbnail_url': '/images/blog/three.png',
|
||||
'reading_time': 7,
|
||||
'days_ago': 12
|
||||
},
|
||||
{
|
||||
'title': 'Cloud Migration Best Practices for Enterprise',
|
||||
'content': '''
|
||||
<h2>Planning Your Cloud Migration</h2>
|
||||
<p>Cloud migration is a complex process that requires careful planning and execution. A well-thought-out strategy can mean the difference between success and costly setbacks.</p>
|
||||
|
||||
<h3>Assessing Your Current Infrastructure</h3>
|
||||
<p>Before migrating to the cloud, it's essential to thoroughly understand your current infrastructure, dependencies, and workload requirements.</p>
|
||||
|
||||
<h3>Choosing the Right Cloud Provider</h3>
|
||||
<p>AWS, Azure, and Google Cloud each offer unique strengths. The right choice depends on your specific requirements, existing technology stack, and business objectives.</p>
|
||||
|
||||
<h2>Migration Strategies</h2>
|
||||
<ol>
|
||||
<li><strong>Lift and Shift:</strong> Move applications as-is to the cloud</li>
|
||||
<li><strong>Replatform:</strong> Make minor optimizations during migration</li>
|
||||
<li><strong>Refactor:</strong> Redesign applications to be cloud-native</li>
|
||||
<li><strong>Rebuild:</strong> Completely rewrite applications for the cloud</li>
|
||||
</ol>
|
||||
|
||||
<h3>Security Considerations</h3>
|
||||
<p>Security should be a top priority during cloud migration. Implement encryption, access controls, and monitoring from day one.</p>
|
||||
|
||||
<h2>Post-Migration Optimization</h2>
|
||||
<p>Migration is just the beginning. Continuous optimization of costs, performance, and security is essential for maximizing cloud benefits.</p>
|
||||
''',
|
||||
'excerpt': 'Essential best practices and strategies for successfully migrating enterprise applications to the cloud.',
|
||||
'author': authors['Michael Chen'],
|
||||
'category': categories['cloud-solutions'],
|
||||
'tags': ['Cloud', 'AWS', 'Azure', 'Migration'],
|
||||
'thumbnail_url': '/images/blog/four.png',
|
||||
'featured': False,
|
||||
'reading_time': 9,
|
||||
'days_ago': 15
|
||||
},
|
||||
{
|
||||
'title': 'Enterprise Security in the Digital Age',
|
||||
'content': '''
|
||||
<h2>The Evolving Security Landscape</h2>
|
||||
<p>As enterprises embrace digital transformation, security challenges have become more complex. Organizations must adopt a comprehensive approach to protect their assets and data.</p>
|
||||
|
||||
<h3>Zero Trust Architecture</h3>
|
||||
<p>Zero Trust is a security model that assumes no user or system should be trusted by default, even if they're inside the network perimeter.</p>
|
||||
|
||||
<h3>Multi-Factor Authentication</h3>
|
||||
<p>MFA is no longer optional for enterprise security. Implementing strong authentication mechanisms is critical for protecting sensitive data and systems.</p>
|
||||
|
||||
<h2>Common Security Threats</h2>
|
||||
<ul>
|
||||
<li>Phishing and social engineering attacks</li>
|
||||
<li>Ransomware and malware</li>
|
||||
<li>Insider threats</li>
|
||||
<li>DDoS attacks</li>
|
||||
<li>API vulnerabilities</li>
|
||||
</ul>
|
||||
|
||||
<h3>Implementing a Security-First Culture</h3>
|
||||
<p>Technology alone cannot protect an organization. Security awareness training and a culture of security consciousness are equally important.</p>
|
||||
|
||||
<h2>Compliance and Regulations</h2>
|
||||
<p>Understanding and adhering to regulations like GDPR, HIPAA, and SOC 2 is essential for enterprise organizations operating in regulated industries.</p>
|
||||
''',
|
||||
'excerpt': 'Understanding modern security challenges and implementing robust security measures for enterprise environments.',
|
||||
'author': authors['Sarah Johnson'],
|
||||
'category': categories['security'],
|
||||
'tags': ['Security', 'Enterprise', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/five.png',
|
||||
'reading_time': 6,
|
||||
'days_ago': 18
|
||||
},
|
||||
{
|
||||
'title': 'Building Scalable API Architectures',
|
||||
'content': '''
|
||||
<h2>Scalability Fundamentals</h2>
|
||||
<p>Building APIs that can scale to millions of requests requires careful architectural planning and implementation of proven patterns.</p>
|
||||
|
||||
<h3>Horizontal vs Vertical Scaling</h3>
|
||||
<p>Understanding the difference between horizontal and vertical scaling is crucial for building scalable systems. Most modern APIs benefit from horizontal scaling strategies.</p>
|
||||
|
||||
<h3>Caching Strategies</h3>
|
||||
<p>Implementing effective caching can dramatically improve API performance and reduce load on backend systems. Learn about different caching layers and when to use them.</p>
|
||||
|
||||
<h2>Performance Optimization</h2>
|
||||
<ul>
|
||||
<li>Database query optimization</li>
|
||||
<li>Connection pooling</li>
|
||||
<li>Asynchronous processing</li>
|
||||
<li>Load balancing</li>
|
||||
<li>CDN integration</li>
|
||||
</ul>
|
||||
|
||||
<h3>Monitoring and Observability</h3>
|
||||
<p>You can't optimize what you can't measure. Implementing comprehensive monitoring and logging is essential for maintaining scalable APIs.</p>
|
||||
|
||||
<h2>Rate Limiting and Throttling</h2>
|
||||
<p>Protecting your API from abuse and ensuring fair usage requires implementing rate limiting and throttling mechanisms.</p>
|
||||
''',
|
||||
'excerpt': 'Learn the principles and patterns for building API architectures that can scale to meet growing demands.',
|
||||
'author': authors['Emily Rodriguez'],
|
||||
'category': categories['api-development'],
|
||||
'tags': ['API', 'Architecture', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/six.png',
|
||||
'reading_time': 8,
|
||||
'days_ago': 22
|
||||
},
|
||||
{
|
||||
'title': 'Microservices Architecture for Enterprise Applications',
|
||||
'content': '''
|
||||
<h2>Understanding Microservices</h2>
|
||||
<p>Microservices architecture has become the de facto standard for building scalable, maintainable enterprise applications. This architectural style structures an application as a collection of loosely coupled services.</p>
|
||||
|
||||
<h3>Key Characteristics</h3>
|
||||
<p>Microservices are independently deployable, organized around business capabilities, and can be written in different programming languages.</p>
|
||||
|
||||
<h3>Service Communication Patterns</h3>
|
||||
<p>Understanding different communication patterns between microservices is crucial. Options include synchronous REST APIs, message queues, and event-driven architectures.</p>
|
||||
|
||||
<h2>Design Patterns</h2>
|
||||
<ol>
|
||||
<li>API Gateway Pattern</li>
|
||||
<li>Service Discovery</li>
|
||||
<li>Circuit Breaker</li>
|
||||
<li>Event Sourcing</li>
|
||||
<li>CQRS (Command Query Responsibility Segregation)</li>
|
||||
</ol>
|
||||
|
||||
<h3>Containerization with Docker</h3>
|
||||
<p>Docker containers provide the perfect deployment vehicle for microservices, ensuring consistency across development and production environments.</p>
|
||||
|
||||
<h2>Challenges and Solutions</h2>
|
||||
<p>While microservices offer many benefits, they also introduce complexity in areas like distributed transactions, data consistency, and service orchestration.</p>
|
||||
''',
|
||||
'excerpt': 'A deep dive into microservices architecture patterns and best practices for enterprise applications.',
|
||||
'author': authors['Michael Chen'],
|
||||
'category': categories['enterprise-software'],
|
||||
'tags': ['Microservices', 'Architecture', 'Docker', 'Kubernetes'],
|
||||
'thumbnail_url': '/images/blog/seven.png',
|
||||
'featured': True,
|
||||
'reading_time': 11,
|
||||
'days_ago': 25
|
||||
},
|
||||
{
|
||||
'title': 'Data Analytics and Business Intelligence Solutions',
|
||||
'content': '''
|
||||
<h2>The Power of Data-Driven Decisions</h2>
|
||||
<p>In today's business environment, data analytics and business intelligence are no longer optional—they're essential for staying competitive.</p>
|
||||
|
||||
<h3>Modern BI Tools and Platforms</h3>
|
||||
<p>Modern BI platforms offer self-service analytics, real-time dashboards, and AI-powered insights that democratize data access across the organization.</p>
|
||||
|
||||
<h3>Building a Data Warehouse</h3>
|
||||
<p>A well-designed data warehouse serves as the foundation for business intelligence initiatives, providing a single source of truth for organizational data.</p>
|
||||
|
||||
<h2>Analytics Maturity Model</h2>
|
||||
<ol>
|
||||
<li>Descriptive Analytics - What happened?</li>
|
||||
<li>Diagnostic Analytics - Why did it happen?</li>
|
||||
<li>Predictive Analytics - What will happen?</li>
|
||||
<li>Prescriptive Analytics - What should we do?</li>
|
||||
</ol>
|
||||
|
||||
<h3>Real-Time Analytics</h3>
|
||||
<p>The ability to analyze data in real-time enables organizations to respond quickly to changing conditions and make timely decisions.</p>
|
||||
|
||||
<h2>Data Governance and Quality</h2>
|
||||
<p>Without proper data governance and quality management, analytics initiatives can produce unreliable results. Establishing data quality standards is crucial.</p>
|
||||
|
||||
<h3>Machine Learning Integration</h3>
|
||||
<p>Integrating machine learning models into BI platforms can provide deeper insights and enable predictive capabilities.</p>
|
||||
''',
|
||||
'excerpt': 'Leveraging data analytics and business intelligence to drive informed decision-making in enterprise environments.',
|
||||
'author': authors['David Thompson'],
|
||||
'category': categories['digital-transformation'],
|
||||
'tags': ['Analytics', 'BI', 'Enterprise', 'Best Practices'],
|
||||
'thumbnail_url': '/images/blog/eight.png',
|
||||
'reading_time': 9,
|
||||
'days_ago': 30
|
||||
}
|
||||
]
|
||||
|
||||
# Create posts
|
||||
for post_data in posts_data:
|
||||
tag_names = post_data.pop('tags')
|
||||
days_ago = post_data.pop('days_ago')
|
||||
|
||||
post = BlogPost.objects.create(
|
||||
**post_data,
|
||||
published_at=timezone.now() - timedelta(days=days_ago)
|
||||
)
|
||||
|
||||
# Add tags
|
||||
for tag_name in tag_names:
|
||||
if tag_name in tags:
|
||||
post.tags.add(tags[tag_name])
|
||||
|
||||
self.stdout.write(f'Created post: {post.title}')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\nSuccessfully populated blog data!'))
|
||||
self.stdout.write(f'Created {BlogAuthor.objects.count()} authors')
|
||||
self.stdout.write(f'Created {BlogCategory.objects.count()} categories')
|
||||
self.stdout.write(f'Created {BlogTag.objects.count()} tags')
|
||||
self.stdout.write(f'Created {BlogPost.objects.count()} blog posts')
|
||||
|
||||
129
gnx-react/backend/blog/migrations/0001_initial.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Generated by Django 4.2.7 on 2025-10-08 09:41
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogAuthor',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, unique=True)),
|
||||
('bio', models.TextField(blank=True)),
|
||||
('avatar', models.ImageField(blank=True, null=True, upload_to='blog/authors/')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Author',
|
||||
'verbose_name_plural': 'Blog Authors',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('display_order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Category',
|
||||
'verbose_name_plural': 'Blog Categories',
|
||||
'ordering': ['display_order', 'title'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Tag',
|
||||
'verbose_name_plural': 'Blog Tags',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=300)),
|
||||
('slug', models.SlugField(max_length=300, unique=True)),
|
||||
('content', models.TextField()),
|
||||
('excerpt', models.TextField(blank=True, help_text='Short excerpt for preview')),
|
||||
('thumbnail', models.ImageField(blank=True, null=True, upload_to='blog/thumbnails/')),
|
||||
('thumbnail_url', models.CharField(blank=True, help_text='External thumbnail URL', max_length=500)),
|
||||
('featured_image', models.ImageField(blank=True, null=True, upload_to='blog/featured/')),
|
||||
('featured_image_url', models.CharField(blank=True, help_text='External featured image URL', max_length=500)),
|
||||
('meta_description', models.CharField(blank=True, max_length=160)),
|
||||
('meta_keywords', models.CharField(blank=True, max_length=255)),
|
||||
('published', models.BooleanField(default=True)),
|
||||
('featured', models.BooleanField(default=False, help_text='Featured post')),
|
||||
('views_count', models.PositiveIntegerField(default=0)),
|
||||
('reading_time', models.PositiveIntegerField(default=5, help_text='Estimated reading time in minutes')),
|
||||
('published_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='posts', to='blog.blogauthor')),
|
||||
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='posts', to='blog.blogcategory')),
|
||||
('tags', models.ManyToManyField(blank=True, related_name='posts', to='blog.blogtag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Post',
|
||||
'verbose_name_plural': 'Blog Posts',
|
||||
'ordering': ['-published_at', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogComment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('content', models.TextField()),
|
||||
('is_approved', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='blog.blogcomment')),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='blog.blogpost')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Blog Comment',
|
||||
'verbose_name_plural': 'Blog Comments',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='blogpost',
|
||||
index=models.Index(fields=['-published_at'], name='blog_blogpo_publish_e75c11_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='blogpost',
|
||||
index=models.Index(fields=['slug'], name='blog_blogpo_slug_361555_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='blogpost',
|
||||
index=models.Index(fields=['published'], name='blog_blogpo_publish_059755_idx'),
|
||||
),
|
||||
]
|
||||
0
gnx-react/backend/blog/migrations/__init__.py
Normal file
178
gnx-react/backend/blog/models.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
class BlogAuthor(models.Model):
|
||||
"""Model for blog post authors"""
|
||||
name = models.CharField(max_length=200)
|
||||
email = models.EmailField(unique=True, blank=True, null=True)
|
||||
bio = models.TextField(blank=True)
|
||||
avatar = models.ImageField(upload_to='blog/authors/', blank=True, null=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Author"
|
||||
verbose_name_plural = "Blog Authors"
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class BlogCategory(models.Model):
|
||||
"""Model for blog post categories"""
|
||||
title = models.CharField(max_length=100)
|
||||
slug = models.SlugField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Category"
|
||||
verbose_name_plural = "Blog Categories"
|
||||
ordering = ['display_order', 'title']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class BlogTag(models.Model):
|
||||
"""Model for blog post tags"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(max_length=50, unique=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Tag"
|
||||
verbose_name_plural = "Blog Tags"
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class BlogPost(models.Model):
|
||||
"""Model for blog posts"""
|
||||
title = models.CharField(max_length=300)
|
||||
slug = models.SlugField(max_length=300, unique=True)
|
||||
content = models.TextField()
|
||||
excerpt = models.TextField(blank=True, help_text="Short excerpt for preview")
|
||||
thumbnail = models.ImageField(upload_to='blog/thumbnails/', blank=True, null=True)
|
||||
thumbnail_url = models.CharField(max_length=500, blank=True, help_text="External thumbnail URL")
|
||||
featured_image = models.ImageField(upload_to='blog/featured/', blank=True, null=True)
|
||||
featured_image_url = models.CharField(max_length=500, blank=True, help_text="External featured image URL")
|
||||
|
||||
author = models.ForeignKey(
|
||||
BlogAuthor,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='posts'
|
||||
)
|
||||
category = models.ForeignKey(
|
||||
BlogCategory,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='posts'
|
||||
)
|
||||
tags = models.ManyToManyField(BlogTag, related_name='posts', blank=True)
|
||||
|
||||
# SEO and metadata
|
||||
meta_description = models.CharField(max_length=160, blank=True)
|
||||
meta_keywords = models.CharField(max_length=255, blank=True)
|
||||
|
||||
# Status and visibility
|
||||
published = models.BooleanField(default=True)
|
||||
featured = models.BooleanField(default=False, help_text="Featured post")
|
||||
views_count = models.PositiveIntegerField(default=0)
|
||||
reading_time = models.PositiveIntegerField(default=5, help_text="Estimated reading time in minutes")
|
||||
|
||||
# Timestamps
|
||||
published_at = models.DateTimeField(default=timezone.now)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Post"
|
||||
verbose_name_plural = "Blog Posts"
|
||||
ordering = ['-published_at', '-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['-published_at']),
|
||||
models.Index(fields=['slug']),
|
||||
models.Index(fields=['published']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)
|
||||
if not self.excerpt and self.content:
|
||||
# Generate excerpt from content (first 200 characters)
|
||||
self.excerpt = self.content[:200] + '...' if len(self.content) > 200 else self.content
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def get_thumbnail_url(self):
|
||||
"""Return the thumbnail URL (uploaded image or external URL)"""
|
||||
if self.thumbnail and hasattr(self.thumbnail, 'url'):
|
||||
return self.thumbnail.url
|
||||
elif self.thumbnail_url:
|
||||
return self.thumbnail_url
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_featured_image_url(self):
|
||||
"""Return the featured image URL (uploaded image or external URL)"""
|
||||
if self.featured_image and hasattr(self.featured_image, 'url'):
|
||||
return self.featured_image.url
|
||||
elif self.featured_image_url:
|
||||
return self.featured_image_url
|
||||
return None
|
||||
|
||||
def increment_views(self):
|
||||
"""Increment the view count"""
|
||||
self.views_count += 1
|
||||
self.save(update_fields=['views_count'])
|
||||
|
||||
|
||||
class BlogComment(models.Model):
|
||||
"""Model for blog post comments"""
|
||||
post = models.ForeignKey(BlogPost, on_delete=models.CASCADE, related_name='comments')
|
||||
name = models.CharField(max_length=100)
|
||||
email = models.EmailField()
|
||||
content = models.TextField()
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='replies'
|
||||
)
|
||||
is_approved = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Blog Comment"
|
||||
verbose_name_plural = "Blog Comments"
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"Comment by {self.name} on {self.post.title}"
|
||||
113
gnx-react/backend/blog/serializers.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from rest_framework import serializers
|
||||
from .models import BlogPost, BlogCategory, BlogAuthor, BlogTag, BlogComment
|
||||
|
||||
|
||||
class BlogAuthorSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog authors"""
|
||||
class Meta:
|
||||
model = BlogAuthor
|
||||
fields = ['id', 'name', 'email', 'bio', 'avatar']
|
||||
|
||||
|
||||
class BlogCategorySerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog categories"""
|
||||
posts_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = BlogCategory
|
||||
fields = ['id', 'title', 'slug', 'description', 'display_order', 'posts_count']
|
||||
|
||||
def get_posts_count(self, obj):
|
||||
return obj.posts.filter(published=True).count()
|
||||
|
||||
|
||||
class BlogTagSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog tags"""
|
||||
class Meta:
|
||||
model = BlogTag
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class BlogPostListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog post list view"""
|
||||
author_name = serializers.CharField(source='author.name', read_only=True)
|
||||
category_title = serializers.CharField(source='category.title', read_only=True)
|
||||
category_slug = serializers.CharField(source='category.slug', read_only=True)
|
||||
thumbnail = serializers.SerializerMethodField()
|
||||
tags = BlogTagSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'excerpt', 'thumbnail',
|
||||
'author_name', 'category_title', 'category_slug',
|
||||
'tags', 'published_at', 'created_at', 'updated_at',
|
||||
'views_count', 'reading_time', 'featured', 'published'
|
||||
]
|
||||
|
||||
def get_thumbnail(self, obj):
|
||||
return obj.get_thumbnail_url
|
||||
|
||||
|
||||
class BlogPostDetailSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog post detail view"""
|
||||
author = BlogAuthorSerializer(read_only=True)
|
||||
category = BlogCategorySerializer(read_only=True)
|
||||
tags = BlogTagSerializer(many=True, read_only=True)
|
||||
thumbnail = serializers.SerializerMethodField()
|
||||
featured_image = serializers.SerializerMethodField()
|
||||
related_posts = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'content', 'excerpt',
|
||||
'thumbnail', 'featured_image', 'author', 'category', 'tags',
|
||||
'meta_description', 'meta_keywords',
|
||||
'published', 'featured', 'views_count', 'reading_time',
|
||||
'published_at', 'created_at', 'updated_at', 'related_posts'
|
||||
]
|
||||
|
||||
def get_thumbnail(self, obj):
|
||||
return obj.get_thumbnail_url
|
||||
|
||||
def get_featured_image(self, obj):
|
||||
return obj.get_featured_image_url
|
||||
|
||||
def get_related_posts(self, obj):
|
||||
"""Get related posts from the same category"""
|
||||
related = BlogPost.objects.filter(
|
||||
category=obj.category,
|
||||
published=True
|
||||
).exclude(id=obj.id)[:3]
|
||||
return BlogPostListSerializer(related, many=True, context=self.context).data
|
||||
|
||||
|
||||
class BlogCommentSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for blog comments"""
|
||||
replies = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = BlogComment
|
||||
fields = [
|
||||
'id', 'post', 'name', 'email', 'content',
|
||||
'parent', 'is_approved', 'created_at', 'updated_at', 'replies'
|
||||
]
|
||||
read_only_fields = ['is_approved', 'created_at', 'updated_at']
|
||||
|
||||
def get_replies(self, obj):
|
||||
"""Get nested replies"""
|
||||
if obj.replies.exists():
|
||||
return BlogCommentSerializer(
|
||||
obj.replies.filter(is_approved=True),
|
||||
many=True
|
||||
).data
|
||||
return []
|
||||
|
||||
|
||||
class BlogCommentCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for creating blog comments"""
|
||||
class Meta:
|
||||
model = BlogComment
|
||||
fields = ['post', 'name', 'email', 'content', 'parent']
|
||||
|
||||
3
gnx-react/backend/blog/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
21
gnx-react/backend/blog/urls.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
BlogPostViewSet,
|
||||
BlogCategoryViewSet,
|
||||
BlogAuthorViewSet,
|
||||
BlogTagViewSet,
|
||||
BlogCommentViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'posts', BlogPostViewSet, basename='blog-post')
|
||||
router.register(r'categories', BlogCategoryViewSet, basename='blog-category')
|
||||
router.register(r'authors', BlogAuthorViewSet, basename='blog-author')
|
||||
router.register(r'tags', BlogTagViewSet, basename='blog-tag')
|
||||
router.register(r'comments', BlogCommentViewSet, basename='blog-comment')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
189
gnx-react/backend/blog/views.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from rest_framework import viewsets, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Q
|
||||
from .models import BlogPost, BlogCategory, BlogAuthor, BlogTag, BlogComment
|
||||
from .serializers import (
|
||||
BlogPostListSerializer,
|
||||
BlogPostDetailSerializer,
|
||||
BlogCategorySerializer,
|
||||
BlogAuthorSerializer,
|
||||
BlogTagSerializer,
|
||||
BlogCommentSerializer,
|
||||
BlogCommentCreateSerializer
|
||||
)
|
||||
|
||||
|
||||
class BlogPagination(PageNumberPagination):
|
||||
"""Custom pagination for blog posts"""
|
||||
page_size = 9
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for blog posts
|
||||
Supports filtering by category, tags, author, and search
|
||||
"""
|
||||
queryset = BlogPost.objects.filter(published=True)
|
||||
pagination_class = BlogPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['category__slug', 'author', 'featured', 'tags__slug']
|
||||
search_fields = ['title', 'content', 'excerpt']
|
||||
ordering_fields = ['published_at', 'views_count', 'created_at']
|
||||
ordering = ['-published_at']
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return BlogPostDetailSerializer
|
||||
return BlogPostListSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Filter by category
|
||||
category_slug = self.request.query_params.get('category', None)
|
||||
if category_slug and category_slug != 'all':
|
||||
queryset = queryset.filter(category__slug=category_slug)
|
||||
|
||||
# Filter by tag
|
||||
tag_slug = self.request.query_params.get('tag', None)
|
||||
if tag_slug:
|
||||
queryset = queryset.filter(tags__slug=tag_slug)
|
||||
|
||||
# Filter by author
|
||||
author_id = self.request.query_params.get('author', None)
|
||||
if author_id:
|
||||
queryset = queryset.filter(author_id=author_id)
|
||||
|
||||
# Search query
|
||||
search = self.request.query_params.get('search', None)
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(title__icontains=search) |
|
||||
Q(content__icontains=search) |
|
||||
Q(excerpt__icontains=search)
|
||||
)
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Override retrieve to increment view count"""
|
||||
instance = self.get_object()
|
||||
instance.increment_views()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def featured(self, request):
|
||||
"""Get featured blog posts"""
|
||||
featured_posts = self.get_queryset().filter(featured=True)[:6]
|
||||
serializer = self.get_serializer(featured_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def latest(self, request):
|
||||
"""Get latest blog posts"""
|
||||
limit = int(request.query_params.get('limit', 5))
|
||||
latest_posts = self.get_queryset()[:limit]
|
||||
serializer = self.get_serializer(latest_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def popular(self, request):
|
||||
"""Get popular blog posts by views"""
|
||||
limit = int(request.query_params.get('limit', 5))
|
||||
popular_posts = self.get_queryset().order_by('-views_count')[:limit]
|
||||
serializer = self.get_serializer(popular_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def related(self, request, slug=None):
|
||||
"""Get related posts for a specific post"""
|
||||
post = self.get_object()
|
||||
related_posts = self.get_queryset().filter(
|
||||
category=post.category
|
||||
).exclude(id=post.id)[:4]
|
||||
serializer = self.get_serializer(related_posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for blog categories"""
|
||||
queryset = BlogCategory.objects.filter(is_active=True)
|
||||
serializer_class = BlogCategorySerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def with_posts(self, request):
|
||||
"""Get categories that have published posts"""
|
||||
categories = self.get_queryset().filter(posts__published=True).distinct()
|
||||
serializer = self.get_serializer(categories, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogAuthorViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for blog authors"""
|
||||
queryset = BlogAuthor.objects.filter(is_active=True)
|
||||
serializer_class = BlogAuthorSerializer
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def posts(self, request, pk=None):
|
||||
"""Get all posts by a specific author"""
|
||||
author = self.get_object()
|
||||
posts = BlogPost.objects.filter(author=author, published=True)
|
||||
page = self.paginate_queryset(posts)
|
||||
if page is not None:
|
||||
serializer = BlogPostListSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = BlogPostListSerializer(posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogTagViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for blog tags"""
|
||||
queryset = BlogTag.objects.filter(is_active=True)
|
||||
serializer_class = BlogTagSerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def posts(self, request, slug=None):
|
||||
"""Get all posts with a specific tag"""
|
||||
tag = self.get_object()
|
||||
posts = BlogPost.objects.filter(tags=tag, published=True)
|
||||
page = self.paginate_queryset(posts)
|
||||
if page is not None:
|
||||
serializer = BlogPostListSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = BlogPostListSerializer(posts, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BlogCommentViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for blog comments"""
|
||||
queryset = BlogComment.objects.filter(is_approved=True)
|
||||
serializer_class = BlogCommentSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['post']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return BlogCommentCreateSerializer
|
||||
return BlogCommentSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Create a new comment (requires moderation)"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
return Response(
|
||||
{
|
||||
'message': 'Comment submitted successfully. It will be visible after moderation.',
|
||||
'data': serializer.data
|
||||
},
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
208
gnx-react/backend/case_studies/README.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Case Studies API
|
||||
|
||||
## Overview
|
||||
|
||||
The Case Studies API provides a comprehensive backend and frontend solution for managing and displaying case study content. This feature includes categories, clients, process steps, gallery images, and related case studies.
|
||||
|
||||
## Backend Structure
|
||||
|
||||
### Models
|
||||
|
||||
1. **CaseStudyCategory**
|
||||
- Category management for case studies
|
||||
- Fields: name, slug, description, display_order, is_active
|
||||
|
||||
2. **Client**
|
||||
- Client information management
|
||||
- Fields: name, slug, logo, description, website
|
||||
|
||||
3. **CaseStudy**
|
||||
- Main case study model
|
||||
- Fields: title, slug, subtitle, description, excerpt
|
||||
- Images: thumbnail, featured_image, poster_image, project_image
|
||||
- Relations: category, client
|
||||
- Content: project_overview, site_map_content
|
||||
|
||||
4. **CaseStudyImage**
|
||||
- Gallery images for case studies
|
||||
- Fields: image, caption, display_order
|
||||
|
||||
5. **CaseStudyProcess**
|
||||
- Process steps for case studies
|
||||
- Fields: step_number, title, description
|
||||
|
||||
### API Endpoints
|
||||
|
||||
Base URL: `/api/case-studies/`
|
||||
|
||||
#### Case Studies
|
||||
|
||||
- `GET /case-studies/` - List all case studies (with pagination and filtering)
|
||||
- Query params: `category`, `client`, `search`, `featured`, `ordering`, `page`, `page_size`
|
||||
- `GET /case-studies/{slug}/` - Get case study details
|
||||
- `GET /case-studies/featured/` - Get featured case studies
|
||||
- `GET /case-studies/latest/?limit=6` - Get latest case studies
|
||||
- `GET /case-studies/popular/?limit=6` - Get popular case studies
|
||||
- `GET /case-studies/{slug}/related/` - Get related case studies
|
||||
|
||||
#### Categories
|
||||
|
||||
- `GET /categories/` - List all categories
|
||||
- `GET /categories/{slug}/` - Get category details
|
||||
- `GET /categories/with_case_studies/` - Get categories with case studies
|
||||
|
||||
#### Clients
|
||||
|
||||
- `GET /clients/` - List all clients
|
||||
- `GET /clients/{slug}/` - Get client details
|
||||
- `GET /clients/{slug}/case_studies/` - Get case studies for a client
|
||||
|
||||
## Frontend Structure
|
||||
|
||||
### API Service (`lib/api/caseStudyService.ts`)
|
||||
|
||||
Provides TypeScript functions for all API endpoints with proper typing.
|
||||
|
||||
### Hooks (`lib/hooks/useCaseStudy.ts`)
|
||||
|
||||
React hooks for data fetching:
|
||||
- `useCaseStudies()` - Fetch all case studies
|
||||
- `useCaseStudy(slug)` - Fetch single case study
|
||||
- `useFeaturedCaseStudies()` - Fetch featured case studies
|
||||
- `useLatestCaseStudies()` - Fetch latest case studies
|
||||
- `usePopularCaseStudies()` - Fetch popular case studies
|
||||
- `useRelatedCaseStudies(slug)` - Fetch related case studies
|
||||
- `useCaseStudyCategories()` - Fetch categories
|
||||
- `useClients()` - Fetch clients
|
||||
|
||||
### Components
|
||||
|
||||
1. **CaseItems** (`components/pages/case-study/CaseItems.tsx`)
|
||||
- Lists all case studies in a grid
|
||||
- Includes tab navigation for "Case Study" and "Client" views
|
||||
- Dynamically rendered from API data
|
||||
|
||||
2. **CaseSingle** (`components/pages/case-study/CaseSingle.tsx`)
|
||||
- Displays detailed case study information
|
||||
- Shows poster image, project overview, and gallery
|
||||
- Dynamically rendered from API data
|
||||
|
||||
3. **Process** (`components/pages/case-study/Process.tsx`)
|
||||
- Displays process steps for a case study
|
||||
- Dynamically rendered from API data
|
||||
|
||||
4. **RelatedCase** (`components/pages/case-study/RelatedCase.tsx`)
|
||||
- Shows related case studies
|
||||
- Dynamically rendered from API data
|
||||
|
||||
### Pages
|
||||
|
||||
- `/case-study` - Lists all case studies
|
||||
- `/case-study/[slug]` - Displays individual case study
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### Backend Setup
|
||||
|
||||
1. Add `'case_studies'` to `INSTALLED_APPS` in `settings.py` ✅
|
||||
2. Run migrations:
|
||||
```bash
|
||||
python manage.py makemigrations case_studies
|
||||
python manage.py migrate case_studies
|
||||
```
|
||||
|
||||
3. Populate sample data:
|
||||
```bash
|
||||
python manage.py populate_case_studies
|
||||
```
|
||||
|
||||
4. Add URL patterns to main `urls.py`:
|
||||
```python
|
||||
path('api/case-studies/', include('case_studies.urls')),
|
||||
```
|
||||
|
||||
### Frontend Setup
|
||||
|
||||
The frontend is already integrated and ready to use. The components will automatically fetch data from the API when the pages load.
|
||||
|
||||
## Admin Panel
|
||||
|
||||
Access the Django admin panel to manage:
|
||||
- Case Study Categories
|
||||
- Clients
|
||||
- Case Studies
|
||||
- Case Study Images
|
||||
- Case Study Process Steps
|
||||
|
||||
URL: `/admin/`
|
||||
|
||||
## Data Population
|
||||
|
||||
The `populate_case_studies` management command creates:
|
||||
- 6 case study categories
|
||||
- 4 clients
|
||||
- 8 case studies
|
||||
- 32 gallery images
|
||||
- 40 process steps
|
||||
|
||||
## Features
|
||||
|
||||
✅ Full CRUD operations via Django admin
|
||||
✅ RESTful API with filtering and pagination
|
||||
✅ Dynamic frontend with React hooks
|
||||
✅ Image support (local and external URLs)
|
||||
✅ Related case studies
|
||||
✅ Process steps with ordering
|
||||
✅ Gallery images with captions
|
||||
✅ Category and client management
|
||||
✅ Featured and popular case studies
|
||||
✅ SEO fields (meta description, keywords)
|
||||
✅ View count tracking
|
||||
|
||||
## Testing the API
|
||||
|
||||
You can test the API using:
|
||||
|
||||
1. Django REST Framework browsable API:
|
||||
- Navigate to `http://localhost:8000/api/case-studies/case-studies/`
|
||||
|
||||
2. Swagger UI:
|
||||
- Navigate to `http://localhost:8000/swagger/`
|
||||
|
||||
3. Frontend:
|
||||
- Navigate to `http://localhost:3000/case-study`
|
||||
|
||||
## Example API Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Artificial intelligence is the simulation of human processes",
|
||||
"slug": "artificial-intelligence-is-the-simulation-of-human-processes",
|
||||
"subtitle": "AI-Powered Business Solutions",
|
||||
"excerpt": "This artificial intelligence project demonstrates...",
|
||||
"thumbnail": "/images/case/one.png",
|
||||
"category": {
|
||||
"id": 4,
|
||||
"name": "AI",
|
||||
"slug": "ai"
|
||||
},
|
||||
"client": {
|
||||
"id": 1,
|
||||
"name": "Tarapio",
|
||||
"slug": "tarapio"
|
||||
},
|
||||
"gallery_images": [...],
|
||||
"process_steps": [...],
|
||||
"related_case_studies": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All images support both uploaded files and external URLs
|
||||
- Slugs are automatically generated from titles
|
||||
- Case studies are ordered by display_order and published_at
|
||||
- Only published case studies are returned via the API
|
||||
- View counts are automatically incremented when viewing details
|
||||
|
||||
0
gnx-react/backend/case_studies/__init__.py
Normal file
BIN
gnx-react/backend/case_studies/__pycache__/admin.cpython-312.pyc
Normal file
BIN
gnx-react/backend/case_studies/__pycache__/apps.cpython-312.pyc
Normal file
BIN
gnx-react/backend/case_studies/__pycache__/urls.cpython-312.pyc
Normal file
BIN
gnx-react/backend/case_studies/__pycache__/views.cpython-312.pyc
Normal file
105
gnx-react/backend/case_studies/admin.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from django.contrib import admin
|
||||
from .models import CaseStudy, CaseStudyCategory, Client, CaseStudyImage, CaseStudyProcess
|
||||
|
||||
|
||||
@admin.register(CaseStudyCategory)
|
||||
class CaseStudyCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'display_order', 'is_active', 'case_studies_count']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'slug']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ['display_order', 'name']
|
||||
|
||||
def case_studies_count(self, obj):
|
||||
return obj.case_studies.count()
|
||||
case_studies_count.short_description = 'Case Studies Count'
|
||||
|
||||
|
||||
@admin.register(Client)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'website', 'is_active', 'case_studies_count']
|
||||
list_filter = ['is_active', 'created_at']
|
||||
search_fields = ['name', 'slug', 'website']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ['name']
|
||||
|
||||
def case_studies_count(self, obj):
|
||||
return obj.case_studies.count()
|
||||
case_studies_count.short_description = 'Case Studies Count'
|
||||
|
||||
|
||||
class CaseStudyImageInline(admin.TabularInline):
|
||||
model = CaseStudyImage
|
||||
extra = 1
|
||||
fields = ['image', 'image_url', 'caption', 'display_order']
|
||||
|
||||
|
||||
class CaseStudyProcessInline(admin.TabularInline):
|
||||
model = CaseStudyProcess
|
||||
extra = 1
|
||||
fields = ['step_number', 'title', 'description']
|
||||
ordering = ['step_number']
|
||||
|
||||
|
||||
@admin.register(CaseStudy)
|
||||
class CaseStudyAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'title', 'category', 'client', 'published',
|
||||
'featured', 'views_count', 'display_order', 'published_at'
|
||||
]
|
||||
list_filter = [
|
||||
'published', 'featured', 'category',
|
||||
'client', 'published_at', 'created_at'
|
||||
]
|
||||
search_fields = ['title', 'description', 'excerpt']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
date_hierarchy = 'published_at'
|
||||
ordering = ['display_order', '-published_at', '-created_at']
|
||||
inlines = [CaseStudyImageInline, CaseStudyProcessInline]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('title', 'slug', 'subtitle', 'category', 'client')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('excerpt', 'description', 'project_overview', 'site_map_content')
|
||||
}),
|
||||
('Images', {
|
||||
'fields': (
|
||||
'thumbnail', 'thumbnail_url',
|
||||
'featured_image', 'featured_image_url',
|
||||
'poster_image', 'poster_image_url',
|
||||
'project_image', 'project_image_url'
|
||||
)
|
||||
}),
|
||||
('SEO', {
|
||||
'fields': ('meta_description', 'meta_keywords'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Status & Visibility', {
|
||||
'fields': ('published', 'featured', 'display_order', 'published_at')
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('views_count',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['views_count']
|
||||
|
||||
|
||||
@admin.register(CaseStudyImage)
|
||||
class CaseStudyImageAdmin(admin.ModelAdmin):
|
||||
list_display = ['case_study', 'caption', 'display_order', 'created_at']
|
||||
list_filter = ['case_study', 'created_at']
|
||||
search_fields = ['case_study__title', 'caption']
|
||||
ordering = ['case_study', 'display_order']
|
||||
|
||||
|
||||
@admin.register(CaseStudyProcess)
|
||||
class CaseStudyProcessAdmin(admin.ModelAdmin):
|
||||
list_display = ['case_study', 'step_number', 'title', 'created_at']
|
||||
list_filter = ['case_study', 'created_at']
|
||||
search_fields = ['case_study__title', 'title', 'description']
|
||||
ordering = ['case_study', 'step_number']
|
||||
|
||||
7
gnx-react/backend/case_studies/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CaseStudiesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'case_studies'
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from case_studies.models import CaseStudyCategory, Client, CaseStudy, CaseStudyImage, CaseStudyProcess
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Populate database with sample case study data'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('Starting to populate case study data...'))
|
||||
|
||||
# Clear existing data
|
||||
CaseStudyProcess.objects.all().delete()
|
||||
CaseStudyImage.objects.all().delete()
|
||||
CaseStudy.objects.all().delete()
|
||||
Client.objects.all().delete()
|
||||
CaseStudyCategory.objects.all().delete()
|
||||
|
||||
# Create Categories
|
||||
categories_data = [
|
||||
{
|
||||
'name': '3D Render',
|
||||
'slug': '3d-render',
|
||||
'description': '3D rendering and visualization projects',
|
||||
'display_order': 1
|
||||
},
|
||||
{
|
||||
'name': 'UI / UX',
|
||||
'slug': 'ui-ux',
|
||||
'description': 'User interface and user experience design projects',
|
||||
'display_order': 2
|
||||
},
|
||||
{
|
||||
'name': 'Photography',
|
||||
'slug': 'photography',
|
||||
'description': 'Professional photography projects',
|
||||
'display_order': 3
|
||||
},
|
||||
{
|
||||
'name': 'AI',
|
||||
'slug': 'ai',
|
||||
'description': 'Artificial intelligence and machine learning projects',
|
||||
'display_order': 4
|
||||
},
|
||||
{
|
||||
'name': 'Icon Set',
|
||||
'slug': 'icon-set',
|
||||
'description': 'Custom icon set design projects',
|
||||
'display_order': 5
|
||||
},
|
||||
{
|
||||
'name': 'Road Map',
|
||||
'slug': 'road-map',
|
||||
'description': 'Product roadmap and planning projects',
|
||||
'display_order': 6
|
||||
}
|
||||
]
|
||||
|
||||
categories = {}
|
||||
for cat_data in categories_data:
|
||||
category = CaseStudyCategory.objects.create(**cat_data)
|
||||
categories[category.slug] = category
|
||||
self.stdout.write(f'Created category: {category.name}')
|
||||
|
||||
# Create Clients
|
||||
clients_data = [
|
||||
{
|
||||
'name': 'Tarapio',
|
||||
'slug': 'tarapio',
|
||||
'description': 'Leading technology solutions provider',
|
||||
'website': 'https://tarapio.com'
|
||||
},
|
||||
{
|
||||
'name': 'Melenpo',
|
||||
'slug': 'melenpo',
|
||||
'description': 'Digital innovation company',
|
||||
'website': 'https://melenpo.com'
|
||||
},
|
||||
{
|
||||
'name': 'Polax',
|
||||
'slug': 'polax',
|
||||
'description': 'Enterprise software solutions',
|
||||
'website': 'https://polax.com'
|
||||
},
|
||||
{
|
||||
'name': 'AINA',
|
||||
'slug': 'aina',
|
||||
'description': 'AI and automation solutions',
|
||||
'website': 'https://aina.com'
|
||||
}
|
||||
]
|
||||
|
||||
clients = {}
|
||||
for client_data in clients_data:
|
||||
client = Client.objects.create(**client_data)
|
||||
clients[client.slug] = client
|
||||
self.stdout.write(f'Created client: {client.name}')
|
||||
|
||||
# Create Case Studies
|
||||
case_studies_data = [
|
||||
{
|
||||
'title': '3D computer graphics, or "3D graphics',
|
||||
'subtitle': 'Immersive 3D Visualization Experience',
|
||||
'description': '''
|
||||
<h2>Project Overview</h2>
|
||||
<p>A comprehensive 3D rendering project that showcases cutting-edge visualization techniques and photorealistic rendering capabilities. This project demonstrates our expertise in creating stunning visual content for modern digital platforms.</p>
|
||||
|
||||
<h3>The Challenge</h3>
|
||||
<p>Our client needed high-quality 3D visualizations that could accurately represent their products in a digital environment. The challenge was to create renders that were not only photorealistic but also optimized for various platforms and use cases.</p>
|
||||
|
||||
<h3>Our Approach</h3>
|
||||
<p>We employed advanced 3D modeling techniques combined with physically-based rendering (PBR) to achieve exceptional results. Our team utilized industry-standard tools and custom workflows to deliver renders that exceeded client expectations.</p>
|
||||
|
||||
<h3>Key Features</h3>
|
||||
<ul>
|
||||
<li>Photorealistic lighting and materials</li>
|
||||
<li>High-resolution textures and details</li>
|
||||
<li>Multiple viewing angles and perspectives</li>
|
||||
<li>Optimized assets for web and print</li>
|
||||
<li>Interactive 3D viewer integration</li>
|
||||
</ul>
|
||||
|
||||
<h2>Results</h2>
|
||||
<p>The project resulted in a collection of stunning 3D renders that significantly enhanced the client's digital presence. The visualizations led to increased customer engagement and improved conversion rates across their digital channels.</p>
|
||||
''',
|
||||
'category': categories['3d-render'],
|
||||
'client': None,
|
||||
'thumbnail_url': '/images/case/two.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'project_image_url': '/images/case/project.png',
|
||||
'project_overview': 'Lorem ipsum dolor sit amet consectetur. Vestibulum malesuada amet sagittis urna. Mattis eget ultricies est morbi velit ultrices viverra elit facilisi.',
|
||||
'site_map_content': 'Lorem ipsum dolor sit amet consectetur. Vestibulum malesuada amet sagittis urna. Mattis eget ultricies est morbi velit ultrices viverra elit facilisi.',
|
||||
'featured': True,
|
||||
'display_order': 1,
|
||||
'days_ago': 10
|
||||
},
|
||||
{
|
||||
'title': 'Artificial intelligence is the simulation of human processes',
|
||||
'subtitle': 'AI-Powered Business Solutions',
|
||||
'description': '''
|
||||
<h2>About the Project</h2>
|
||||
<p>This artificial intelligence project demonstrates the power of machine learning and AI in solving complex business problems. We developed custom AI models that automate decision-making processes and provide predictive insights.</p>
|
||||
|
||||
<h3>Technology Stack</h3>
|
||||
<p>The project utilizes state-of-the-art AI technologies including neural networks, natural language processing, and computer vision. Our solution is built on a scalable cloud infrastructure that can handle large volumes of data processing.</p>
|
||||
|
||||
<h3>Implementation</h3>
|
||||
<p>We worked closely with the client to understand their specific needs and challenges. The implementation phase involved data collection, model training, validation, and deployment to production environments.</p>
|
||||
|
||||
<h2>Impact</h2>
|
||||
<p>The AI solution has transformed the client's operations, reducing manual work by 60% and improving accuracy in decision-making processes. The system continues to learn and improve over time, providing increasing value to the organization.</p>
|
||||
''',
|
||||
'category': categories['ai'],
|
||||
'client': clients['tarapio'],
|
||||
'thumbnail_url': '/images/case/one.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'featured': True,
|
||||
'display_order': 2,
|
||||
'days_ago': 15
|
||||
},
|
||||
{
|
||||
'title': 'User experience (UX) design is the process design teams',
|
||||
'subtitle': 'Modern UX Design System',
|
||||
'description': '''
|
||||
<h2>Project Summary</h2>
|
||||
<p>A comprehensive UX design project focused on creating intuitive and engaging user experiences. This case study showcases our approach to user-centered design and our ability to create interfaces that delight users.</p>
|
||||
|
||||
<h3>Research and Discovery</h3>
|
||||
<p>We conducted extensive user research including interviews, surveys, and usability testing to understand user needs and pain points. This research formed the foundation of our design decisions.</p>
|
||||
|
||||
<h3>Design Process</h3>
|
||||
<p>Our design process involved creating user personas, journey maps, wireframes, and high-fidelity prototypes. Each step was validated with real users to ensure we were on the right track.</p>
|
||||
|
||||
<h2>Deliverables</h2>
|
||||
<ul>
|
||||
<li>Comprehensive design system</li>
|
||||
<li>Interactive prototypes</li>
|
||||
<li>User flow diagrams</li>
|
||||
<li>Accessibility guidelines</li>
|
||||
<li>Component library</li>
|
||||
</ul>
|
||||
''',
|
||||
'category': categories['ui-ux'],
|
||||
'client': clients['melenpo'],
|
||||
'thumbnail_url': '/images/case/three.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'featured': False,
|
||||
'display_order': 3,
|
||||
'days_ago': 20
|
||||
},
|
||||
{
|
||||
'title': 'Photography is the art, application, and practice',
|
||||
'subtitle': 'Professional Photography Portfolio',
|
||||
'description': '''
|
||||
<h2>About This Project</h2>
|
||||
<p>A stunning photography project that captures the essence of modern visual storytelling. This portfolio demonstrates our expertise in various photography styles and techniques.</p>
|
||||
|
||||
<h3>Photography Style</h3>
|
||||
<p>The project incorporates multiple photography styles including product photography, portrait photography, and architectural photography. Each image is carefully composed and post-processed to achieve the desired aesthetic.</p>
|
||||
|
||||
<h2>Technical Excellence</h2>
|
||||
<p>Using professional-grade equipment and advanced lighting techniques, we created images that stand out in quality and artistic vision.</p>
|
||||
''',
|
||||
'category': categories['photography'],
|
||||
'client': None,
|
||||
'thumbnail_url': '/images/case/four.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'featured': False,
|
||||
'display_order': 4,
|
||||
'days_ago': 25
|
||||
},
|
||||
{
|
||||
'title': 'UX case study for a medical app- medical product design',
|
||||
'subtitle': 'Healthcare UX Innovation',
|
||||
'description': '''
|
||||
<h2>Healthcare Design Challenge</h2>
|
||||
<p>Designing for healthcare requires special attention to accessibility, security, and user trust. This medical app design project showcases our expertise in creating intuitive interfaces for complex healthcare workflows.</p>
|
||||
|
||||
<h3>Compliance and Security</h3>
|
||||
<p>The design adheres to HIPAA regulations and industry best practices for healthcare data security while maintaining an intuitive user experience.</p>
|
||||
|
||||
<h2>User Research</h2>
|
||||
<p>We conducted extensive research with healthcare professionals and patients to ensure the design meets the needs of all stakeholders.</p>
|
||||
''',
|
||||
'category': categories['ui-ux'],
|
||||
'client': clients['polax'],
|
||||
'thumbnail_url': '/images/case/five.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'featured': False,
|
||||
'display_order': 5,
|
||||
'days_ago': 30
|
||||
},
|
||||
{
|
||||
'title': 'Make icon set for the educational project',
|
||||
'subtitle': 'Custom Educational Icon Set',
|
||||
'description': '''
|
||||
<h2>Icon Design Project</h2>
|
||||
<p>A comprehensive icon set designed specifically for educational platforms. This project demonstrates our ability to create cohesive, scalable, and meaningful iconography.</p>
|
||||
|
||||
<h3>Design Principles</h3>
|
||||
<p>The icons follow consistent design principles including size, style, and metaphor. Each icon is designed to be instantly recognizable and appropriate for educational contexts.</p>
|
||||
|
||||
<h2>Deliverables</h2>
|
||||
<p>The final deliverable includes icons in multiple formats (SVG, PNG) and sizes, along with comprehensive usage guidelines.</p>
|
||||
''',
|
||||
'category': categories['icon-set'],
|
||||
'client': None,
|
||||
'thumbnail_url': '/images/case/six.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'featured': False,
|
||||
'display_order': 6,
|
||||
'days_ago': 35
|
||||
},
|
||||
{
|
||||
'title': 'AI-driven user experience design process',
|
||||
'subtitle': 'AI-Driven User Experience',
|
||||
'description': '''
|
||||
<h2>AI-Enhanced UX Design</h2>
|
||||
<p>This project combines artificial intelligence with user experience design to create adaptive interfaces that learn from user behavior and preferences.</p>
|
||||
|
||||
<h3>Innovation</h3>
|
||||
<p>The design incorporates machine learning algorithms that personalize the user experience based on individual usage patterns and preferences.</p>
|
||||
''',
|
||||
'category': categories['ai'],
|
||||
'client': clients['aina'],
|
||||
'thumbnail_url': '/images/case/seven.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'featured': False,
|
||||
'display_order': 7,
|
||||
'days_ago': 40
|
||||
},
|
||||
{
|
||||
'title': 'UX site rode map app product design system',
|
||||
'subtitle': 'Product Roadmap Visualization',
|
||||
'description': '''
|
||||
<h2>Product Roadmap Design</h2>
|
||||
<p>A comprehensive product roadmap visualization system that helps teams plan, communicate, and execute their product strategy effectively.</p>
|
||||
|
||||
<h3>Features</h3>
|
||||
<p>The roadmap system includes timeline views, milestone tracking, dependency mapping, and collaboration tools for distributed teams.</p>
|
||||
|
||||
<h2>Implementation</h2>
|
||||
<p>Built with modern web technologies and optimized for performance and usability across all devices and platforms.</p>
|
||||
''',
|
||||
'category': categories['road-map'],
|
||||
'client': None,
|
||||
'thumbnail_url': '/images/case/eight.png',
|
||||
'poster_image_url': '/images/case/poster.png',
|
||||
'featured': False,
|
||||
'display_order': 8,
|
||||
'days_ago': 45
|
||||
}
|
||||
]
|
||||
|
||||
# Create case studies
|
||||
created_case_studies = []
|
||||
for cs_data in case_studies_data:
|
||||
days_ago = cs_data.pop('days_ago')
|
||||
|
||||
case_study = CaseStudy.objects.create(
|
||||
**cs_data,
|
||||
published_at=timezone.now() - timedelta(days=days_ago)
|
||||
)
|
||||
created_case_studies.append(case_study)
|
||||
|
||||
# Add gallery images
|
||||
gallery_images = [
|
||||
'/images/case/nine.png',
|
||||
'/images/case/ten.png',
|
||||
'/images/case/eleven.png',
|
||||
'/images/case/twelve.png'
|
||||
]
|
||||
|
||||
for idx, img_url in enumerate(gallery_images, 1):
|
||||
CaseStudyImage.objects.create(
|
||||
case_study=case_study,
|
||||
image_url=img_url,
|
||||
caption=f'Gallery Image {idx}',
|
||||
display_order=idx
|
||||
)
|
||||
|
||||
# Add process steps
|
||||
processes = [
|
||||
{'step_number': 1, 'title': 'Computer Vision', 'description': 'Quisque varius malesuada dui, ut posuere purus gravida in. Phasellus ultricies ullamcorper mollis.'},
|
||||
{'step_number': 2, 'title': 'Computer Vision', 'description': 'Quisque varius malesuada dui, ut posuere purus gravida in. Phasellus ultricies ullamcorper mollis.'},
|
||||
{'step_number': 3, 'title': '3D Vision', 'description': 'Quisque varius malesuada dui, ut posuere purus gravida in. Phasellus ultricies ullamcorper mollis.'},
|
||||
{'step_number': 4, 'title': 'Computer Vision', 'description': 'Quisque varius malesuada dui, ut posuere purus gravida in. Phasellus ultricies ullamcorper mollis.'},
|
||||
{'step_number': 5, 'title': '3D Vision', 'description': 'Quisque varius malesuada dui, ut posuere purus gravida in. Phasellus ultricies ullamcorper mollis.'},
|
||||
]
|
||||
|
||||
for process_data in processes:
|
||||
CaseStudyProcess.objects.create(
|
||||
case_study=case_study,
|
||||
**process_data
|
||||
)
|
||||
|
||||
self.stdout.write(f'Created case study: {case_study.title}')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\nSuccessfully populated case study data!'))
|
||||
self.stdout.write(f'Created {CaseStudyCategory.objects.count()} categories')
|
||||
self.stdout.write(f'Created {Client.objects.count()} clients')
|
||||
self.stdout.write(f'Created {CaseStudy.objects.count()} case studies')
|
||||
self.stdout.write(f'Created {CaseStudyImage.objects.count()} gallery images')
|
||||
self.stdout.write(f'Created {CaseStudyProcess.objects.count()} process steps')
|
||||
|
||||
144
gnx-react/backend/case_studies/migrations/0001_initial.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# Generated by Django 4.2.7 on 2025-10-08 10:24
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CaseStudy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=300)),
|
||||
('slug', models.SlugField(max_length=300, unique=True)),
|
||||
('subtitle', models.CharField(blank=True, max_length=300)),
|
||||
('description', models.TextField()),
|
||||
('excerpt', models.TextField(blank=True, help_text='Short excerpt for preview')),
|
||||
('thumbnail', models.ImageField(blank=True, null=True, upload_to='case_studies/thumbnails/')),
|
||||
('thumbnail_url', models.CharField(blank=True, help_text='External thumbnail URL', max_length=500)),
|
||||
('featured_image', models.ImageField(blank=True, null=True, upload_to='case_studies/featured/')),
|
||||
('featured_image_url', models.CharField(blank=True, help_text='External featured image URL', max_length=500)),
|
||||
('poster_image', models.ImageField(blank=True, null=True, upload_to='case_studies/posters/')),
|
||||
('poster_image_url', models.CharField(blank=True, help_text='External poster image URL', max_length=500)),
|
||||
('project_image', models.ImageField(blank=True, null=True, upload_to='case_studies/projects/')),
|
||||
('project_image_url', models.CharField(blank=True, help_text='External project image URL', max_length=500)),
|
||||
('project_overview', models.TextField(blank=True, help_text='Project overview content')),
|
||||
('site_map_content', models.TextField(blank=True, help_text='Site map / process content')),
|
||||
('meta_description', models.CharField(blank=True, max_length=160)),
|
||||
('meta_keywords', models.CharField(blank=True, max_length=255)),
|
||||
('published', models.BooleanField(default=True)),
|
||||
('featured', models.BooleanField(default=False, help_text='Featured case study')),
|
||||
('views_count', models.PositiveIntegerField(default=0)),
|
||||
('display_order', models.PositiveIntegerField(default=0)),
|
||||
('published_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Case Study',
|
||||
'verbose_name_plural': 'Case Studies',
|
||||
'ordering': ['display_order', '-published_at', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CaseStudyCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('display_order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Case Study Category',
|
||||
'verbose_name_plural': 'Case Study Categories',
|
||||
'ordering': ['display_order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Client',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('slug', models.SlugField(max_length=200, unique=True)),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='case_studies/clients/')),
|
||||
('logo_url', models.CharField(blank=True, help_text='External logo URL', max_length=500)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('website', models.URLField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Client',
|
||||
'verbose_name_plural': 'Clients',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CaseStudyProcess',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('step_number', models.PositiveIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('case_study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='process_steps', to='case_studies.casestudy')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Case Study Process',
|
||||
'verbose_name_plural': 'Case Study Processes',
|
||||
'ordering': ['step_number'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CaseStudyImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image', models.ImageField(upload_to='case_studies/gallery/')),
|
||||
('image_url', models.CharField(blank=True, help_text='External image URL', max_length=500)),
|
||||
('caption', models.CharField(blank=True, max_length=200)),
|
||||
('display_order', models.PositiveIntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('case_study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gallery_images', to='case_studies.casestudy')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Case Study Image',
|
||||
'verbose_name_plural': 'Case Study Images',
|
||||
'ordering': ['display_order', 'created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='casestudy',
|
||||
name='category',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='case_studies', to='case_studies.casestudycategory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='casestudy',
|
||||
name='client',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='case_studies', to='case_studies.client'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='casestudy',
|
||||
index=models.Index(fields=['-published_at'], name='case_studie_publish_77559c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='casestudy',
|
||||
index=models.Index(fields=['slug'], name='case_studie_slug_2898e2_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='casestudy',
|
||||
index=models.Index(fields=['published'], name='case_studie_publish_c9c1aa_idx'),
|
||||
),
|
||||
]
|
||||
222
gnx-react/backend/case_studies/models.py
Normal file
@@ -0,0 +1,222 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
class CaseStudyCategory(models.Model):
|
||||
"""Model for case study categories"""
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Case Study Category"
|
||||
verbose_name_plural = "Case Study Categories"
|
||||
ordering = ['display_order', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
"""Model for clients"""
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(max_length=200, unique=True)
|
||||
logo = models.ImageField(upload_to='case_studies/clients/', blank=True, null=True)
|
||||
logo_url = models.CharField(max_length=500, blank=True, help_text="External logo URL")
|
||||
description = models.TextField(blank=True)
|
||||
website = models.URLField(blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Client"
|
||||
verbose_name_plural = "Clients"
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def get_logo_url(self):
|
||||
"""Return the logo URL (uploaded image or external URL)"""
|
||||
if self.logo and hasattr(self.logo, 'url'):
|
||||
return self.logo.url
|
||||
elif self.logo_url:
|
||||
return self.logo_url
|
||||
return None
|
||||
|
||||
|
||||
class CaseStudy(models.Model):
|
||||
"""Model for case studies"""
|
||||
title = models.CharField(max_length=300)
|
||||
slug = models.SlugField(max_length=300, unique=True)
|
||||
subtitle = models.CharField(max_length=300, blank=True)
|
||||
description = models.TextField()
|
||||
excerpt = models.TextField(blank=True, help_text="Short excerpt for preview")
|
||||
|
||||
# Images
|
||||
thumbnail = models.ImageField(upload_to='case_studies/thumbnails/', blank=True, null=True)
|
||||
thumbnail_url = models.CharField(max_length=500, blank=True, help_text="External thumbnail URL")
|
||||
featured_image = models.ImageField(upload_to='case_studies/featured/', blank=True, null=True)
|
||||
featured_image_url = models.CharField(max_length=500, blank=True, help_text="External featured image URL")
|
||||
poster_image = models.ImageField(upload_to='case_studies/posters/', blank=True, null=True)
|
||||
poster_image_url = models.CharField(max_length=500, blank=True, help_text="External poster image URL")
|
||||
project_image = models.ImageField(upload_to='case_studies/projects/', blank=True, null=True)
|
||||
project_image_url = models.CharField(max_length=500, blank=True, help_text="External project image URL")
|
||||
|
||||
# Content sections
|
||||
project_overview = models.TextField(blank=True, help_text="Project overview content")
|
||||
site_map_content = models.TextField(blank=True, help_text="Site map / process content")
|
||||
|
||||
# Relations
|
||||
category = models.ForeignKey(
|
||||
CaseStudyCategory,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='case_studies'
|
||||
)
|
||||
client = models.ForeignKey(
|
||||
Client,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='case_studies'
|
||||
)
|
||||
|
||||
# SEO and metadata
|
||||
meta_description = models.CharField(max_length=160, blank=True)
|
||||
meta_keywords = models.CharField(max_length=255, blank=True)
|
||||
|
||||
# Status and visibility
|
||||
published = models.BooleanField(default=True)
|
||||
featured = models.BooleanField(default=False, help_text="Featured case study")
|
||||
views_count = models.PositiveIntegerField(default=0)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
|
||||
# Timestamps
|
||||
published_at = models.DateTimeField(default=timezone.now)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Case Study"
|
||||
verbose_name_plural = "Case Studies"
|
||||
ordering = ['display_order', '-published_at', '-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['-published_at']),
|
||||
models.Index(fields=['slug']),
|
||||
models.Index(fields=['published']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)
|
||||
if not self.excerpt and self.description:
|
||||
# Generate excerpt from description (first 200 characters)
|
||||
self.excerpt = self.description[:200] + '...' if len(self.description) > 200 else self.description
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def get_thumbnail_url(self):
|
||||
"""Return the thumbnail URL (uploaded image or external URL)"""
|
||||
if self.thumbnail and hasattr(self.thumbnail, 'url'):
|
||||
return self.thumbnail.url
|
||||
elif self.thumbnail_url:
|
||||
return self.thumbnail_url
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_featured_image_url(self):
|
||||
"""Return the featured image URL (uploaded image or external URL)"""
|
||||
if self.featured_image and hasattr(self.featured_image, 'url'):
|
||||
return self.featured_image.url
|
||||
elif self.featured_image_url:
|
||||
return self.featured_image_url
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_poster_image_url(self):
|
||||
"""Return the poster image URL (uploaded image or external URL)"""
|
||||
if self.poster_image and hasattr(self.poster_image, 'url'):
|
||||
return self.poster_image.url
|
||||
elif self.poster_image_url:
|
||||
return self.poster_image_url
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_project_image_url(self):
|
||||
"""Return the project image URL (uploaded image or external URL)"""
|
||||
if self.project_image and hasattr(self.project_image, 'url'):
|
||||
return self.project_image.url
|
||||
elif self.project_image_url:
|
||||
return self.project_image_url
|
||||
return None
|
||||
|
||||
def increment_views(self):
|
||||
"""Increment the view count"""
|
||||
self.views_count += 1
|
||||
self.save(update_fields=['views_count'])
|
||||
|
||||
|
||||
class CaseStudyImage(models.Model):
|
||||
"""Model for additional case study images (gallery)"""
|
||||
case_study = models.ForeignKey(CaseStudy, on_delete=models.CASCADE, related_name='gallery_images')
|
||||
image = models.ImageField(upload_to='case_studies/gallery/')
|
||||
image_url = models.CharField(max_length=500, blank=True, help_text="External image URL")
|
||||
caption = models.CharField(max_length=200, blank=True)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Case Study Image"
|
||||
verbose_name_plural = "Case Study Images"
|
||||
ordering = ['display_order', 'created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.case_study.title} - Image {self.display_order}"
|
||||
|
||||
@property
|
||||
def get_image_url(self):
|
||||
"""Return the image URL (uploaded image or external URL)"""
|
||||
if self.image and hasattr(self.image, 'url'):
|
||||
return self.image.url
|
||||
elif self.image_url:
|
||||
return self.image_url
|
||||
return None
|
||||
|
||||
|
||||
class CaseStudyProcess(models.Model):
|
||||
"""Model for case study process steps"""
|
||||
case_study = models.ForeignKey(CaseStudy, on_delete=models.CASCADE, related_name='process_steps')
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
step_number = models.PositiveIntegerField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Case Study Process"
|
||||
verbose_name_plural = "Case Study Processes"
|
||||
ordering = ['step_number']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.case_study.title} - Step {self.step_number}: {self.title}"
|
||||
|
||||
111
gnx-react/backend/case_studies/serializers.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from rest_framework import serializers
|
||||
from .models import CaseStudy, CaseStudyCategory, Client, CaseStudyImage, CaseStudyProcess
|
||||
|
||||
|
||||
class CaseStudyCategorySerializer(serializers.ModelSerializer):
|
||||
"""Serializer for case study categories"""
|
||||
case_studies_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CaseStudyCategory
|
||||
fields = ['id', 'name', 'slug', 'description', 'display_order', 'case_studies_count']
|
||||
|
||||
def get_case_studies_count(self, obj):
|
||||
return obj.case_studies.filter(published=True).count()
|
||||
|
||||
|
||||
class ClientSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for clients"""
|
||||
logo = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = ['id', 'name', 'slug', 'logo', 'description', 'website']
|
||||
|
||||
def get_logo(self, obj):
|
||||
return obj.get_logo_url
|
||||
|
||||
|
||||
class CaseStudyImageSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for case study gallery images"""
|
||||
image = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CaseStudyImage
|
||||
fields = ['id', 'image', 'caption', 'display_order']
|
||||
|
||||
def get_image(self, obj):
|
||||
return obj.get_image_url
|
||||
|
||||
|
||||
class CaseStudyProcessSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for case study process steps"""
|
||||
class Meta:
|
||||
model = CaseStudyProcess
|
||||
fields = ['id', 'title', 'description', 'step_number']
|
||||
|
||||
|
||||
class CaseStudyListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for case study list view"""
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
category_slug = serializers.CharField(source='category.slug', read_only=True)
|
||||
client_name = serializers.CharField(source='client.name', read_only=True, allow_null=True)
|
||||
thumbnail = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CaseStudy
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'subtitle', 'excerpt', 'thumbnail',
|
||||
'category_name', 'category_slug', 'client_name',
|
||||
'published_at', 'created_at', 'updated_at',
|
||||
'views_count', 'featured', 'published', 'display_order'
|
||||
]
|
||||
|
||||
def get_thumbnail(self, obj):
|
||||
return obj.get_thumbnail_url
|
||||
|
||||
|
||||
class CaseStudyDetailSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for case study detail view"""
|
||||
category = CaseStudyCategorySerializer(read_only=True)
|
||||
client = ClientSerializer(read_only=True)
|
||||
thumbnail = serializers.SerializerMethodField()
|
||||
featured_image = serializers.SerializerMethodField()
|
||||
poster_image = serializers.SerializerMethodField()
|
||||
project_image = serializers.SerializerMethodField()
|
||||
gallery_images = CaseStudyImageSerializer(many=True, read_only=True)
|
||||
process_steps = CaseStudyProcessSerializer(many=True, read_only=True)
|
||||
related_case_studies = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CaseStudy
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'subtitle', 'description', 'excerpt',
|
||||
'thumbnail', 'featured_image', 'poster_image', 'project_image',
|
||||
'project_overview', 'site_map_content',
|
||||
'category', 'client', 'gallery_images', 'process_steps',
|
||||
'meta_description', 'meta_keywords',
|
||||
'published', 'featured', 'views_count', 'display_order',
|
||||
'published_at', 'created_at', 'updated_at', 'related_case_studies'
|
||||
]
|
||||
|
||||
def get_thumbnail(self, obj):
|
||||
return obj.get_thumbnail_url
|
||||
|
||||
def get_featured_image(self, obj):
|
||||
return obj.get_featured_image_url
|
||||
|
||||
def get_poster_image(self, obj):
|
||||
return obj.get_poster_image_url
|
||||
|
||||
def get_project_image(self, obj):
|
||||
return obj.get_project_image_url
|
||||
|
||||
def get_related_case_studies(self, obj):
|
||||
"""Get related case studies from the same category"""
|
||||
related = CaseStudy.objects.filter(
|
||||
category=obj.category,
|
||||
published=True
|
||||
).exclude(id=obj.id)[:3]
|
||||
return CaseStudyListSerializer(related, many=True, context=self.context).data
|
||||
|
||||
5
gnx-react/backend/case_studies/tests.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Test case for case_studies app
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
17
gnx-react/backend/case_studies/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
CaseStudyViewSet,
|
||||
CaseStudyCategoryViewSet,
|
||||
ClientViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'case-studies', CaseStudyViewSet, basename='case-study')
|
||||
router.register(r'categories', CaseStudyCategoryViewSet, basename='case-study-category')
|
||||
router.register(r'clients', ClientViewSet, basename='client')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
138
gnx-react/backend/case_studies/views.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from rest_framework import viewsets, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Q
|
||||
from .models import CaseStudy, CaseStudyCategory, Client
|
||||
from .serializers import (
|
||||
CaseStudyListSerializer,
|
||||
CaseStudyDetailSerializer,
|
||||
CaseStudyCategorySerializer,
|
||||
ClientSerializer
|
||||
)
|
||||
|
||||
|
||||
class CaseStudyPagination(PageNumberPagination):
|
||||
"""Custom pagination for case studies"""
|
||||
page_size = 9
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class CaseStudyViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for case studies
|
||||
Supports filtering by category, client, and search
|
||||
"""
|
||||
queryset = CaseStudy.objects.filter(published=True)
|
||||
pagination_class = CaseStudyPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['category__slug', 'client', 'featured']
|
||||
search_fields = ['title', 'description', 'excerpt']
|
||||
ordering_fields = ['published_at', 'views_count', 'created_at', 'display_order']
|
||||
ordering = ['display_order', '-published_at']
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return CaseStudyDetailSerializer
|
||||
return CaseStudyListSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Filter by category
|
||||
category_slug = self.request.query_params.get('category', None)
|
||||
if category_slug and category_slug != 'all':
|
||||
queryset = queryset.filter(category__slug=category_slug)
|
||||
|
||||
# Filter by client
|
||||
client_slug = self.request.query_params.get('client', None)
|
||||
if client_slug:
|
||||
queryset = queryset.filter(client__slug=client_slug)
|
||||
|
||||
# Search query
|
||||
search = self.request.query_params.get('search', None)
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(title__icontains=search) |
|
||||
Q(description__icontains=search) |
|
||||
Q(excerpt__icontains=search)
|
||||
)
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Override retrieve to increment view count"""
|
||||
instance = self.get_object()
|
||||
instance.increment_views()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def featured(self, request):
|
||||
"""Get featured case studies"""
|
||||
featured_case_studies = self.get_queryset().filter(featured=True)[:6]
|
||||
serializer = self.get_serializer(featured_case_studies, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def latest(self, request):
|
||||
"""Get latest case studies"""
|
||||
limit = int(request.query_params.get('limit', 6))
|
||||
latest_case_studies = self.get_queryset()[:limit]
|
||||
serializer = self.get_serializer(latest_case_studies, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def popular(self, request):
|
||||
"""Get popular case studies by views"""
|
||||
limit = int(request.query_params.get('limit', 6))
|
||||
popular_case_studies = self.get_queryset().order_by('-views_count')[:limit]
|
||||
serializer = self.get_serializer(popular_case_studies, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def related(self, request, slug=None):
|
||||
"""Get related case studies for a specific case study"""
|
||||
case_study = self.get_object()
|
||||
related_case_studies = self.get_queryset().filter(
|
||||
category=case_study.category
|
||||
).exclude(id=case_study.id)[:4]
|
||||
serializer = self.get_serializer(related_case_studies, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class CaseStudyCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for case study categories"""
|
||||
queryset = CaseStudyCategory.objects.filter(is_active=True)
|
||||
serializer_class = CaseStudyCategorySerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def with_case_studies(self, request):
|
||||
"""Get categories that have published case studies"""
|
||||
categories = self.get_queryset().filter(case_studies__published=True).distinct()
|
||||
serializer = self.get_serializer(categories, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ClientViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for clients"""
|
||||
queryset = Client.objects.filter(is_active=True)
|
||||
serializer_class = ClientSerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def case_studies(self, request, slug=None):
|
||||
"""Get all case studies for a specific client"""
|
||||
client = self.get_object()
|
||||
case_studies = CaseStudy.objects.filter(client=client, published=True)
|
||||
page = self.paginate_queryset(case_studies)
|
||||
if page is not None:
|
||||
serializer = CaseStudyListSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = CaseStudyListSerializer(case_studies, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -52,6 +52,8 @@ INSTALLED_APPS = [
|
||||
'about',
|
||||
'career',
|
||||
'support',
|
||||
'blog',
|
||||
'case_studies',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -155,7 +157,7 @@ REST_FRAMEWORK = {
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
'rest_framework.permissions.AllowAny', # Allow public access to read-only endpoints
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
|
||||
@@ -51,6 +51,8 @@ urlpatterns = [
|
||||
path('about/', include('about.urls')),
|
||||
path('career/', include('career.urls')),
|
||||
path('support/', include('support.urls')),
|
||||
path('blog/', include('blog.urls')),
|
||||
path('case-studies/', include('case_studies.urls')),
|
||||
])),
|
||||
]
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
@@ -1,13 +1,73 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import PostFilterItems from "./post-filter/PostFilterItems";
|
||||
|
||||
const BlogItems = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const postsPerPage = 6;
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
setCurrentPage(page);
|
||||
// Scroll to top of posts section
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTotalPagesChange = (total: number) => {
|
||||
setTotalPages(total);
|
||||
};
|
||||
|
||||
// Generate page numbers to display
|
||||
const getPageNumbers = () => {
|
||||
const pages: (number | string)[] = [];
|
||||
const maxPagesToShow = 5;
|
||||
|
||||
if (totalPages <= maxPagesToShow) {
|
||||
// Show all pages if total is small
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Show first page
|
||||
pages.push(1);
|
||||
|
||||
// Calculate range around current page
|
||||
let startPage = Math.max(2, currentPage - 1);
|
||||
let endPage = Math.min(totalPages - 1, currentPage + 1);
|
||||
|
||||
// Add ellipsis after first page if needed
|
||||
if (startPage > 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Add pages around current page
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
// Add ellipsis before last page if needed
|
||||
if (endPage < totalPages - 1) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Show last page
|
||||
if (totalPages > 1) {
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="fix-top pb-120 blog-m">
|
||||
<div className="container">
|
||||
<div className="row align-items-center vertical-column-gap">
|
||||
<div className="col-12 col-lg-7">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim">Blog</h2>
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim">Latest Company Insights</h2>
|
||||
</div>
|
||||
<div className="col-12 col-lg-5">
|
||||
<form action="#" method="post" autoComplete="off">
|
||||
@@ -30,36 +90,58 @@ const BlogItems = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<PostFilterItems />
|
||||
<div className="row mt-60">
|
||||
<div className="col-12">
|
||||
<div className="section__cta">
|
||||
<ul className="pagination">
|
||||
<li>
|
||||
<button>
|
||||
<i className="fa-solid fa-angle-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="blog">1</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="blog" className="active">
|
||||
2
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="blog">3</Link>
|
||||
</li>
|
||||
<li>
|
||||
<button>
|
||||
<i className="fa-solid fa-angle-right"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<PostFilterItems
|
||||
currentPage={currentPage}
|
||||
onPageChange={handlePageChange}
|
||||
onTotalPagesChange={handleTotalPagesChange}
|
||||
postsPerPage={postsPerPage}
|
||||
/>
|
||||
{totalPages > 1 && (
|
||||
<div className="row mt-60">
|
||||
<div className="col-12">
|
||||
<div className="section__cta">
|
||||
<ul className="pagination">
|
||||
<li>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
aria-label="Previous page"
|
||||
style={{ opacity: currentPage === 1 ? 0.5 : 1, cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<i className="fa-solid fa-angle-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
{getPageNumbers().map((page, index) => (
|
||||
<li key={index}>
|
||||
{typeof page === 'number' ? (
|
||||
<button
|
||||
onClick={() => handlePageChange(page)}
|
||||
className={currentPage === page ? 'active' : ''}
|
||||
aria-label={`Go to page ${page}`}
|
||||
aria-current={currentPage === page ? 'page' : undefined}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
) : (
|
||||
<span style={{ padding: '0 10px' }}>{page}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
aria-label="Next page"
|
||||
style={{ opacity: currentPage === totalPages ? 0.5 : 1, cursor: currentPage === totalPages ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<i className="fa-solid fa-angle-right"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,207 +1,289 @@
|
||||
"use client";
|
||||
import { useParams } from "next/navigation";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import poster from "@/public/images/blog/blog-poster.png";
|
||||
import info from "@/public/images/blog/blog-info.png";
|
||||
import { useBlogPost } from "@/lib/hooks/useBlog";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
|
||||
const BlogSingle = () => {
|
||||
return (
|
||||
<section className="tp-post-details fix-top pb-120 fade-wrapper">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="tp-post-intro">
|
||||
<h2 className="title-anim text-xxl fw-7 text-secondary mt-8">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</h2>
|
||||
<div className="mt-60 mb-24 d-flex align-items-center justify-content-between tppr">
|
||||
<div className="d-flex align-items-center tp-post-tags-container mt-8">
|
||||
<p className="text-xs">Scope:</p>
|
||||
<div className="d-flex align-items-center tp-post-tags">
|
||||
<Link href="blog">AI</Link>
|
||||
<span></span>
|
||||
<Link href="blog">Artificial Intelligence</Link>
|
||||
<span></span>
|
||||
<Link href="blog">Data Science</Link>
|
||||
<span></span>
|
||||
<Link href="blog">Machine Learning</Link>
|
||||
</div>
|
||||
const params = useParams();
|
||||
const slug = params?.slug as string;
|
||||
const { post, loading, error } = useBlogPost(slug);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="blog-single-section fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="loading-state text-center py-5">
|
||||
<div className="spinner-border text-primary mb-3" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div className="tp-post-meta mt-8">
|
||||
<p className="author text-xs text-tertiary">Denial Lio</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">18 Dec 2022</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tp-post-poster fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={poster}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group mt-60">
|
||||
<p className="cur-lg mb-24">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec
|
||||
nec tortor id erat faucibus tempor id eget turpis. Donec
|
||||
lobortis et neque eget congue. Mauris laoreet orci ac dictum
|
||||
interdum. Sed dapibus convallis arcu, a aliquam purus sodales
|
||||
nec. Integer consequat et magna sit amet porta. Maecenas
|
||||
consectetur eros sed risus porta convallis eget et massa.
|
||||
Integer auctor convallis ligula, sit amet sollicitudin justo
|
||||
tincidunt a. Sed tellus diam.
|
||||
</p>
|
||||
<p className="cur-lg mb-24 fw-6">
|
||||
Bibendum tincidunt orci vel, sollicitudin bibendum ligula.
|
||||
Pellentesque sollicitudin nulla felis, a ornare tellus
|
||||
tristique ac. Proin ultricies a turpis sit amet lacinia. Ut
|
||||
laoreet nunc leo, ac congue enim laoreet in. Aenean suscipit
|
||||
arcu at ligula tempor porta.
|
||||
</p>
|
||||
<p className="cur-lg">
|
||||
Quisque et fringilla lacus, quis luctus elit. Curabitur eu dui
|
||||
mattis turpis commodo eleifend. Sed porta ornare nunc et
|
||||
tristique. Curabitur vel eros a ante cursus lacinia. Nam nisl
|
||||
leo, aliquet a placerat at, porttitor quis augue. Proin quis
|
||||
aliquet libero. Pellentesque habitant morbi tristique senectus
|
||||
et netus et malesuada fames ac turpis egestas. Vestibulum
|
||||
varius a ipsum ornare blandit. Integer vitae eleifend risus,
|
||||
id tincidunt elit. Integer tincidunt ipsum vitae sagittis
|
||||
porta. Aenean ut facilisis dui. Praesent at ultricies purus.
|
||||
Nam a arcu vel diam ullamcorper tincidunt. Curabitur
|
||||
vestibulum commodo erat non laoreet. Proin nibh nibh,
|
||||
scelerisque a nibh nec, scelerisque convallis leo. Nunc eget
|
||||
elit nunc.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group-info mt-60">
|
||||
<div className="row align-items-center vertical-column-gap">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={info}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<p className="cur-lg mb-24">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Donec nec tortor id erat faucibus tempor id eget turpis.
|
||||
Donec lobortis et neque eget congue. Mauris laoreet orci
|
||||
ac dictum interdum. Sed dapibus convallis arcu, a aliquam
|
||||
purus sodales nec.
|
||||
</p>
|
||||
<p className="cur-lg">
|
||||
Quisque et fringilla lacus, quis luctus elit. Curabitur eu
|
||||
dui mattis turpis commodo eleifend. Sed porta ornare nunc
|
||||
et tristique. Curabitur vel eros a ante cursus lacinia.
|
||||
Nam nisl leo, aliquet a placerat at, porttitor quis augue.
|
||||
Proin quis aliquet libero. Pellentesque habitant morbi
|
||||
tristique senectus et netus et malesuada fames ac turpis
|
||||
egestas. Vestibulum varius a ipsum ornare blandit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group mt-60">
|
||||
<h4 className="mt-8 fw-7 text-secondary title-anim mb-24">
|
||||
The Hidden Markov Model's Limitations
|
||||
</h4>
|
||||
<p className="cur-lg mb-24">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec
|
||||
nec tortor id erat faucibus tempor id eget turpis. Donec
|
||||
lobortis et neque eget congue. Mauris laoreet orci ac dictum
|
||||
interdum. Sed dapibus convallis arcu, a aliquam purus sodales
|
||||
nec.
|
||||
</p>
|
||||
<p className="cur-lg">
|
||||
Quisque et fringilla lacus, quis luctus elit. Curabitur eu dui
|
||||
mattis turpis commodo eleifend. Sed porta ornare nunc et
|
||||
tristique. Curabitur vel eros a ante cursus lacinia. Nam nisl
|
||||
leo, aliquet a placerat at, porttitor quis augue. Proin quis
|
||||
aliquet libero. Pellentesque habitant morbi tristique senectus
|
||||
et netus et malesuada fames ac turpis egestas. Vestibulum
|
||||
varius a ipsum ornare blandit.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group mt-60">
|
||||
<h4 className="mt-8 fw-7 text-secondary title-anim mb-24">
|
||||
The Effect
|
||||
</h4>
|
||||
<p className="cur-lg mb-24">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec
|
||||
nec tortor id erat faucibus tempor id eget turpis. Donec
|
||||
lobortis et neque eget congue. Mauris laoreet orci ac dictum
|
||||
interdum. Sed dapibus convallis arcu, a aliquam purus sodales
|
||||
nec.
|
||||
</p>
|
||||
<p className="cur-lg">
|
||||
Quisque et fringilla lacus, quis luctus elit. Curabitur eu dui
|
||||
mattis turpis commodo eleifend. Sed porta ornare nunc et
|
||||
tristique. Curabitur vel eros a ante cursus lacinia. Nam nisl
|
||||
leo, aliquet a placerat at, porttitor quis augue. Proin quis
|
||||
aliquet libero. Pellentesque habitant morbi tristique senectus
|
||||
et netus et malesuada fames ac turpis egestas. Vestibulum
|
||||
varius a ipsum ornare blandit.
|
||||
</p>
|
||||
<p className="text-tertiary">Loading insight...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mt-80">
|
||||
<div className="col-12">
|
||||
<div className="bd-social">
|
||||
<p className="fw-5 text-uppercase">Share :</p>
|
||||
<ul className=" social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.facebook.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on facebook"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.twitter.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.pinterest.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on pinterest"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.instagram.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on instagram"
|
||||
>
|
||||
<i className="fa-brands fa-instagram"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !post) {
|
||||
return (
|
||||
<section className="blog-single-section fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="error-state text-center py-5">
|
||||
<div className="error-icon mb-4">
|
||||
<i className="fa-solid fa-exclamation-circle fa-4x text-tertiary"></i>
|
||||
</div>
|
||||
<h2 className="text-secondary mb-3">Insight Not Found</h2>
|
||||
<p className="text-tertiary mb-4">
|
||||
The insight you're looking for doesn't exist or has been removed.
|
||||
</p>
|
||||
<Link href="/insights" className="btn btn-primary">
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to Insights
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="blog-single-section fix-top pb-120">
|
||||
<div className="container">
|
||||
{/* Article Content */}
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<article className="blog-single-article">
|
||||
{/* Article Header */}
|
||||
<header className="article-header">
|
||||
{/* Top Meta Bar */}
|
||||
<div className="article-top-meta d-flex flex-wrap align-items-center justify-content-between mb-4">
|
||||
<div className="left-meta d-flex align-items-center gap-3">
|
||||
{/* Category Badge */}
|
||||
{post.category && (
|
||||
<Link href={`/insights?category=${post.category.slug}`} className="category-badge">
|
||||
<i className="fa-solid fa-folder-open me-2"></i>
|
||||
{post.category.title}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<i className="fa-solid fa-calendar me-2"></i>
|
||||
<span>
|
||||
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="right-meta d-flex align-items-center gap-3">
|
||||
{/* Reading Time */}
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<i className="fa-solid fa-clock me-2"></i>
|
||||
<span>{post.reading_time} min</span>
|
||||
</div>
|
||||
|
||||
{/* Views */}
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<i className="fa-solid fa-eye me-2"></i>
|
||||
<span>{post.views_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="article-title">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{/* Author and Tags Bar */}
|
||||
<div className="article-bottom-meta d-flex flex-wrap align-items-center justify-content-between mt-4 pt-4">
|
||||
{/* Author */}
|
||||
<div className="author-meta d-flex align-items-center">
|
||||
<div className="author-avatar me-3">
|
||||
{post.author?.avatar ? (
|
||||
<Image
|
||||
src={post.author.avatar}
|
||||
alt={post.author.name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="rounded-circle"
|
||||
/>
|
||||
) : (
|
||||
<div className="avatar-placeholder">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="author-info">
|
||||
<div className="author-label">Written by</div>
|
||||
<div className="author-name">
|
||||
{post.author?.name || post.author_name || 'Admin'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="article-tags d-flex flex-wrap align-items-center gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<Link
|
||||
key={tag.id}
|
||||
href={`/insights?tag=${tag.slug}`}
|
||||
className="tag-badge"
|
||||
>
|
||||
#{tag.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Featured Image */}
|
||||
{(post.featured_image || post.thumbnail) && (
|
||||
<div className="article-featured-image">
|
||||
<div className="image-wrapper">
|
||||
<Image
|
||||
src={getValidImageUrl(
|
||||
post.featured_image || post.thumbnail,
|
||||
FALLBACK_IMAGES.BLOG
|
||||
)}
|
||||
alt={getValidImageAlt(post.title)}
|
||||
width={1200}
|
||||
height={600}
|
||||
layout="responsive"
|
||||
className="featured-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Article Body */}
|
||||
<div className="article-body">
|
||||
{/* Excerpt */}
|
||||
{post.excerpt && (
|
||||
<div className="article-excerpt">
|
||||
<p className="lead">{post.excerpt}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{post.content && (
|
||||
<div
|
||||
className="article-content enterprise-content"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Section */}
|
||||
<footer className="article-footer">
|
||||
{/* Share Section */}
|
||||
<div className="article-share">
|
||||
<div className="share-container">
|
||||
<h6 className="share-title">
|
||||
Share this insight
|
||||
</h6>
|
||||
<div className="social-share">
|
||||
<Link
|
||||
href={`https://www.linkedin.com/shareArticle?mini=true&url=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}&title=${encodeURIComponent(post.title)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="share-btn share-linkedin"
|
||||
aria-label="Share on LinkedIn"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
<span>LinkedIn</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`https://twitter.com/intent/tweet?url=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}&text=${encodeURIComponent(post.title)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="share-btn share-twitter"
|
||||
aria-label="Share on Twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
<span>Twitter</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="share-btn share-facebook"
|
||||
aria-label="Share on Facebook"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
<span>Facebook</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined' && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
alert('Link copied to clipboard!');
|
||||
}
|
||||
}}
|
||||
className="share-btn share-copy"
|
||||
aria-label="Copy link"
|
||||
>
|
||||
<i className="fa-solid fa-link"></i>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Author Bio */}
|
||||
{post.author && post.author.bio && (
|
||||
<div className="author-bio-section">
|
||||
<div className="author-bio-card">
|
||||
<div className="author-avatar-container">
|
||||
{post.author.avatar ? (
|
||||
<Image
|
||||
src={post.author.avatar}
|
||||
alt={post.author.name}
|
||||
width={90}
|
||||
height={90}
|
||||
className="rounded-circle"
|
||||
/>
|
||||
) : (
|
||||
<div className="author-avatar-large">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="author-bio-content">
|
||||
<div className="bio-label">About the Author</div>
|
||||
<h6 className="author-name">{post.author.name}</h6>
|
||||
<p className="author-bio-text">{post.author.bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="article-navigation">
|
||||
<Link href="/insights" className="btn-back-insights">
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to All Insights
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -4,12 +4,30 @@ import Link from "next/link";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { Autoplay } from "swiper/modules";
|
||||
import "swiper/swiper-bundle.css";
|
||||
import one from "@/public/images/blog/related-one.png";
|
||||
import two from "@/public/images/blog/related-two.png";
|
||||
import three from "@/public/images/blog/related-three.png";
|
||||
import four from "@/public/images/blog/related-four.png";
|
||||
import { useLatestPosts } from "@/lib/hooks/useBlog";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
|
||||
const LatestPost = () => {
|
||||
const { posts, loading, error } = useLatestPosts(8);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading latest posts...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || posts.length === 0) {
|
||||
return null; // Don't show the section if there's an error or no posts
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
|
||||
<div className="container">
|
||||
@@ -17,14 +35,14 @@ const LatestPost = () => {
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="tp-lp-title text-center text-lg-start">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim">
|
||||
Related posts
|
||||
Related Insights
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-lp-cta text-center text-lg-end">
|
||||
<Link href="blog" className="btn-line text-uppercase">
|
||||
See All Posts
|
||||
<Link href="/insights" className="btn-line text-uppercase">
|
||||
View All Insights
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +57,7 @@ const LatestPost = () => {
|
||||
slidesPerGroup={1}
|
||||
freeMode={true}
|
||||
speed={1200}
|
||||
loop={true}
|
||||
loop={posts.length > 3}
|
||||
roundLengths={true}
|
||||
modules={[Autoplay]}
|
||||
autoplay={{
|
||||
@@ -49,378 +67,43 @@ const LatestPost = () => {
|
||||
}}
|
||||
className="tp-lp-slider"
|
||||
>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={one}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
{posts.map((post) => (
|
||||
<SwiperSlide key={post.id}>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href={`/insights/${post.slug}`} className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={getValidImageUrl(post.thumbnail, FALLBACK_IMAGES.BLOG)}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt={getValidImageAlt(post.title)}
|
||||
/>
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={two}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={three}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
{post.author_name || 'Admin'}
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href={`/insights/${post.slug}`}>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={four}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={one}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={two}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={three}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={four}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={one}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={two}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={three}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={four}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { BlogCategoryButtons } from "@/public/data/blog-category";
|
||||
import { useBlogCategories } from "@/lib/hooks/useBlog";
|
||||
|
||||
const PostFilterButtons = ({ handleClick, active }: any) => {
|
||||
const [categories, setCategories] = useState(BlogCategoryButtons);
|
||||
const { categories: apiCategories, loading, error } = useBlogCategories();
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
|
||||
// TODO: Replace with API call to get blog categories
|
||||
// useEffect(() => {
|
||||
// const fetchCategories = async () => {
|
||||
// try {
|
||||
// const response = await blogCategoryAPI.getAll();
|
||||
// if (response.success) {
|
||||
// setCategories(response.data);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Error fetching categories:', error);
|
||||
// }
|
||||
// };
|
||||
// fetchCategories();
|
||||
// }, []);
|
||||
useEffect(() => {
|
||||
if (!loading && apiCategories.length > 0) {
|
||||
// Add "All" category at the beginning
|
||||
const allCategory = {
|
||||
id: 0,
|
||||
title: "All",
|
||||
slug: "all",
|
||||
display_order: 0
|
||||
};
|
||||
setCategories([allCategory, ...apiCategories]);
|
||||
}
|
||||
}, [apiCategories, loading]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="post-filter__wrapper mt-80">
|
||||
<p>Loading categories...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('Error loading categories:', error);
|
||||
// Fallback to showing "All" button only
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="post-filter__wrapper mt-80">
|
||||
<button
|
||||
aria-label="Filter Post"
|
||||
className="active"
|
||||
onClick={() => handleClick("all")}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
|
||||
@@ -5,102 +5,81 @@ import Link from "next/link";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import PostFilterButtons from "./PostFilterButtons";
|
||||
import { useBlogPosts } from "@/lib/hooks/useBlog";
|
||||
|
||||
const PostFilterItems = () => {
|
||||
interface PostFilterItemsProps {
|
||||
currentPage: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onTotalPagesChange: (totalPages: number) => void;
|
||||
postsPerPage?: number;
|
||||
}
|
||||
|
||||
const PostFilterItems = ({ currentPage, onPageChange, onTotalPagesChange, postsPerPage = 10 }: PostFilterItemsProps) => {
|
||||
const [active, setActive] = useState("all");
|
||||
|
||||
// Static blog posts data
|
||||
const allPosts = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Enterprise Software Development Best Practices",
|
||||
content: "Learn about the latest best practices in enterprise software development...",
|
||||
slug: "enterprise-software-development-best-practices",
|
||||
author_id: 1,
|
||||
category_id: 1,
|
||||
thumbnail: "/images/blog/one.png",
|
||||
published: true,
|
||||
category_title: "Development",
|
||||
category_slug: "development",
|
||||
author_name: "John Smith",
|
||||
created_at: "2024-01-15T10:00:00Z",
|
||||
updated_at: "2024-01-15T10:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "API Integration Strategies for Modern Enterprises",
|
||||
content: "Discover effective strategies for API integration in enterprise environments...",
|
||||
slug: "api-integration-strategies-modern-enterprises",
|
||||
author_id: 1,
|
||||
category_id: 2,
|
||||
thumbnail: "/images/blog/two.png",
|
||||
published: true,
|
||||
category_title: "Integration",
|
||||
category_slug: "integration",
|
||||
author_name: "Jane Doe",
|
||||
created_at: "2024-01-10T14:30:00Z",
|
||||
updated_at: "2024-01-10T14:30:00Z"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Cloud Migration: A Complete Guide",
|
||||
content: "Everything you need to know about migrating your enterprise to the cloud...",
|
||||
slug: "cloud-migration-complete-guide",
|
||||
author_id: 1,
|
||||
category_id: 3,
|
||||
thumbnail: "/images/blog/three.png",
|
||||
published: true,
|
||||
category_title: "Cloud",
|
||||
category_slug: "cloud",
|
||||
author_name: "Mike Johnson",
|
||||
created_at: "2024-01-05T09:15:00Z",
|
||||
updated_at: "2024-01-05T09:15:00Z"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Digital Transformation in Enterprise",
|
||||
content: "How digital transformation is reshaping enterprise operations...",
|
||||
slug: "digital-transformation-enterprise",
|
||||
author_id: 1,
|
||||
category_id: 1,
|
||||
thumbnail: "/images/blog/four.png",
|
||||
published: true,
|
||||
category_title: "Development",
|
||||
category_slug: "development",
|
||||
author_name: "Sarah Wilson",
|
||||
created_at: "2024-01-01T16:45:00Z",
|
||||
updated_at: "2024-01-01T16:45:00Z"
|
||||
const { posts: allPosts, loading, error, totalCount } = useBlogPosts({
|
||||
category: active === "all" ? undefined : active,
|
||||
page: currentPage,
|
||||
page_size: postsPerPage
|
||||
});
|
||||
const [displayData, setDisplayData] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && allPosts.length > 0) {
|
||||
setDisplayData(allPosts);
|
||||
}
|
||||
];
|
||||
|
||||
const [displayData, setDisplayData] = useState(allPosts);
|
||||
}, [allPosts, loading]);
|
||||
|
||||
// Calculate and update total pages when totalCount changes
|
||||
useEffect(() => {
|
||||
if (totalCount !== undefined && totalCount !== null) {
|
||||
const calculatedTotalPages = Math.ceil(totalCount / postsPerPage);
|
||||
onTotalPagesChange(calculatedTotalPages);
|
||||
}
|
||||
}, [totalCount, postsPerPage, onTotalPagesChange]);
|
||||
|
||||
const handleCategoryClick = (category: SetStateAction<string>) => {
|
||||
if (category === active) return;
|
||||
setActive(category);
|
||||
setDisplayData([]);
|
||||
onPageChange(1); // Reset to page 1 when category changes
|
||||
|
||||
if (category === "all") {
|
||||
setDisplayData(allPosts);
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredData = allPosts.filter(
|
||||
(item) => item.category_slug === category
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
setDisplayData(filteredData);
|
||||
}, 600);
|
||||
// The API call will be triggered by the change in active state
|
||||
// which will update allPosts and trigger the useEffect above
|
||||
};
|
||||
|
||||
if (loading && displayData.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
||||
<div className="row mt-60">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading posts...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('Error loading posts:', error);
|
||||
return (
|
||||
<>
|
||||
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
||||
<div className="row mt-60">
|
||||
<div className="col-12">
|
||||
<p className="text-center text-danger">Error loading posts. Please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
||||
<motion.div className="row mt-60 masonry-grid" layout>
|
||||
<AnimatePresence>
|
||||
{displayData.slice(0, 8).map((item) => {
|
||||
{displayData.map((item) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="col-12 col-lg-6 grid-item-main"
|
||||
@@ -114,7 +93,7 @@ const PostFilterItems = () => {
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link
|
||||
href={`/blog/${item.slug}`}
|
||||
href={`/insights/${item.slug}`}
|
||||
className="w-100 overflow-hidden d-block"
|
||||
>
|
||||
<div className="parallax-image-wrap">
|
||||
@@ -137,7 +116,7 @@ const PostFilterItems = () => {
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
{new Date(item.created_at).toLocaleDateString('en-US', {
|
||||
{new Date(item.published_at || item.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
@@ -145,7 +124,7 @@ const PostFilterItems = () => {
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href={`/blog/${item.slug}`}>{item.title}</Link>
|
||||
<Link href={`/insights/${item.slug}`}>{item.title}</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { useCaseStudies, useClients } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import one from "@/public/images/case/one.png";
|
||||
import two from "@/public/images/case/two.png";
|
||||
import three from "@/public/images/case/three.png";
|
||||
import four from "@/public/images/case/four.png";
|
||||
import five from "@/public/images/case/five.png";
|
||||
import six from "@/public/images/case/six.png";
|
||||
import seven from "@/public/images/case/seven.png";
|
||||
import eight from "@/public/images/case/eight.png";
|
||||
import thirteen from "@/public/images/case/thirteen.png";
|
||||
import fourteen from "@/public/images/case/fourteen.png";
|
||||
import fifteen from "@/public/images/case/fifteen.png";
|
||||
import sixteen from "@/public/images/case/sixteen.png";
|
||||
|
||||
const CaseItems = () => {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
const { caseStudies, loading: casesLoading } = useCaseStudies();
|
||||
const { clients, loading: clientsLoading } = useClients();
|
||||
|
||||
const handleTabClick = (index: number) => {
|
||||
setActiveTabIndex(index);
|
||||
};
|
||||
|
||||
// Filter case studies by category
|
||||
const caseStudiesData = caseStudies.filter((cs) => !cs.client);
|
||||
const clientCaseStudies = caseStudies.filter((cs) => cs.client);
|
||||
|
||||
if (casesLoading || clientsLoading) {
|
||||
return (
|
||||
<section className="fix-top pb-120 c-study">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading case studies...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="fix-top pb-120 c-study">
|
||||
@@ -79,194 +90,38 @@ const CaseItems = () => {
|
||||
}`}
|
||||
>
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={two}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
3D Render
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
3D computer graphics, or “3D graphics.
|
||||
{caseStudiesData.map((caseStudy) => (
|
||||
<div key={caseStudy.id} className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
|
||||
<Image
|
||||
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
|
||||
className="w-100 mh-300"
|
||||
alt={caseStudy.title}
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href={`/case-study/${caseStudy.slug}`} className="mb-30 fw-6">
|
||||
{caseStudy.category_name || 'Case Study'}
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href={`/case-study/${caseStudy.slug}`}>
|
||||
{caseStudy.title}
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={one}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
3D Render
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
Artificial intelligence is the simulation of human
|
||||
processes.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
))}
|
||||
{caseStudiesData.length === 0 && (
|
||||
<div className="col-12">
|
||||
<p className="text-center">No case studies found.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={three}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
UI / UX
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
User experience (UX) design is the process design
|
||||
teams.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={four}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
Photography
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
Photography is the art, application, and practice.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={five}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
UI / UX
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
UX case study for a medical app- medical product
|
||||
design.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={six}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
Icon Set
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
Make icon set for the educational project.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={seven}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
AI
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
User experience (UX) design is the process design
|
||||
teams.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<Image
|
||||
src={eight}
|
||||
className="w-100 mh-300 "
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
Road Map
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
UX site rode map app product design system
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -275,154 +130,48 @@ const CaseItems = () => {
|
||||
}`}
|
||||
>
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="col-12">
|
||||
<div className="row vertical-column-gap-md align-items-center">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-tab__client">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary mb-24">
|
||||
Tarapio
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Pharetra sit
|
||||
amet est tellus nibh sit lacus in duis. Condimentum
|
||||
tellus sit pharetra consectetur magna massa. In odio
|
||||
leo pellentesque aenean egestas est risus etiam.
|
||||
Quam in nunc consectetur blandit id.
|
||||
</p>
|
||||
<div className="mt-40">
|
||||
<Link
|
||||
href="case-study-single"
|
||||
className="btn-anim btn-anim-light"
|
||||
>
|
||||
Read More
|
||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
{clientCaseStudies.map((caseStudy, index) => (
|
||||
<div key={caseStudy.id} className="col-12">
|
||||
<div className={`row vertical-column-gap-md align-items-center ${index % 2 === 1 ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-tab__client">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary mb-24">
|
||||
{caseStudy.client?.name || caseStudy.title}
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
{caseStudy.excerpt || caseStudy.client?.description}
|
||||
</p>
|
||||
<div className="mt-40">
|
||||
<Link
|
||||
href={`/case-study/${caseStudy.slug}`}
|
||||
className="btn-anim btn-anim-light"
|
||||
>
|
||||
Read More
|
||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1">
|
||||
<div className="c-tab__thumb">
|
||||
<Image
|
||||
src={thirteen}
|
||||
className="w-100 mh-300"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="row vertical-column-gap-md align-items-center">
|
||||
<div className="col-12 col-lg-6 col-xxl-5">
|
||||
<div className="c-tab__thumb">
|
||||
<Image
|
||||
src={fourteen}
|
||||
className="w-100 mh-300"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 offset-xxl-1">
|
||||
<div className="c-tab__client">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary mb-24">
|
||||
Melenpo
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Pharetra sit
|
||||
amet est tellus nibh sit lacus in duis. Condimentum
|
||||
tellus sit pharetra consectetur magna massa. In odio
|
||||
leo pellentesque aenean egestas est risus etiam.
|
||||
Quam in nunc consectetur blandit id.
|
||||
</p>
|
||||
<div className="mt-40">
|
||||
<Link
|
||||
href="case-study-single"
|
||||
className="btn-anim btn-anim-light"
|
||||
>
|
||||
Read More
|
||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
<div className={`col-12 col-lg-6 col-xxl-5 ${index % 2 === 0 ? 'offset-xxl-1' : ''}`}>
|
||||
<div className="c-tab__thumb">
|
||||
<Image
|
||||
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
|
||||
className="w-100 mh-300"
|
||||
alt={caseStudy.client?.name || caseStudy.title}
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="row vertical-column-gap-md align-items-center">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-tab__client">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary mb-24">
|
||||
Polax
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Pharetra sit
|
||||
amet est tellus nibh sit lacus in duis. Condimentum
|
||||
tellus sit pharetra consectetur magna massa. In odio
|
||||
leo pellentesque aenean egestas est risus etiam.
|
||||
Quam in nunc consectetur blandit id.
|
||||
</p>
|
||||
<div className="mt-40">
|
||||
<Link
|
||||
href="case-study-single"
|
||||
className="btn-anim btn-anim-light"
|
||||
>
|
||||
Read More
|
||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1">
|
||||
<div className="c-tab__thumb">
|
||||
<Image
|
||||
src={fifteen}
|
||||
className="w-100 mh-300"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{clientCaseStudies.length === 0 && (
|
||||
<div className="col-12">
|
||||
<p className="text-center">No client case studies found.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="row vertical-column-gap-md align-items-center">
|
||||
<div className="col-12 col-lg-6 col-xxl-5">
|
||||
<div className="c-tab__thumb">
|
||||
<Image
|
||||
src={sixteen}
|
||||
className="w-100 mh-300"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 offset-xxl-1">
|
||||
<div className="c-tab__client">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary mb-24">
|
||||
AINA
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Pharetra sit
|
||||
amet est tellus nibh sit lacus in duis. Condimentum
|
||||
tellus sit pharetra consectetur magna massa. In odio
|
||||
leo pellentesque aenean egestas est risus etiam.
|
||||
Quam in nunc consectetur blandit id.
|
||||
</p>
|
||||
<div className="mt-40">
|
||||
<Link
|
||||
href="case-study-single"
|
||||
className="btn-anim btn-anim-light"
|
||||
>
|
||||
Read More
|
||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,47 @@
|
||||
"use client";
|
||||
import { use } from 'react';
|
||||
import Image from "next/legacy/image";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import poster from "@/public/images/case/poster.png";
|
||||
import project from "@/public/images/case/project.png";
|
||||
import nine from "@/public/images/case/nine.png";
|
||||
import ten from "@/public/images/case/ten.png";
|
||||
import eleven from "@/public/images/case/eleven.png";
|
||||
import twelve from "@/public/images/case/twelve.png";
|
||||
|
||||
const CaseSingle = () => {
|
||||
interface CaseSingleProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const CaseSingle = ({ slug }: CaseSingleProps) => {
|
||||
const { caseStudy, loading, error } = useCaseStudy(slug);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading case study...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !caseStudy) {
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center text-danger">Case study not found.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<div className="container">
|
||||
@@ -14,16 +49,20 @@ const CaseSingle = () => {
|
||||
<div className="col-12">
|
||||
<div className="c-details-intro">
|
||||
<h2 className="mt-8 text-secondary title-anim fw-7">
|
||||
Artificial intelligence is the simulation of human intelligence
|
||||
processes.
|
||||
{caseStudy.title}
|
||||
</h2>
|
||||
{caseStudy.subtitle && (
|
||||
<h4 className="mt-4 text-secondary">{caseStudy.subtitle}</h4>
|
||||
)}
|
||||
<div className="poster mt-60 fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={poster}
|
||||
src={caseStudy.poster_image ? getImageUrl(caseStudy.poster_image) : poster}
|
||||
className="w-100 parallax-image mh-300"
|
||||
alt="Image"
|
||||
alt={caseStudy.title}
|
||||
width={1200}
|
||||
height={600}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,17 +73,14 @@ const CaseSingle = () => {
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim mb-24">
|
||||
Project
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Vestibulum
|
||||
malesuada amet sagittis urna. Mattis eget ultricies est
|
||||
morbi velit ultrices viverra elit facilisi. Amet est cras
|
||||
euismod accumsan ornare sagittis ut integer. Sagittis sed
|
||||
neque massa amet. Lorem vulputate nunc pulvinar maecenas
|
||||
convallis augue. Magna massa viverra tincidunt vitae lacus
|
||||
donec arcu consequat in. Maecenas dui nunc in convallis
|
||||
vulputate vitae lectus eu lacus donec arcu consequat in.
|
||||
Maecenas dui nunc in convallis vulputate vitae lectus eu.
|
||||
</p>
|
||||
{caseStudy.project_overview ? (
|
||||
<p className="cur-lg">{caseStudy.project_overview}</p>
|
||||
) : (
|
||||
<div
|
||||
className="cur-lg"
|
||||
dangerouslySetInnerHTML={{ __html: caseStudy.description || '' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1 fade-wrapper">
|
||||
@@ -52,88 +88,50 @@ const CaseSingle = () => {
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={project}
|
||||
src={caseStudy.project_image ? getImageUrl(caseStudy.project_image) : project}
|
||||
className="w-100 parallax-image mh-260"
|
||||
alt="Image"
|
||||
alt={`${caseStudy.title} - Project`}
|
||||
width={600}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="road-map__content">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim mb-24">
|
||||
Site Map
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Vestibulum
|
||||
malesuada amet sagittis urna. Mattis eget ultricies est
|
||||
morbi velit ultrices viverra elit facilisi. Amet est cras
|
||||
euismod accumsan ornare sagittis ut integer. Sagittis sed
|
||||
neque massa amet. Lorem vulputate nunc pulvinar maecenas
|
||||
convallis augue. Magna massa viverra tincidunt vitae lacus
|
||||
donec arcu consequat in. Maecenas dui nunc in convallis
|
||||
vulputate vitae lectus eu.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row vertical-column-gap mt-60 fade-wrapper">
|
||||
<div className="col-12 col-sm-6 col-xl-3">
|
||||
<div className="c-details-thumb fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={nine}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
{caseStudy.site_map_content && (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="road-map__content">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim mb-24">
|
||||
Site Map
|
||||
</h2>
|
||||
<p className="cur-lg">{caseStudy.site_map_content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-xl-3">
|
||||
<div className="c-details-thumb fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={ten}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
)}
|
||||
{caseStudy.gallery_images && caseStudy.gallery_images.length > 0 && (
|
||||
<div className="row vertical-column-gap mt-60 fade-wrapper">
|
||||
{caseStudy.gallery_images.map((image) => (
|
||||
<div key={image.id} className="col-12 col-sm-6 col-xl-3">
|
||||
<div className="c-details-thumb fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={getImageUrl(image.image) || nine}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt={image.caption || caseStudy.title}
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-xl-3">
|
||||
<div className="c-details-thumb fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={eleven}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-xl-3">
|
||||
<div className="c-details-thumb fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={twelve}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
const Process = () => {
|
||||
"use client";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
|
||||
interface ProcessProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const Process = ({ slug }: ProcessProps) => {
|
||||
const { caseStudy, loading } = useCaseStudy(slug);
|
||||
|
||||
if (loading || !caseStudy || !caseStudy.process_steps || caseStudy.process_steps.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pt-120 pb-120 tp-process bg-black sticky-wrapper">
|
||||
<div className="container">
|
||||
@@ -6,64 +19,28 @@ const Process = () => {
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="process__content sticky-item">
|
||||
<h2 className="mt-8 title-anim text-white fw-7 mb-24">
|
||||
Artificial intelligence Process
|
||||
{caseStudy.title} Process
|
||||
</h2>
|
||||
<p className="cur-lg text-quinary">
|
||||
Quisque varius malesuada dui, ut posuere purus gravida in.
|
||||
Phasellus ultricies ullamcorper mollis. Pellentesque varius
|
||||
lectus in massa placerat cursus. Donec in dictum nisl. In
|
||||
maximus posuere leo nec porttitor.
|
||||
{caseStudy.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1">
|
||||
<div className="process__thumb sticky-item">
|
||||
<div className="process__single">
|
||||
<span className="op-text text-white mb-40 cur-lg">01</span>
|
||||
<h5 className="mt-8 text-white mb-24 title-anim">
|
||||
Computer Vision
|
||||
</h5>
|
||||
<p className="cur-lg text-quinary">
|
||||
Quisque varius malesuada dui, ut posuere purus gravida in.
|
||||
Phasellus ultricies ullamcorper mollis.
|
||||
</p>
|
||||
</div>
|
||||
<div className="process__single">
|
||||
<span className="op-text text-white mb-40 cur-lg">02</span>
|
||||
<h5 className="mt-8 text-white mb-24 title-anim">
|
||||
Computer Vision
|
||||
</h5>
|
||||
<p className="cur-lg text-quinary">
|
||||
Quisque varius malesuada dui, ut posuere purus gravida in.
|
||||
Phasellus ultricies ullamcorper mollis.
|
||||
</p>
|
||||
</div>
|
||||
<div className="process__single">
|
||||
<span className="op-text text-white mb-40 cur-lg">03</span>
|
||||
<h5 className="mt-8 text-white mb-24 title-anim">3D Vision</h5>
|
||||
<p className="cur-lg text-quinary">
|
||||
Quisque varius malesuada dui, ut posuere purus gravida in.
|
||||
Phasellus ultricies ullamcorper mollis.
|
||||
</p>
|
||||
</div>
|
||||
<div className="process__single">
|
||||
<span className="op-text text-white mb-40 cur-lg">04</span>
|
||||
<h5 className="mt-8 text-white mb-24 title-anim">
|
||||
Computer Vision
|
||||
</h5>
|
||||
<p className="cur-lg text-quinary">
|
||||
Quisque varius malesuada dui, ut posuere purus gravida in.
|
||||
Phasellus ultricies ullamcorper mollis.
|
||||
</p>
|
||||
</div>
|
||||
<div className="process__single">
|
||||
<span className="op-text text-white mb-40 cur-lg">05</span>
|
||||
<h5 className="mt-8 text-white mb-24 title-anim">3D Vision</h5>
|
||||
<p className="cur-lg text-quinary">
|
||||
Quisque varius malesuada dui, ut posuere purus gravida in.
|
||||
Phasellus ultricies ullamcorper mollis.
|
||||
</p>
|
||||
</div>
|
||||
{caseStudy.process_steps.map((step) => (
|
||||
<div key={step.id} className="process__single">
|
||||
<span className="op-text text-white mb-40 cur-lg">
|
||||
{String(step.step_number).padStart(2, '0')}
|
||||
</span>
|
||||
<h5 className="mt-8 text-white mb-24 title-anim">
|
||||
{step.title}
|
||||
</h5>
|
||||
<p className="cur-lg text-quinary">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
"use client";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import one from "@/public/images/case/one.png";
|
||||
import two from "@/public/images/case/two.png";
|
||||
|
||||
const RelatedCase = () => {
|
||||
interface RelatedCaseProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const RelatedCase = ({ slug }: RelatedCaseProps) => {
|
||||
const { caseStudy, loading } = useCaseStudy(slug);
|
||||
|
||||
if (loading || !caseStudy || !caseStudy.related_case_studies || caseStudy.related_case_studies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pt-120 pb-120 c-study fade-wrapper">
|
||||
<div className="container">
|
||||
@@ -15,61 +27,37 @@ const RelatedCase = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single fade-top">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={two}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
{caseStudy.related_case_studies.slice(0, 2).map((relatedCase) => (
|
||||
<div key={relatedCase.id} className="col-12 col-lg-6">
|
||||
<div className="c-study-single fade-top">
|
||||
<div className="thumb mb-24">
|
||||
<Link href={`/case-study/${relatedCase.slug}`} className="w-100">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt={relatedCase.title}
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
3D Render
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
3D computer graphics, or “3D graphics.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href={`/case-study/${relatedCase.slug}`} className="mb-30 fw-6">
|
||||
{relatedCase.category_name || 'Case Study'}
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href={`/case-study/${relatedCase.slug}`}>
|
||||
{relatedCase.title}
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-study-single fade-top">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="case-study-single" className="w-100">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={one}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href="case-study" className="mb-30 fw-6">
|
||||
3D Render
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href="case-study-single">
|
||||
Artificial intelligence is the simulation of human
|
||||
processes.
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,12 +4,12 @@ import Link from "next/link";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { Autoplay } from "swiper/modules";
|
||||
import "swiper/swiper-bundle.css";
|
||||
import one from "@/public/images/blog/related-one.png";
|
||||
import two from "@/public/images/blog/related-two.png";
|
||||
import three from "@/public/images/blog/related-three.png";
|
||||
import four from "@/public/images/blog/related-four.png";
|
||||
import { useLatestPosts } from "@/lib/hooks/useBlog";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
|
||||
const HomeLatestPost = () => {
|
||||
const { posts, loading, error } = useLatestPosts(12); // Get 12 latest posts
|
||||
|
||||
return (
|
||||
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
|
||||
<div className="container">
|
||||
@@ -17,14 +17,14 @@ const HomeLatestPost = () => {
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="tp-lp-title text-center text-lg-start">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim">
|
||||
Related posts
|
||||
Latest Insights
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-lp-cta text-center text-lg-end">
|
||||
<Link href="blog" className="btn-line text-uppercase">
|
||||
See All Posts
|
||||
<Link href="/insights" className="btn-line text-uppercase">
|
||||
View All Insights
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,397 +32,86 @@ const HomeLatestPost = () => {
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="tp-lp-slider-wrapper mt-60">
|
||||
<div className="tp-lp-slider-wrapper">
|
||||
<Swiper
|
||||
slidesPerView={"auto"}
|
||||
spaceBetween={24}
|
||||
slidesPerGroup={1}
|
||||
freeMode={true}
|
||||
speed={1200}
|
||||
loop={true}
|
||||
roundLengths={true}
|
||||
modules={[Autoplay]}
|
||||
autoplay={{
|
||||
delay: 5000,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: true,
|
||||
}}
|
||||
className="tp-lp-slider"
|
||||
>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={one}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
{loading ? (
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="text-tertiary mt-3">Loading insights...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-danger">Error loading insights. Please try again later.</p>
|
||||
</div>
|
||||
) : posts.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-tertiary">No insights available yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tp-lp-slider-wrapper">
|
||||
<Swiper
|
||||
slidesPerView={"auto"}
|
||||
spaceBetween={24}
|
||||
slidesPerGroup={1}
|
||||
freeMode={true}
|
||||
speed={1200}
|
||||
loop={posts.length > 3}
|
||||
roundLengths={true}
|
||||
modules={[Autoplay]}
|
||||
autoplay={{
|
||||
delay: 5000,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: true,
|
||||
}}
|
||||
className="tp-lp-slider"
|
||||
>
|
||||
{posts.map((post) => (
|
||||
<SwiperSlide key={post.id}>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link
|
||||
href={`/insights/${post.slug}`}
|
||||
className="w-100 overflow-hidden d-block"
|
||||
>
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={getValidImageUrl(post.thumbnail, FALLBACK_IMAGES.BLOG)}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220 parallax-image"
|
||||
alt={getValidImageAlt(post.title)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
{post.author_name || 'Admin'}
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href={`/insights/${post.slug}`}>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={two}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={three}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={four}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={one}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={two}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={three}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={four}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={one}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={two}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={three}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href="blog-single" className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={four}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt="Image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
Denial Lio
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
18 Dec 2022
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href="blog-single">
|
||||
Tackling data of annotation problems in healthcare
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,72 +1,92 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
|
||||
import { API_CONFIG } from "@/lib/config/api";
|
||||
|
||||
const Story = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
||||
|
||||
// Static case studies data
|
||||
const storyData = [
|
||||
// Fetch case studies from API with ordering and limit
|
||||
const params = useMemo(() => ({
|
||||
ordering: 'display_order',
|
||||
page_size: 5
|
||||
}), []);
|
||||
|
||||
const { caseStudies, loading, error } = useCaseStudies(params);
|
||||
|
||||
// Fallback to static data if API fails or is loading
|
||||
const staticStoryData = [
|
||||
{
|
||||
id: 1,
|
||||
destination: "Financial Services",
|
||||
category_name: "Financial Services",
|
||||
title: "Banking System Modernization",
|
||||
subtitle: "Complete digital transformation of legacy banking systems with enhanced security and real-time processing capabilities.",
|
||||
image: "/images/case/one.png",
|
||||
path: "/case-study/banking-system-modernization",
|
||||
excerpt: "Complete digital transformation of legacy banking systems with enhanced security and real-time processing capabilities.",
|
||||
thumbnail: "/images/case/one.png",
|
||||
slug: "banking-system-modernization",
|
||||
display_order: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
destination: "Healthcare",
|
||||
category_name: "Healthcare",
|
||||
title: "Patient Management System",
|
||||
subtitle: "Enterprise-grade patient management system with HIPAA compliance and seamless integration across multiple healthcare facilities.",
|
||||
image: "/images/case/two.png",
|
||||
path: "/case-study/patient-management-system",
|
||||
excerpt: "Enterprise-grade patient management system with HIPAA compliance and seamless integration across multiple healthcare facilities.",
|
||||
thumbnail: "/images/case/two.png",
|
||||
slug: "patient-management-system",
|
||||
display_order: 2,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
destination: "Manufacturing",
|
||||
category_name: "Manufacturing",
|
||||
title: "Supply Chain Optimization",
|
||||
subtitle: "Advanced supply chain management system with real-time tracking, predictive analytics, and automated inventory management.",
|
||||
image: "/images/case/three.png",
|
||||
path: "/case-study/supply-chain-optimization",
|
||||
excerpt: "Advanced supply chain management system with real-time tracking, predictive analytics, and automated inventory management.",
|
||||
thumbnail: "/images/case/three.png",
|
||||
slug: "supply-chain-optimization",
|
||||
display_order: 3,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
destination: "E-commerce",
|
||||
category_name: "E-commerce",
|
||||
title: "Multi-Platform Integration",
|
||||
subtitle: "Seamless integration of multiple e-commerce platforms with unified inventory management and real-time synchronization.",
|
||||
image: "/images/case/four.png",
|
||||
path: "/case-study/multi-platform-integration",
|
||||
excerpt: "Seamless integration of multiple e-commerce platforms with unified inventory management and real-time synchronization.",
|
||||
thumbnail: "/images/case/four.png",
|
||||
slug: "multi-platform-integration",
|
||||
display_order: 4,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
destination: "Education",
|
||||
category_name: "Education",
|
||||
title: "Learning Management System",
|
||||
subtitle: "Comprehensive LMS with advanced analytics, automated grading, and seamless integration with existing educational tools.",
|
||||
image: "/images/case/five.png",
|
||||
path: "/case-study/learning-management-system",
|
||||
excerpt: "Comprehensive LMS with advanced analytics, automated grading, and seamless integration with existing educational tools.",
|
||||
thumbnail: "/images/case/five.png",
|
||||
slug: "learning-management-system",
|
||||
display_order: 5,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// Use API data if available, otherwise use static data
|
||||
const storyData = caseStudies.length > 0 ? caseStudies : staticStoryData;
|
||||
|
||||
// Log when API data is loaded
|
||||
useEffect(() => {
|
||||
if (caseStudies.length > 0) {
|
||||
console.log('Case studies loaded from API:', caseStudies.length);
|
||||
}
|
||||
}, [caseStudies]);
|
||||
|
||||
const handleMouseEnter = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
setActiveImageIndex(index);
|
||||
@@ -93,12 +113,14 @@ const Story = () => {
|
||||
onMouseEnter={() => handleMouseEnter(index)}
|
||||
>
|
||||
<p className="fw-6 mt-8">
|
||||
<Link href={`${item.path}`}>{item.destination}</Link>
|
||||
<Link href={`/case-study/${item.slug}`}>
|
||||
{item.category_name || "Case Study"}
|
||||
</Link>
|
||||
</p>
|
||||
<h5 className="fw-4 mt-12 mb-12 text-white">
|
||||
{item.title}
|
||||
</h5>
|
||||
<p className="text-xs">{item.subtitle}</p>
|
||||
<p className="text-xs">{item.excerpt}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -108,6 +130,26 @@ const Story = () => {
|
||||
<div className="col-12 col-lg-7 col-xxl-6 offset-xxl-1 d-none d-lg-block">
|
||||
<div className="tp-story__thumbs sticky-item">
|
||||
{storyData.map((item, index) => {
|
||||
// Get the image URL - handle different scenarios
|
||||
let imageUrl;
|
||||
if (item.thumbnail) {
|
||||
if (item.thumbnail.startsWith('http')) {
|
||||
// Full URL (external)
|
||||
imageUrl = item.thumbnail;
|
||||
} else if (item.thumbnail.startsWith('/media')) {
|
||||
// Relative path starting with /media
|
||||
imageUrl = `${API_CONFIG.BASE_URL}${item.thumbnail}`;
|
||||
} else {
|
||||
// Just filename or relative path
|
||||
imageUrl = `${API_CONFIG.MEDIA_URL}/${item.thumbnail}`;
|
||||
}
|
||||
console.log('Case study image URL:', item.title, '→', imageUrl);
|
||||
} else {
|
||||
// Fallback to static image
|
||||
imageUrl = getValidImageUrl('/images/case/one.png', FALLBACK_IMAGES.CASE_STUDY);
|
||||
console.log('Using fallback image for:', item.title);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
@@ -116,11 +158,11 @@ const Story = () => {
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={getValidImageUrl(item.image, FALLBACK_IMAGES.CASE_STUDY)}
|
||||
src={imageUrl}
|
||||
width={600}
|
||||
height={300}
|
||||
className="w-100 mh-300"
|
||||
alt="Image"
|
||||
alt={item.title || "Case Study"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/legacy/image";
|
||||
import gsap from "gsap";
|
||||
import ScrollTrigger from "gsap/dist/ScrollTrigger";
|
||||
import location from "@/public/images/footer/location.png";
|
||||
import phone from "@/public/images/footer/phone.png";
|
||||
import gmail from "@/public/images/footer/gmail.png";
|
||||
@@ -33,24 +30,6 @@ const Footer = () => {
|
||||
})
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
gsap.set(".foot-fade", {
|
||||
x: -100,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
ScrollTrigger.batch(".foot-fade", {
|
||||
start: "-100px bottom",
|
||||
onEnter: (elements) =>
|
||||
gsap.to(elements, {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
stagger: 0.3,
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Get logo URL from static data
|
||||
const logoSrc = headerData.logoUrl;
|
||||
|
||||
@@ -80,8 +59,8 @@ const Footer = () => {
|
||||
width={160}
|
||||
height={120}
|
||||
style={{
|
||||
height: 'auto',
|
||||
maxHeight: '120px'
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
@@ -132,7 +111,7 @@ const Footer = () => {
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Resources</h6>
|
||||
<ul className="footer-links">
|
||||
<li><Link href="blog">Software Blog</Link></li>
|
||||
<li><Link href="/insights">Software Insights</Link></li>
|
||||
<li><Link href="case-study">Development Resources</Link></li>
|
||||
<li><Link href="services">API Documentation</Link></li>
|
||||
<li><Link href="contact-us">Technical Support</Link></li>
|
||||
|
||||
@@ -155,7 +155,9 @@ const Header = () => {
|
||||
alt="Logo"
|
||||
width={160}
|
||||
height={120}
|
||||
priority
|
||||
style={{
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
}}
|
||||
/>
|
||||
|
||||
437
gnx-react/lib/api/blogService.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
import { API_CONFIG } from '../config/api';
|
||||
|
||||
// Types for Blog API
|
||||
export interface BlogAuthor {
|
||||
id: number;
|
||||
name: string;
|
||||
email?: string;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export interface BlogCategory {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
display_order: number;
|
||||
posts_count?: number;
|
||||
}
|
||||
|
||||
export interface BlogTag {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
content?: string;
|
||||
excerpt: string;
|
||||
thumbnail?: string;
|
||||
featured_image?: string;
|
||||
author?: BlogAuthor;
|
||||
author_name?: string;
|
||||
category?: BlogCategory;
|
||||
category_title?: string;
|
||||
category_slug?: string;
|
||||
tags?: BlogTag[];
|
||||
meta_description?: string;
|
||||
meta_keywords?: string;
|
||||
published: boolean;
|
||||
featured: boolean;
|
||||
views_count: number;
|
||||
reading_time: number;
|
||||
published_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
related_posts?: BlogPost[];
|
||||
}
|
||||
|
||||
export interface BlogPostListResponse {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: BlogPost[];
|
||||
}
|
||||
|
||||
export interface BlogComment {
|
||||
id: number;
|
||||
post: number;
|
||||
name: string;
|
||||
email: string;
|
||||
content: string;
|
||||
parent?: number;
|
||||
is_approved: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
replies?: BlogComment[];
|
||||
}
|
||||
|
||||
export interface BlogCommentCreateData {
|
||||
post: number;
|
||||
name: string;
|
||||
email: string;
|
||||
content: string;
|
||||
parent?: number;
|
||||
}
|
||||
|
||||
// Helper function to build query string
|
||||
const buildQueryString = (params: Record<string, any>): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
// Blog API functions
|
||||
export const blogService = {
|
||||
// Get all blog posts with optional filtering
|
||||
getPosts: async (params?: {
|
||||
category?: string;
|
||||
tag?: string;
|
||||
author?: number;
|
||||
search?: string;
|
||||
featured?: boolean;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<BlogPostListResponse> => {
|
||||
try {
|
||||
const queryString = params ? buildQueryString(params) : '';
|
||||
const url = `${API_CONFIG.BASE_URL}/api/blog/posts/${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog posts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get a single blog post by slug
|
||||
getPostBySlug: async (slug: string): Promise<BlogPost> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/${slug}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog post:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get featured blog posts
|
||||
getFeaturedPosts: async (): Promise<BlogPost[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/featured/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching featured posts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get latest blog posts
|
||||
getLatestPosts: async (limit: number = 5): Promise<BlogPost[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/latest/?limit=${limit}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching latest posts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get popular blog posts
|
||||
getPopularPosts: async (limit: number = 5): Promise<BlogPost[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/popular/?limit=${limit}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching popular posts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get related posts for a specific post
|
||||
getRelatedPosts: async (postSlug: string): Promise<BlogPost[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/${postSlug}/related/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching related posts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get all blog categories
|
||||
getCategories: async (): Promise<BlogCategory[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/categories/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : data.results || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog categories:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get categories with posts
|
||||
getCategoriesWithPosts: async (): Promise<BlogCategory[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/categories/with_posts/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories with posts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get a single category by slug
|
||||
getCategoryBySlug: async (slug: string): Promise<BlogCategory> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/categories/${slug}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog category:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get all blog tags
|
||||
getTags: async (): Promise<BlogTag[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/tags/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : data.results || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog tags:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get posts by tag
|
||||
getPostsByTag: async (tagSlug: string): Promise<BlogPostListResponse> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/tags/${tagSlug}/posts/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts by tag:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get all blog authors
|
||||
getAuthors: async (): Promise<BlogAuthor[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/authors/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : data.results || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog authors:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get posts by author
|
||||
getPostsByAuthor: async (authorId: number): Promise<BlogPostListResponse> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/authors/${authorId}/posts/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts by author:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get comments for a post
|
||||
getComments: async (postId: number): Promise<BlogComment[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/comments/?post=${postId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : data.results || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching comments:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Create a new comment
|
||||
createComment: async (commentData: BlogCommentCreateData): Promise<{ message: string; data: BlogComment }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/comments/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(commentData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error creating comment:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
375
gnx-react/lib/api/caseStudyService.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import { API_CONFIG } from '../config/api';
|
||||
|
||||
// Types for Case Study API
|
||||
export interface CaseStudyCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
display_order: number;
|
||||
case_studies_count?: number;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
logo?: string;
|
||||
description?: string;
|
||||
website?: string;
|
||||
}
|
||||
|
||||
export interface CaseStudyImage {
|
||||
id: number;
|
||||
image: string;
|
||||
caption?: string;
|
||||
display_order: number;
|
||||
}
|
||||
|
||||
export interface CaseStudyProcess {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
step_number: number;
|
||||
}
|
||||
|
||||
export interface CaseStudy {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
excerpt: string;
|
||||
thumbnail?: string;
|
||||
featured_image?: string;
|
||||
poster_image?: string;
|
||||
project_image?: string;
|
||||
project_overview?: string;
|
||||
site_map_content?: string;
|
||||
category?: CaseStudyCategory;
|
||||
category_name?: string;
|
||||
category_slug?: string;
|
||||
client?: Client;
|
||||
client_name?: string;
|
||||
gallery_images?: CaseStudyImage[];
|
||||
process_steps?: CaseStudyProcess[];
|
||||
meta_description?: string;
|
||||
meta_keywords?: string;
|
||||
published: boolean;
|
||||
featured: boolean;
|
||||
views_count: number;
|
||||
display_order: number;
|
||||
published_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
related_case_studies?: CaseStudy[];
|
||||
}
|
||||
|
||||
export interface CaseStudyListResponse {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: CaseStudy[];
|
||||
}
|
||||
|
||||
// Helper function to build query string
|
||||
const buildQueryString = (params: Record<string, any>): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
// Case Study API functions
|
||||
export const caseStudyService = {
|
||||
// Get all case studies with optional filtering
|
||||
getCaseStudies: async (params?: {
|
||||
category?: string;
|
||||
client?: string;
|
||||
search?: string;
|
||||
featured?: boolean;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<CaseStudyListResponse> => {
|
||||
try {
|
||||
const queryString = params ? buildQueryString(params) : '';
|
||||
const url = queryString
|
||||
? `${API_CONFIG.BASE_URL}/api/case-studies/case-studies/?${queryString}`
|
||||
: `${API_CONFIG.BASE_URL}/api/case-studies/case-studies/`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching case studies:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get a single case study by slug
|
||||
getCaseStudyBySlug: async (slug: string): Promise<CaseStudy> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/${slug}/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching case study ${slug}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get featured case studies
|
||||
getFeaturedCaseStudies: async (): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/featured/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching featured case studies:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get latest case studies
|
||||
getLatestCaseStudies: async (limit: number = 6): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/latest/?limit=${limit}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching latest case studies:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get popular case studies
|
||||
getPopularCaseStudies: async (limit: number = 6): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/popular/?limit=${limit}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching popular case studies:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get related case studies for a specific case study
|
||||
getRelatedCaseStudies: async (slug: string): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/${slug}/related/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching related case studies for ${slug}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get all categories
|
||||
getCategories: async (): Promise<CaseStudyCategory[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/categories/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching case study categories:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get categories with case studies
|
||||
getCategoriesWithCaseStudies: async (): Promise<CaseStudyCategory[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/categories/with_case_studies/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories with case studies:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get all clients
|
||||
getClients: async (): Promise<Client[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/clients/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching clients:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get a client by slug
|
||||
getClientBySlug: async (slug: string): Promise<Client> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/clients/${slug}/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching client ${slug}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get case studies for a specific client
|
||||
getClientCaseStudies: async (slug: string): Promise<CaseStudyListResponse> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/clients/${slug}/case_studies/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching case studies for client ${slug}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default caseStudyService;
|
||||
|
||||
@@ -9,6 +9,9 @@ export const API_CONFIG = {
|
||||
// Django API Base URL
|
||||
BASE_URL: API_BASE_URL,
|
||||
|
||||
// Media files URL (for uploaded images, etc.)
|
||||
MEDIA_URL: `${API_BASE_URL}/media`,
|
||||
|
||||
// API Endpoints
|
||||
ENDPOINTS: {
|
||||
CONTACT: '/api/contact',
|
||||
|
||||
250
gnx-react/lib/hooks/useBlog.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
blogService,
|
||||
BlogPost,
|
||||
BlogPostListResponse,
|
||||
BlogCategory,
|
||||
BlogTag,
|
||||
BlogAuthor
|
||||
} from '../api/blogService';
|
||||
|
||||
// Hook for fetching all blog posts
|
||||
export const useBlogPosts = (params?: {
|
||||
category?: string;
|
||||
tag?: string;
|
||||
author?: number;
|
||||
search?: string;
|
||||
featured?: boolean;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}) => {
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [pagination, setPagination] = useState<{
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
}>({ count: 0, next: null, previous: null });
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getPosts(params);
|
||||
setPosts(data.results);
|
||||
setPagination({
|
||||
count: data.count,
|
||||
next: data.next,
|
||||
previous: data.previous
|
||||
});
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setPosts([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPosts();
|
||||
}, [params?.category, params?.tag, params?.author, params?.search, params?.page, params?.page_size]);
|
||||
|
||||
return { posts, loading, error, pagination, totalCount: pagination.count };
|
||||
};
|
||||
|
||||
// Hook for fetching a single blog post by slug
|
||||
export const useBlogPost = (slug: string | null) => {
|
||||
const [post, setPost] = useState<BlogPost | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchPost = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getPostBySlug(slug);
|
||||
setPost(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setPost(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPost();
|
||||
}, [slug]);
|
||||
|
||||
return { post, loading, error };
|
||||
};
|
||||
|
||||
// Hook for fetching featured blog posts
|
||||
export const useFeaturedPosts = () => {
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFeaturedPosts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getFeaturedPosts();
|
||||
setPosts(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setPosts([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFeaturedPosts();
|
||||
}, []);
|
||||
|
||||
return { posts, loading, error };
|
||||
};
|
||||
|
||||
// Hook for fetching latest blog posts
|
||||
export const useLatestPosts = (limit: number = 5) => {
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLatestPosts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getLatestPosts(limit);
|
||||
setPosts(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setPosts([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLatestPosts();
|
||||
}, [limit]);
|
||||
|
||||
return { posts, loading, error };
|
||||
};
|
||||
|
||||
// Hook for fetching popular blog posts
|
||||
export const usePopularPosts = (limit: number = 5) => {
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPopularPosts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getPopularPosts(limit);
|
||||
setPosts(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setPosts([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPopularPosts();
|
||||
}, [limit]);
|
||||
|
||||
return { posts, loading, error };
|
||||
};
|
||||
|
||||
// Hook for fetching blog categories
|
||||
export const useBlogCategories = () => {
|
||||
const [categories, setCategories] = useState<BlogCategory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getCategories();
|
||||
setCategories(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setCategories([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
return { categories, loading, error };
|
||||
};
|
||||
|
||||
// Hook for fetching blog tags
|
||||
export const useBlogTags = () => {
|
||||
const [tags, setTags] = useState<BlogTag[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getTags();
|
||||
setTags(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setTags([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
return { tags, loading, error };
|
||||
};
|
||||
|
||||
// Hook for fetching blog authors
|
||||
export const useBlogAuthors = () => {
|
||||
const [authors, setAuthors] = useState<BlogAuthor[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAuthors = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await blogService.getAuthors();
|
||||
setAuthors(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
setAuthors([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAuthors();
|
||||
}, []);
|
||||
|
||||
return { authors, loading, error };
|
||||
};
|
||||
|
||||
351
gnx-react/lib/hooks/useCaseStudy.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import caseStudyService, {
|
||||
CaseStudy,
|
||||
CaseStudyCategory,
|
||||
Client,
|
||||
CaseStudyListResponse,
|
||||
} from '../api/caseStudyService';
|
||||
|
||||
// Hook to fetch all case studies
|
||||
export const useCaseStudies = (params?: {
|
||||
category?: string;
|
||||
client?: string;
|
||||
search?: string;
|
||||
featured?: boolean;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}) => {
|
||||
const [caseStudies, setCaseStudies] = useState<CaseStudy[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [pagination, setPagination] = useState<{
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCaseStudies = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getCaseStudies(params);
|
||||
setCaseStudies(data.results);
|
||||
setPagination({
|
||||
count: data.count,
|
||||
next: data.next,
|
||||
previous: data.previous,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error('Error fetching case studies:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCaseStudies();
|
||||
}, [
|
||||
params?.category,
|
||||
params?.client,
|
||||
params?.search,
|
||||
params?.featured,
|
||||
params?.ordering,
|
||||
params?.page,
|
||||
params?.page_size,
|
||||
]);
|
||||
|
||||
return { caseStudies, loading, error, pagination };
|
||||
};
|
||||
|
||||
// Hook to fetch a single case study by slug
|
||||
export const useCaseStudy = (slug: string | null) => {
|
||||
const [caseStudy, setCaseStudy] = useState<CaseStudy | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchCaseStudy = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getCaseStudyBySlug(slug);
|
||||
setCaseStudy(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error(`Error fetching case study ${slug}:`, err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCaseStudy();
|
||||
}, [slug]);
|
||||
|
||||
return { caseStudy, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch featured case studies
|
||||
export const useFeaturedCaseStudies = () => {
|
||||
const [featuredCaseStudies, setFeaturedCaseStudies] = useState<CaseStudy[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFeaturedCaseStudies = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getFeaturedCaseStudies();
|
||||
setFeaturedCaseStudies(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error('Error fetching featured case studies:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFeaturedCaseStudies();
|
||||
}, []);
|
||||
|
||||
return { featuredCaseStudies, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch latest case studies
|
||||
export const useLatestCaseStudies = (limit: number = 6) => {
|
||||
const [latestCaseStudies, setLatestCaseStudies] = useState<CaseStudy[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLatestCaseStudies = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getLatestCaseStudies(limit);
|
||||
setLatestCaseStudies(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error('Error fetching latest case studies:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLatestCaseStudies();
|
||||
}, [limit]);
|
||||
|
||||
return { latestCaseStudies, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch popular case studies
|
||||
export const usePopularCaseStudies = (limit: number = 6) => {
|
||||
const [popularCaseStudies, setPopularCaseStudies] = useState<CaseStudy[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPopularCaseStudies = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getPopularCaseStudies(limit);
|
||||
setPopularCaseStudies(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error('Error fetching popular case studies:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPopularCaseStudies();
|
||||
}, [limit]);
|
||||
|
||||
return { popularCaseStudies, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch related case studies
|
||||
export const useRelatedCaseStudies = (slug: string | null) => {
|
||||
const [relatedCaseStudies, setRelatedCaseStudies] = useState<CaseStudy[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchRelatedCaseStudies = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getRelatedCaseStudies(slug);
|
||||
setRelatedCaseStudies(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error(`Error fetching related case studies for ${slug}:`, err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRelatedCaseStudies();
|
||||
}, [slug]);
|
||||
|
||||
return { relatedCaseStudies, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch case study categories
|
||||
export const useCaseStudyCategories = () => {
|
||||
const [categories, setCategories] = useState<CaseStudyCategory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getCategories();
|
||||
setCategories(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error('Error fetching case study categories:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
return { categories, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch categories with case studies
|
||||
export const useCategoriesWithCaseStudies = () => {
|
||||
const [categories, setCategories] = useState<CaseStudyCategory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getCategoriesWithCaseStudies();
|
||||
setCategories(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error('Error fetching categories with case studies:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
return { categories, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch all clients
|
||||
export const useClients = () => {
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getClients();
|
||||
setClients(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error('Error fetching clients:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchClients();
|
||||
}, []);
|
||||
|
||||
return { clients, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch a single client by slug
|
||||
export const useClient = (slug: string | null) => {
|
||||
const [client, setClient] = useState<Client | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchClient = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getClientBySlug(slug);
|
||||
setClient(data);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error(`Error fetching client ${slug}:`, err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchClient();
|
||||
}, [slug]);
|
||||
|
||||
return { client, loading, error };
|
||||
};
|
||||
|
||||
// Hook to fetch case studies for a specific client
|
||||
export const useClientCaseStudies = (slug: string | null) => {
|
||||
const [caseStudies, setCaseStudies] = useState<CaseStudy[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchClientCaseStudies = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await caseStudyService.getClientCaseStudies(slug);
|
||||
setCaseStudies(data.results);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
console.error(`Error fetching case studies for client ${slug}:`, err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchClientCaseStudies();
|
||||
}, [slug]);
|
||||
|
||||
return { caseStudies, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Image utility functions
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
export const FALLBACK_IMAGES = {
|
||||
BLOG: '/images/blog/blog-poster.png',
|
||||
CASE_STUDY: '/images/case/poster.png',
|
||||
@@ -19,8 +21,22 @@ export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// If it starts with /, it's already a public path
|
||||
// If it starts with /media/, it's a Django media file - prepend API base URL
|
||||
if (imageUrl.startsWith('/media/')) {
|
||||
return `${API_BASE_URL}${imageUrl}`;
|
||||
}
|
||||
|
||||
// If it starts with /images/, it's a local public file
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// If it starts with /, check if it's a media file
|
||||
if (imageUrl.startsWith('/')) {
|
||||
// If it contains /media/, prepend API base URL
|
||||
if (imageUrl.includes('/media/')) {
|
||||
return `${API_BASE_URL}${imageUrl}`;
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
@@ -28,6 +44,9 @@ export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||
return `/${imageUrl}`;
|
||||
}
|
||||
|
||||
// Alias for backward compatibility
|
||||
export const getImageUrl = getValidImageUrl;
|
||||
|
||||
export function getImageAlt(title?: string, fallback: string = 'Image'): string {
|
||||
return title ? `${title} - Image` : fallback;
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ export const OffcanvasData = [
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "Blog",
|
||||
path: "/blog",
|
||||
title: "Insights",
|
||||
path: "/insights",
|
||||
parent_id: null,
|
||||
display_order: 5,
|
||||
submenu: null,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
// page-specific styles
|
||||
@use "./pages/about-enterprise";
|
||||
@use "./pages/support-center";
|
||||
@use "./pages/blog-single";
|
||||
|
||||
// responsive styles (CSS-only files) - using @use for modern Sass
|
||||
@use "./responsive";
|
||||
|
||||
692
gnx-react/public/styles/pages/_blog-single.scss
Normal file
@@ -0,0 +1,692 @@
|
||||
// Blog Single Page - Enhanced Enterprise Layout
|
||||
// =======================================================
|
||||
|
||||
.blog-single-section {
|
||||
padding-top: 140px;
|
||||
background: #f8f9fa;
|
||||
|
||||
@media (max-width: 991px) {
|
||||
padding-top: 120px;
|
||||
}
|
||||
|
||||
// Loading and Error States
|
||||
.loading-state,
|
||||
.error-state {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.spinner-border {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Article Styling
|
||||
.blog-single-article {
|
||||
background: #ffffff;
|
||||
border-top: 4px solid var(--color-primary);
|
||||
|
||||
// Article Header
|
||||
.article-header {
|
||||
padding: 50px 80px 40px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
padding: 40px 60px 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
padding: 30px 40px 25px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 25px 20px 20px;
|
||||
}
|
||||
|
||||
// Top Meta Bar
|
||||
.article-top-meta {
|
||||
.left-meta,
|
||||
.right-meta {
|
||||
@media (max-width: 767px) {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 18px;
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
|
||||
i {
|
||||
color: var(--color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Title
|
||||
.article-title {
|
||||
font-size: 52px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-secondary);
|
||||
margin: 30px 0 0;
|
||||
font-weight: 700;
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
font-size: 36px;
|
||||
margin: 25px 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
font-size: 28px;
|
||||
margin: 20px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom Meta Bar
|
||||
.article-bottom-meta {
|
||||
border-top: 1px solid #e9ecef;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.author-meta {
|
||||
.author-avatar {
|
||||
.avatar-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.author-info {
|
||||
.author-label {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-tags {
|
||||
@media (max-width: 767px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tag-badge {
|
||||
padding: 6px 14px;
|
||||
background: #f0f0f0;
|
||||
color: var(--color-secondary);
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: #ffffff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Featured Image
|
||||
.article-featured-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, transparent 70%, rgba(0, 0, 0, 0.05) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Article Body
|
||||
.article-body {
|
||||
padding: 60px 80px;
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
padding: 50px 60px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
padding: 40px 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Excerpt
|
||||
.article-excerpt {
|
||||
padding: 35px 45px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-left: 5px solid var(--color-primary);
|
||||
margin-bottom: 50px;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 25px 20px;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 22px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-secondary);
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Article Content
|
||||
.article-content {
|
||||
font-size: 18px;
|
||||
line-height: 1.8;
|
||||
color: #333333;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--color-secondary);
|
||||
margin-top: 60px;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 3px solid var(--color-primary);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
font-size: 28px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--color-secondary);
|
||||
margin-top: 45px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
font-size: 24px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--color-secondary);
|
||||
margin-top: 35px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 22px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 28px 0;
|
||||
padding-left: 35px;
|
||||
|
||||
li {
|
||||
margin-bottom: 14px;
|
||||
color: #333333;
|
||||
|
||||
&::marker {
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 40px 0;
|
||||
padding: 30px 40px;
|
||||
background: #f8f9fa;
|
||||
border-left: 6px solid var(--color-primary);
|
||||
font-style: italic;
|
||||
color: var(--color-secondary);
|
||||
font-size: 19px;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 25px 20px;
|
||||
margin: 30px 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 4px 10px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 15px;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 30px 0;
|
||||
padding: 25px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #f8f8f2;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 40px 0;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 30px 0;
|
||||
border-collapse: collapse;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
thead {
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
||||
color: #ffffff;
|
||||
|
||||
th {
|
||||
padding: 18px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Article Footer
|
||||
.article-footer {
|
||||
background: #f8f9fa;
|
||||
padding: 50px 80px 60px;
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
padding: 40px 60px 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
padding: 35px 40px 45px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 30px 20px 35px;
|
||||
}
|
||||
}
|
||||
|
||||
// Share Section
|
||||
.article-share {
|
||||
margin-bottom: 50px;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.share-container {
|
||||
.share-title {
|
||||
font-size: 20px;
|
||||
color: var(--color-secondary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.social-share {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.share-btn {
|
||||
min-width: 140px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.share-linkedin {
|
||||
background: #0077b5;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background: #ffffff;
|
||||
color: #0077b5;
|
||||
border-color: #0077b5;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(0, 119, 181, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&.share-twitter {
|
||||
background: #1da1f2;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background: #ffffff;
|
||||
color: #1da1f2;
|
||||
border-color: #1da1f2;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(29, 161, 242, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&.share-facebook {
|
||||
background: #1877f2;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background: #ffffff;
|
||||
color: #1877f2;
|
||||
border-color: #1877f2;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(24, 119, 242, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&.share-copy {
|
||||
background: #6c757d;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background: #ffffff;
|
||||
color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(108, 117, 125, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Author Bio Section
|
||||
.author-bio-section {
|
||||
margin-bottom: 45px;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.author-bio-card {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
padding: 35px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.author-avatar-container {
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.author-avatar-large {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.author-bio-content {
|
||||
flex: 1;
|
||||
|
||||
.bio-label {
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 22px;
|
||||
color: var(--color-secondary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.author-bio-text {
|
||||
font-size: 16px;
|
||||
color: #6c757d;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
.article-navigation {
|
||||
text-align: center;
|
||||
|
||||
.btn-back-insights {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 14px 35px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: #ffffff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Adjustments
|
||||
@media (max-width: 767px) {
|
||||
.blog-single-section {
|
||||
.blog-single-article {
|
||||
.article-header {
|
||||
.article-top-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 12px !important;
|
||||
|
||||
.left-meta,
|
||||
.right-meta {
|
||||
gap: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-share {
|
||||
.share-container {
|
||||
.social-share {
|
||||
.share-btn {
|
||||
min-width: calc(50% - 8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||