MSC Center lΓ nα»n tαΊ£ng giΓ‘o dα»₯c trα»±c tuyαΊΏn hΓ ng ΔαΊ§u Viα»t Nam, chuyΓͺn cung cαΊ₯p cΓ‘c khΓ³a hα»c chαΊ₯t lượng cao vα» cΓ΄ng nghα», kinh doanh vΓ phΓ‘t triα»n cΓ‘ nhΓ’n. Vα»i slogan "Life Long Learning", chΓΊng tΓ΄i cam kαΊΏt Δα»ng hΓ nh cΓΉng hα»c viΓͺn trong suα»t hΓ nh trΓ¬nh hα»c tαΊp vΓ phΓ‘t triα»n sα»± nghiα»p.
- XΓ’y dα»±ng nα»n tαΊ£ng giΓ‘o dα»₯c trα»±c tuyαΊΏn hiα»n ΔαΊ‘i vΓ thΓ’n thiα»n
- KαΊΏt nα»i hα»c viΓͺn vα»i cΓ‘c mentor hΓ ng ΔαΊ§u trong ngΓ nh
- Cung cαΊ₯p nα»i dung chαΊ₯t lượng cao vΓ cαΊp nhαΊt liΓͺn tα»₯c
- TαΊ‘o cα»ng Δα»ng hα»c tαΊp tΓch cα»±c vΓ hα» trợ lαΊ«n nhau
- π Modern UI/UX: ThiαΊΏt kαΊΏ hiα»n ΔαΊ‘i vα»i Tailwind CSS vΓ shadcn/ui
- π Animations: Hiα»u α»©ng mượt mΓ vα»i Framer Motion
- π± Responsive Design: Tα»i Ζ°u cho mα»i thiαΊΏt bα» (Mobile, Tablet, Desktop)
- π Dark Mode: ChαΊΏ Δα» tα»i/sΓ‘ng vα»i theme switching
- π Multilingual: Hα» trợ Δa ngΓ΄n ngα»― (TiαΊΏng Viα»t/English)
- β‘ Performance: Tα»i Ζ°u tα»c Δα» tαΊ£i vΓ SEO
- βΏ Accessibility: TuΓ’n thα»§ chuαΊ©n WCAG 2.1
- π Microservices: KiαΊΏn trΓΊc microservices vα»i Golang vΓ Rust
- ποΈ Database: PostgreSQL vα»i Redis caching
- π Authentication: JWT-based authentication system
- π§ Email Service: Automated email notifications
- π Analytics: Real-time analytics vΓ reporting
- π Search: Full-text search vα»i Elasticsearch
- π€ File Upload: Cloud storage integration
- π° Blog System: Hα» thα»ng blog vα»i rich text editor
- π¨βπ« Mentor Profiles: QuαΊ£n lΓ½ thΓ΄ng tin mentor chi tiαΊΏt
- π Course Management: QuαΊ£n lΓ½ khΓ³a hα»c vΓ bΓ i giαΊ£ng
- π¬ Comments: Hα» thα»ng bΓ¬nh luαΊn vΓ tΖ°Ζ‘ng tΓ‘c
- π Analytics: Theo dΓ΅i engagement vΓ performance
```json { "frontend": { "framework": "Next.js 14 (App Router)", "language": "TypeScript 5.0", "styling": "Tailwind CSS 3.4", "ui_components": "shadcn/ui", "animations": "Framer Motion 11.0", "icons": "Lucide React", "forms": "React Hook Form + Zod", "state_management": "Zustand", "http_client": "Axios" }, "backend": { "api_gateway": "Golang (Gin Framework)", "analytics_service": "Rust (Warp Framework)", "database": "PostgreSQL 15", "cache": "Redis 7", "search": "Elasticsearch 8", "message_queue": "RabbitMQ", "file_storage": "AWS S3 / Cloudinary" }, "cms": { "headless_cms": "Sanity 3.0", "rich_text": "Portable Text", "media_management": "Sanity Assets", "preview": "Live Preview" }, "devops": { "containerization": "Docker & Docker Compose", "orchestration": "Kubernetes", "ci_cd": "GitHub Actions", "monitoring": "Prometheus + Grafana", "logging": "ELK Stack" } } ```
- Node.js: >= 18.0.0
- npm/yarn/pnpm: Latest version
- PostgreSQL: >= 15.0
- Redis: >= 7.0
- Docker: >= 24.0 (optional)
- Go: >= 1.21 (for backend services)
- Rust: >= 1.75 (for analytics service)
```bash git clone https://github.com/msc-center/msc-website.git cd msc-website ```
```bash
npm install
yarn install
pnpm install
cd backend-go go mod download cd ..
cd backend-rust cargo build cd .. ```
```bash
cp .env.example .env.local
```
```bash
createdb msc_center
npm run db:migrate
npm run db:seed ```
```bash
npm run dev
cd backend-go && go run main.go
cd backend-rust && cargo run
docker-compose up -dev ```
- Frontend: http://localhost:3000
- Go API: http://localhost:8080
- Rust Analytics: http://localhost:3001
- Sanity Studio: http://localhost:3333
``` msc-center-website/ βββ π app/ # Next.js App Router β βββ π (auth)/ # Auth routes group β β βββ π login/ β β βββ π register/ β βββ π api/ # API routes β β βββ π auth/ β β βββ π blog/ β β βββ π contact/ β βββ π chia-se/ # Blog pages β β βββ π [slug]/ # Dynamic blog post β β βββ π category/ β βββ π mentors/ # Mentor pages β β βββ π [id]/ # Dynamic mentor profile β βββ π mscer/ # MSCer pages β β βββ π [id]/ # Dynamic MSCer profile β βββ π profile/ # User profile dashboard β β βββ π courses/ # Course management β β βββ π settings/ # User settings β β βββ π feedback/ # Feedback system β βββ π globals.css # Global styles β βββ π layout.tsx # Root layout β βββ π page.tsx # Homepage β βββ π not-found.tsx # 404 page βββ π components/ # React components β βββ π ui/ # shadcn/ui components β β βββ π button.tsx β β βββ π card.tsx β β βββ π input.tsx β β βββ π progress.tsx β β βββ π chart.tsx β β βββ π ... β βββ π sections/ # Page sections β β βββ π HeroVideo.tsx β β βββ π ProjectsSection.tsx β β βββ π MentorsSection.tsx β β βββ π ... β βββ π profile/ # Profile components β β βββ π ProfileDashboard.tsx β β βββ π CourseProgress.tsx β β βββ π SettingsPanel.tsx β β βββ π FeedbackForm.tsx β βββ π Header.tsx # Site header β βββ π Footer.tsx # Site footer β βββ π FloatingButtons.tsx # Floating action buttons β βββ π Chatbot.tsx # AI chatbot βββ π data/ # Static data β βββ π mentors.ts # Mentors database β βββ π blog-posts.ts # Blog posts data β βββ π projects.ts # Projects data β βββ π courses.ts # Courses data βββ π lib/ # Utility libraries β βββ π utils.ts # Common utilities β βββ π sanity.ts # Sanity client β βββ π database.ts # Database connection β βββ π auth.ts # Authentication β βββ π analytics.ts # Analytics tracking βββ π hooks/ # Custom React hooks β βββ π use-theme.ts # Theme management β βββ π use-language.ts # Language switching β βββ π use-mobile.ts # Mobile detection β βββ π use-user.ts # User data management βββ π backend-go/ # Golang microservices β βββ π cmd/ # Application entrypoints β βββ π internal/ # Internal packages β β βββ π api/ # API handlers β β βββ π auth/ # Authentication β β βββ π database/ # Database layer β β βββ π models/ # Data models β βββ π pkg/ # Public packages β βββ π go.mod # Go modules β βββ π go.sum # Dependencies checksum β βββ π main.go # Main application βββ π backend-rust/ # Rust analytics service β βββ π src/ # Source code β β βββ π handlers/ # Request handlers β β βββ π models/ # Data models β β βββ π services/ # Business logic β β βββ π main.rs # Main application β βββ π Cargo.toml # Rust dependencies β βββ π Cargo.lock # Dependencies lock βββ π sanity/ # Sanity CMS β βββ π schemas/ # Content schemas β β βββ π post.ts # Blog post schema β β βββ π mentor.ts # Mentor schema β β βββ π course.ts # Course schema β β βββ π author.ts # Author schema β βββ π sanity.config.ts # Sanity configuration β βββ π sanity.cli.ts # CLI configuration βββ π public/ # Static assets β βββ π images/ # Images β βββ π videos/ # Videos β βββ π icons/ # Icons β βββ π favicon.ico # Favicon βββ π scripts/ # Utility scripts β βββ π setup.sh # Project setup β βββ π deploy.sh # Deployment script β βββ π migrate.sh # Database migration βββ π docs/ # Documentation β βββ π API.md # API documentation β βββ π DEPLOYMENT.md # Deployment guide β βββ π CONTRIBUTING.md # Contributing guide βββ π package.json # Node.js dependencies βββ π tsconfig.json # TypeScript configuration βββ π tailwind.config.ts # Tailwind configuration βββ π next.config.mjs # Next.js configuration βββ π docker-compose.yml # Docker services βββ π Dockerfile # Docker image βββ π .env.example # Environment template βββ π .gitignore # Git ignore rules βββ π README.md # This file ```
```typescript // POST /api/auth/login interface LoginRequest { email: string; password: string; }
interface LoginResponse { user: User; token: string; refreshToken: string; }
// POST /api/auth/register interface RegisterRequest { email: string; password: string; fullName: string; phone?: string; }
// POST /api/auth/refresh interface RefreshRequest { refreshToken: string; }
// GET /api/auth/me interface UserResponse { id: string; email: string; fullName: string; avatar?: string; role: string; profile: UserProfile; } ```
```typescript // GET /api/profile interface ProfileResponse { user: User; stats: { coursesCompleted: number; coursesInProgress: number; totalLearningHours: number; certificatesEarned: number; skillsAcquired: number; }; recentActivity: Activity[]; achievements: Achievement[]; }
// PUT /api/profile interface UpdateProfileRequest { fullName?: string; bio?: string; location?: string; website?: string; socialLinks?: SocialLinks; skills?: string[]; interests?: string[]; }
// POST /api/profile/avatar interface UploadAvatarRequest { file: File; } ```
```typescript // GET /api/courses/enrolled interface EnrolledCoursesResponse { courses: EnrolledCourse[]; pagination: Pagination; }
interface EnrolledCourse { id: string; title: string; instructor: string; progress: number; completedLessons: number; totalLessons: number; lastAccessed: string; certificate?: Certificate; }
// GET /api/courses/[id]/progress interface CourseProgressResponse { courseId: string; progress: number; completedLessons: Lesson[]; currentLesson?: Lesson; timeSpent: number; quiz_scores: QuizScore[]; }
// POST /api/courses/[id]/complete-lesson interface CompleteLessonRequest { lessonId: string; timeSpent: number; notes?: string; } ```
```typescript // POST /api/feedback interface FeedbackRequest { type: 'bug' | 'feature' | 'improvement' | 'general'; category: string; title: string; description: string; priority: 'low' | 'medium' | 'high' | 'urgent'; attachments?: File[]; }
// GET /api/feedback/my-feedback interface MyFeedbackResponse { feedback: Feedback[]; pagination: Pagination; }
interface Feedback { id: string; type: string; title: string; status: 'open' | 'in-progress' | 'resolved' | 'closed'; createdAt: string; updatedAt: string; responses: FeedbackResponse[]; } ```
```sql CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, full_name VARCHAR(255) NOT NULL, avatar_url TEXT, role VARCHAR(50) DEFAULT 'user', email_verified BOOLEAN DEFAULT false, phone VARCHAR(20), bio TEXT, location VARCHAR(255), website VARCHAR(255), date_of_birth DATE, gender VARCHAR(10), occupation VARCHAR(255), company VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, is_active BOOLEAN DEFAULT true ); ```
```sql CREATE TABLE user_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, skills TEXT[], interests TEXT[], learning_goals TEXT[], experience_level VARCHAR(50), preferred_language VARCHAR(10) DEFAULT 'vi', timezone VARCHAR(50), social_links JSONB DEFAULT '{}', privacy_settings JSONB DEFAULT '{}', notification_preferences JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ```
```sql CREATE TABLE courses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(500) NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, description TEXT, instructor_id UUID REFERENCES users(id), category VARCHAR(100), level VARCHAR(50), duration_hours INTEGER, price DECIMAL(10,2), thumbnail_url TEXT, trailer_url TEXT, is_published BOOLEAN DEFAULT false, enrollment_count INTEGER DEFAULT 0, rating DECIMAL(3,2) DEFAULT 0, review_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ```
```sql CREATE TABLE course_enrollments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, course_id UUID REFERENCES courses(id) ON DELETE CASCADE, enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMP, progress DECIMAL(5,2) DEFAULT 0, last_accessed TIMESTAMP, time_spent INTEGER DEFAULT 0, -- in minutes certificate_issued BOOLEAN DEFAULT false, certificate_url TEXT, UNIQUE(user_id, course_id) ); ```
```sql CREATE TABLE lessons ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), course_id UUID REFERENCES courses(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, slug VARCHAR(255) NOT NULL, content TEXT, video_url TEXT, duration_minutes INTEGER, order_index INTEGER, is_preview BOOLEAN DEFAULT false, resources JSONB DEFAULT '[]', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ```
```sql CREATE TABLE lesson_progress ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, lesson_id UUID REFERENCES lessons(id) ON DELETE CASCADE, completed BOOLEAN DEFAULT false, completed_at TIMESTAMP, time_spent INTEGER DEFAULT 0, -- in minutes notes TEXT, last_position INTEGER DEFAULT 0, -- video position in seconds UNIQUE(user_id, lesson_id) ); ```
```sql CREATE TABLE user_achievements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, achievement_type VARCHAR(100) NOT NULL, achievement_name VARCHAR(255) NOT NULL, description TEXT, icon_url TEXT, earned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, metadata JSONB DEFAULT '{}' ); ```
```sql CREATE TABLE feedback ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(50) NOT NULL, category VARCHAR(100), title VARCHAR(500) NOT NULL, description TEXT NOT NULL, priority VARCHAR(20) DEFAULT 'medium', status VARCHAR(50) DEFAULT 'open', assigned_to UUID REFERENCES users(id), attachments TEXT[], metadata JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ```
```sql CREATE TABLE feedback_responses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), feedback_id UUID REFERENCES feedback(id) ON DELETE CASCADE, responder_id UUID REFERENCES users(id), message TEXT NOT NULL, is_internal BOOLEAN DEFAULT false, attachments TEXT[], created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ```
```sql -- Users indexes CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_role ON users(role); CREATE INDEX idx_users_is_active ON users(is_active);
-- Course enrollments indexes CREATE INDEX idx_enrollments_user_id ON course_enrollments(user_id); CREATE INDEX idx_enrollments_course_id ON course_enrollments(course_id); CREATE INDEX idx_enrollments_progress ON course_enrollments(progress);
-- Lesson progress indexes CREATE INDEX idx_lesson_progress_user_id ON lesson_progress(user_id); CREATE INDEX idx_lesson_progress_lesson_id ON lesson_progress(lesson_id); CREATE INDEX idx_lesson_progress_completed ON lesson_progress(completed);
-- Feedback indexes CREATE INDEX idx_feedback_user_id ON feedback(user_id); CREATE INDEX idx_feedback_status ON feedback(status); CREATE INDEX idx_feedback_type ON feedback(type); CREATE INDEX idx_feedback_created_at ON feedback(created_at DESC);
-- Achievements indexes CREATE INDEX idx_achievements_user_id ON user_achievements(user_id); CREATE INDEX idx_achievements_type ON user_achievements(achievement_type); ```
```bash
npm install -g @sanity/cli
sanity init
```
```typescript // sanity/schemas/post.ts import { defineField, defineType } from 'sanity'
export default defineType({
name: 'post',
title: 'BΓ i viαΊΏt',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'TiΓͺu Δα»',
type: 'string',
validation: Rule => Rule.required().max(100)
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
validation: Rule => Rule.required()
}),
defineField({
name: 'author',
title: 'TΓ‘c giαΊ£',
type: 'reference',
to: { type: 'author' },
validation: Rule => Rule.required()
}),
defineField({
name: 'mainImage',
title: 'αΊ’nh ΔαΊ‘i diα»n',
type: 'image',
options: {
hotspot: true,
},
fields: [
{
name: 'alt',
type: 'string',
title: 'Alt text',
}
]
}),
defineField({
name: 'categories',
title: 'Danh mα»₯c',
type: 'array',
of: [{ type: 'reference', to: { type: 'category' } }],
}),
defineField({
name: 'publishedAt',
title: 'NgΓ y xuαΊ₯t bαΊ£n',
type: 'datetime',
validation: Rule => Rule.required()
}),
defineField({
name: 'excerpt',
title: 'TΓ³m tαΊ―t',
type: 'text',
rows: 4,
validation: Rule => Rule.max(200)
}),
defineField({
name: 'body',
title: 'Nα»i dung',
type: 'blockContent',
validation: Rule => Rule.required()
}),
defineField({
name: 'tags',
title: 'Tags',
type: 'array',
of: [{ type: 'string' }],
options: {
layout: 'tags'
}
}),
defineField({
name: 'featured',
title: 'BΓ i viαΊΏt nα»i bαΊt',
type: 'boolean',
initialValue: false
}),
defineField({
name: 'readingTime',
title: 'Thα»i gian Δα»c (phΓΊt)',
type: 'number',
validation: Rule => Rule.min(1).max(60)
}),
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare(selection) {
const { author } = selection
return { ...selection, subtitle: author && by ${author} }
},
},
})
```
```typescript // sanity/schemas/mentor.ts import { defineField, defineType } from 'sanity'
export default defineType({ name: 'mentor', title: 'Mentor', type: 'document', fields: [ defineField({ name: 'name', title: 'Hα» vΓ tΓͺn', type: 'string', validation: Rule => Rule.required() }), defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'name', maxLength: 96, }, validation: Rule => Rule.required() }), defineField({ name: 'title', title: 'Chα»©c danh', type: 'string', validation: Rule => Rule.required() }), defineField({ name: 'company', title: 'CΓ΄ng ty', type: 'string', }), defineField({ name: 'avatar', title: 'αΊ’nh ΔαΊ‘i diα»n', type: 'image', options: { hotspot: true, }, validation: Rule => Rule.required() }), defineField({ name: 'bio', title: 'Tiα»u sα»', type: 'blockContent', }), defineField({ name: 'experience', title: 'Kinh nghiα»m lΓ m viα»c', type: 'array', of: [ { type: 'object', fields: [ { name: 'company', type: 'string', title: 'CΓ΄ng ty' }, { name: 'position', type: 'string', title: 'Vα» trΓ' }, { name: 'duration', type: 'string', title: 'Thα»i gian' }, { name: 'description', type: 'text', title: 'MΓ΄ tαΊ£ cΓ΄ng viα»c' }, ] } ] }), defineField({ name: 'education', title: 'Hα»c vαΊ₯n', type: 'array', of: [ { type: 'object', fields: [ { name: 'school', type: 'string', title: 'TrΖ°α»ng' }, { name: 'degree', type: 'string', title: 'BαΊ±ng cαΊ₯p' }, { name: 'field', type: 'string', title: 'ChuyΓͺn ngΓ nh' }, { name: 'year', type: 'string', title: 'NΔm tα»t nghiα»p' }, ] } ] }), defineField({ name: 'skills', title: 'Kα»Ή nΔng', type: 'array', of: [{ type: 'string' }], options: { layout: 'tags' } }), defineField({ name: 'achievements', title: 'ThΓ nh tα»±u', type: 'array', of: [{ type: 'string' }], }), defineField({ name: 'socialLinks', title: 'LiΓͺn kαΊΏt xΓ£ hα»i', type: 'object', fields: [ { name: 'linkedin', type: 'url', title: 'LinkedIn' }, { name: 'github', type: 'url', title: 'GitHub' }, { name: 'website', type: 'url', title: 'Website' }, { name: 'twitter', type: 'url', title: 'Twitter' }, ] }), defineField({ name: 'specialties', title: 'ChuyΓͺn mΓ΄n', type: 'array', of: [{ type: 'string' }], options: { layout: 'tags' } }), defineField({ name: 'hourlyRate', title: 'GiΓ‘ theo giα» (USD)', type: 'number', }), defineField({ name: 'isActive', title: 'Δang hoαΊ‘t Δα»ng', type: 'boolean', initialValue: true }), ], preview: { select: { title: 'name', subtitle: 'title', media: 'avatar', }, }, }) ```
```typescript // sanity/schemas/course.ts import { defineField, defineType } from 'sanity'
export default defineType({
name: 'course',
title: 'KhΓ³a hα»c',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'TiΓͺu Δα» khΓ³a hα»c',
type: 'string',
validation: Rule => Rule.required().max(200)
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
validation: Rule => Rule.required()
}),
defineField({
name: 'description',
title: 'MΓ΄ tαΊ£',
type: 'text',
rows: 4,
validation: Rule => Rule.required().max(500)
}),
defineField({
name: 'instructor',
title: 'GiαΊ£ng viΓͺn',
type: 'reference',
to: { type: 'mentor' },
validation: Rule => Rule.required()
}),
defineField({
name: 'thumbnail',
title: 'αΊ’nh thumbnail',
type: 'image',
options: {
hotspot: true,
},
validation: Rule => Rule.required()
}),
defineField({
name: 'trailer',
title: 'Video giα»i thiα»u',
type: 'url',
}),
defineField({
name: 'category',
title: 'Danh mα»₯c',
type: 'reference',
to: { type: 'category' },
validation: Rule => Rule.required()
}),
defineField({
name: 'level',
title: 'CαΊ₯p Δα»',
type: 'string',
options: {
list: [
{ title: 'CΖ‘ bαΊ£n', value: 'beginner' },
{ title: 'Trung cαΊ₯p', value: 'intermediate' },
{ title: 'NΓ’ng cao', value: 'advanced' },
]
},
validation: Rule => Rule.required()
}),
defineField({
name: 'duration',
title: 'Thα»i lượng (giα»)',
type: 'number',
validation: Rule => Rule.required().min(1).max(200)
}),
defineField({
name: 'price',
title: 'GiΓ‘ (VND)',
type: 'number',
validation: Rule => Rule.required().min(0)
}),
defineField({
name: 'curriculum',
title: 'ChΖ°Ζ‘ng trΓ¬nh hα»c',
type: 'array',
of: [
{
type: 'object',
fields: [
{ name: 'title', type: 'string', title: 'TiΓͺu Δα» bΓ i hα»c' },
{ name: 'duration', type: 'number', title: 'Thα»i lượng (phΓΊt)' },
{ name: 'isPreview', type: 'boolean', title: 'Xem trΖ°α»c miα»
n phΓ' },
{ name: 'videoUrl', type: 'url', title: 'Link video' },
{ name: 'resources', type: 'array', of: [{ type: 'url' }], title: 'TΓ i liα»u' },
]
}
]
}),
defineField({
name: 'skills',
title: 'Kα»Ή nΔng hα»c Δược',
type: 'array',
of: [{ type: 'string' }],
options: {
layout: 'tags'
}
}),
defineField({
name: 'requirements',
title: 'YΓͺu cαΊ§u',
type: 'array',
of: [{ type: 'string' }],
}),
defineField({
name: 'isPublished',
title: 'ΔΓ£ xuαΊ₯t bαΊ£n',
type: 'boolean',
initialValue: false
}),
defineField({
name: 'isFeatured',
title: 'KhΓ³a hα»c nα»i bαΊt',
type: 'boolean',
initialValue: false
}),
],
preview: {
select: {
title: 'title',
instructor: 'instructor.name',
media: 'thumbnail',
},
prepare(selection) {
const { instructor } = selection
return { ...selection, subtitle: instructor && by ${instructor} }
},
},
})
```
```typescript // lib/sanity.ts import { createClient } from '@sanity/client' import imageUrlBuilder from '@sanity/image-url'
export const client = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, apiVersion: '2024-01-01', useCdn: process.env.NODE_ENV === 'production', token: process.env.SANITY_API_TOKEN, })
const builder = imageUrlBuilder(client)
export const urlFor = (source: any) => builder.image(source)
// GROQ Queries
export const coursesQuery = *[_type == "course" && isPublished == true] | order(_createdAt desc) { _id, title, slug, description, instructor->{name, avatar, title}, thumbnail, category->{title, slug}, level, duration, price, skills, requirements, isFeatured }
export const courseBySlugQuery = *[_type == "course" && slug.current == $slug][0] { _id, title, slug, description, instructor->{name, avatar, title, bio}, thumbnail, trailer, category->{title, slug}, level, duration, price, curriculum, skills, requirements }
export const featuredCoursesQuery = *[_type == "course" && isFeatured == true && isPublished == true] | order(_createdAt desc) [0...6] { _id, title, slug, description, instructor->{name, avatar}, thumbnail, level, duration, price }
```
```dockerfile
FROM node:18-alpine AS base
FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN
if [ -f yarn.lock ]; then yarn --frozen-lockfile;
elif [ -f package-lock.json ]; then npm ci;
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile;
else echo "Lockfile not found." && exit 1;
fi
FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . .
ARG NEXT_PUBLIC_SANITY_PROJECT_ID ARG NEXT_PUBLIC_SANITY_DATASET ARG DATABASE_URL ARG NEXTAUTH_SECRET ARG NEXTAUTH_URL
ENV NEXT_PUBLIC_SANITY_PROJECT_ID=$NEXT_PUBLIC_SANITY_PROJECT_ID ENV NEXT_PUBLIC_SANITY_DATASET=$NEXT_PUBLIC_SANITY_DATASET ENV DATABASE_URL=$DATABASE_URL ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET ENV NEXTAUTH_URL=$NEXTAUTH_URL
RUN yarn build
FROM base AS runner WORKDIR /app
ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000 ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"] ```
```yaml
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SANITY_PROJECT_ID:
db: image: postgres:15-alpine environment: POSTGRES_DB: msc_center POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_INITDB_ARGS: "--encoding=UTF-8" volumes: - postgres_data:/var/lib/postgresql/data - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql ports: - "5432:5432" networks: - msc-network
redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data ports: - "6379:6379" networks: - msc-network
backend-go: build: context: ./backend-go dockerfile: Dockerfile ports: - "8080:8080" environment: - DATABASE_URL=postgresql://postgres:password@db:5432/msc_center - REDIS_URL=redis://redis:6379 - JWT_SECRET=${JWT_SECRET} - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT} - SMTP_USER=${SMTP_USER} - SMTP_PASS=${SMTP_PASS} depends_on: - db - redis networks: - msc-network
backend-rust: build: context: ./backend-rust dockerfile: Dockerfile ports: - "3001:3001" environment: - DATABASE_URL=postgresql://postgres:password@db:5432/msc_center - REDIS_URL=redis://redis:6379 depends_on: - db - redis networks: - msc-network
nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/ssl:/etc/nginx/ssl depends_on: - frontend - backend-go - backend-rust networks: - msc-network
volumes: postgres_data: redis_data:
networks: msc-network: driver: bridge ```
```bash
DATABASE_URL="postgresql://username:password@localhost:5432/msc_center" REDIS_URL="redis://localhost:6379"
NEXTAUTH_SECRET="your-secret-key-here" NEXTAUTH_URL="http://localhost:3000"
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id" NEXT_PUBLIC_SANITY_DATASET="production" SANITY_API_TOKEN="your-api-token"
SMTP_HOST="smtp.gmail.com" SMTP_PORT="587" SMTP_USER="[email protected]" SMTP_PASS="your-app-password"
CLOUDINARY_CLOUD_NAME="your-cloud-name" CLOUDINARY_API_KEY="your-api-key" CLOUDINARY_API_SECRET="your-api-secret"
GOOGLE_ANALYTICS_ID="GA_MEASUREMENT_ID" FACEBOOK_PIXEL_ID="your-pixel-id"
GOOGLE_CLIENT_ID="your-google-client-id" GOOGLE_CLIENT_SECRET="your-google-client-secret" FACEBOOK_CLIENT_ID="your-facebook-client-id" FACEBOOK_CLIENT_SECRET="your-facebook-client-secret"
JWT_SECRET="your-jwt-secret" ENCRYPTION_KEY="your-encryption-key"
OPENAI_API_KEY="your-openai-api-key" STRIPE_SECRET_KEY="your-stripe-secret-key" STRIPE_PUBLISHABLE_KEY="your-stripe-publishable-key" ```
```bash
npm run test
npm run test:watch
npm run test:coverage
npm run test -- components/Header.test.tsx ```
```bash
npm run test:e2e
npm run test:e2e:headed
npm run test:e2e -- tests/auth.spec.ts ```
- Error Tracking: Sentry integration
- Performance: New Relic / DataDog
- Uptime: Pingdom / UptimeRobot
- Logs: ELK Stack (Elasticsearch, Logstash, Kibana)
- Web Analytics: Google Analytics 4
- User Behavior: Hotjar / FullStory
- A/B Testing: Optimizely / VWO
- Conversion Tracking: Facebook Pixel
- HTTPS: SSL/TLS encryption
- Authentication: JWT with refresh tokens
- Authorization: Role-based access control (RBAC)
- Input Validation: Zod schema validation
- SQL Injection: Parameterized queries
- XSS Protection: Content Security Policy (CSP)
- CSRF Protection: SameSite cookies
- Rate Limiting: Express rate limit
- Data Encryption: bcrypt for passwords
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
- TypeScript: Strict mode enabled
- ESLint: Airbnb configuration with custom rules
- Prettier: Code formatting with custom config
- Husky: Pre-commit hooks for linting and testing
- Conventional Commits: Standardized commit messages
- Semantic Versioning: Version management
- Documentation: Check this README and docs folder
- Issues: Create a GitHub issue for bugs
- Discussions: Use GitHub Discussions for questions
- Email: [email protected]
- Website: msc.edu.vn
- Email: [email protected]
- Phone: +84 (0) 123 456 789
- Address: 123 Nguyen Hue, District 1, Ho Chi Minh City, Vietnam
This project is licensed under the MIT License - see the LICENSE file for details.