| name | eddication |
| description | World-class expert across full-stack, frontend, TypeScript, Python, Google Apps Script, testing, marketing, SaaS, Lean Six Sigma, data analytics, and executive dashboard design. Specialized in PostgreSQL/Supabase, LINE Platform, production-grade application development, and Japanese-style data visualization for executive presentations. Only use when explicitly invoked by user via /eddication command. |
| license | Complete terms in LICENSE.txt |
| invocable | true |
Eddication Expert - World-Class Full-Stack Specialist
Overview
You are a world-class full-stack expert specializing in production-grade application development and executive dashboard design. Your expertise spans modern web development, database architecture, API integrations, testing, business intelligence, process optimization, and creating beautiful, intuitive data presentations.
Core Competencies
| Domain | Technologies |
|---|
| Frontend | React, Vue, Vanilla JS, LINE LIFF, Mobile-First CSS, TailwindCSS |
| Dashboard Design | Japanese minimal design, Executive presentations, Chart.js, Data visualization |
| Backend | Node.js/Express, Python/FastAPI/Django, Google Apps Script |
| Database | PostgreSQL 15+, Supabase (RLS, Realtime, Edge Functions), MongoDB, Redis |
| APIs & Integration | LINE Platform, REST APIs, Webhooks, OAuth, Third-party integrations |
| Testing | Playwright E2E, Vitest/Jest, Pytest, Integration testing |
| Analytics | Python Pandas, SQL analytics, KPI dashboards, Data visualization |
| Business | SaaS metrics, Pricing strategy, LTV/CAC analysis, Funnel optimization |
| Process | Six Sigma DMAIC, Kaizen, Lean process improvement, SPC charts |
Project Context Pattern
When working on any project, first identify:
1. Project Type → Web App / Mobile API / Dashboard / Integration / Automation
2. Tech Stack → Frontend + Backend + Database + External APIs
3. Key Requirements → Authentication? Real-time? Payments? Reporting?
4. Scale → Single user / Team (10-100) / Enterprise (1000+)
5. Deployment → Vercel/Netlify / Self-hosted / Cloud Functions / Hybrid
PART I: QUICK PATTERNS (Essentials)
Database - PostgreSQL Common Patterns
CREATE TABLE table_name (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
status TABLE_STATUS DEFAULT 'pending',
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_table_column ON table_name(column);
CREATE INDEX idx_table_composite ON table_name(col1, col2);
CREATE INDEX idx_table_partial ON table_name(col) WHERE status = 'active';
CREATE INDEX idx_table_jsonb ON table_name USING GIN(jsonb_col);
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own data" ON table_name
FOR SELECT USING (user_id = auth.uid()::text);
CREATE POLICY "Users can insert own data" ON table_name
FOR INSERT WITH CHECK (user_id auth.uid()::text);
POLICY "Admins full access" table_name
(auth.uid() ( id admin_users is_active ));
jsonb_col ;
jsonb_col @ ;
jsonb_col ? ;
jsonb_col jsonb_set(jsonb_col, , );
,
() ( user_id created_at) rn,
() ( user_id score ) rank,
() ( ) prev_value,
(amount) ( created_at UNBOUNDED PRECEDING) running_total
;
TypeScript - Type-Safe Supabase
export interface Database {
public: {
Tables: {
table_name: {
Row: { id: string; user_id: string; status: string; created_at: string }
Insert: { id?: string; user_id: string; status?: string }
Update: { id?: string; user_id?: string; status?: string }
}
}
}
}
import { createClient, SupabaseClient } from '@supabase/supabase-js';
const supabase: SupabaseClient<Database> = createClient(url, key);
const { data, error } = await supabase
.from('table_name')
.select('*, related_table(*)')
.eq('user_id', userId)
.order(, { : });
channel = supabase
.()
.(, {
: ,
: ,
: ,
:
}, {
.(, payload);
})
.();
{ supabase.(channel); };
<T, K keyof T> = T & { [P K]-?: T[P] };
<T> = T | ;
<T, E = > = <[T, ] | [, E]>;
asyncTry<T, E = >(
: <T>
): <T, E> {
{
data = promise;
[data, ];
} (error) {
[, error E];
}
}
<T keyof [][]> {
() {}
() {
query = supabase.(.).();
(filters) {
.(filters).( {
(v !== ) query = query.(k, v);
});
}
query;
}
() {
supabase.(.).().(, id).();
}
() {
supabase.(.).(record).().();
}
() {
supabase.(.).(updates).(, id).().();
}
}
React + Supabase Realtime Component
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/lib/supabase';
import type { Database } from '@/types/database';
type TableRow = Database['public']['Tables']['your_table']['Row'];
export function DataTable({ userId }: { userId: string }) {
const [data, setData] = useState<TableRow[]>([]);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
const { data, error } = await supabase
.from('your_table')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching data:', error);
return;
}
setData(data ?? []);
();
}, [userId]);
( {
();
channel = supabase
.()
.(, {
: ,
: ,
: ,
:
}, fetchData)
.();
{ supabase.(channel); };
}, [fetchData]);
(loading) ;
(data. === ) ;
(
);
}
LINE LIFF Integration
import liff from '@line/liff';
const LIFF_ID = import.meta.env.VITE_LIFF_ID;
async function initLiff() {
try {
await liff.init({ liffId: LIFF_ID });
return true;
} catch (error) {
console.error('LIFF init failed:', error);
return false;
}
}
async function getProfile() {
if (!liff.isLoggedIn()) {
liff.login({ redirectUri: window.location.href });
return null;
}
const profile = await liff.getProfile();
const context = liff.getContext();
{ ...profile, context };
}
() {
liff.([{ : , : message }]);
liff.();
}
isInClient = liff.();
LINE Messaging API
const createFlexMessage = (title: content, items: any[]) => ({
type: 'flex',
altText: title,
contents: {
type: 'bubble',
header: {
type: 'box',
layout: 'vertical',
contents: [{
type: 'text',
text: title,
color: '#FFFFFF',
size: 'md',
align: 'center',
weight: 'bold'
}],
backgroundColor: '#00B900',
paddingAll: 'md'
},
body: {
type: 'box',
layout: 'vertical',
contents: items.map(item => ({
type: 'text',
text: item.label,
margin: 'md'
})),
paddingAll: 'lg'
}
}
});
import crypto ;
(): {
hash = crypto
.(, channelSecret)
.(body)
.();
signature === hash;
}
express ;
app = ();
app.(,
express.({ : }),
{
signature = req.[];
(!(req..(), signature, process..!)) {
res.().();
}
();
},
(req, res) => {
events = .(req..()).;
( event events) {
(event.) {
: (event); ;
: (event); ;
: (event); ;
: (event); ;
}
}
res.().();
}
);
Python FastAPI Backend
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional, List
import asyncpg
import os
app = FastAPI(
title="API",
description="Production API",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(","),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.on_event("startup")
async def startup():
app.db_pool = await asyncpg.create_pool(
host=os.getenv("DB_HOST"),
database=os.getenv("DB_NAME"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
min_size=5,
max_size=20
)
@app.on_event("shutdown")
async def shutdown():
await app.db_pool.close()
class ItemResponse():
:
name:
status:
created_at:
():
name:
metadata: [] =
():
app.db_pool.acquire() conn:
query =
params = []
count =
status:
count +=
query +=
params.append(status)
query +=
params.extend([limit, offset])
rows = conn.fetch(query, *params)
[(row) row rows]
():
app.db_pool.acquire() conn:
row = conn.fetchrow(, item.name, item.metadata)
ItemResponse(**(row))
Google Apps Script Patterns
const Supabase = {
url: PropertiesService.getScriptProperties().getProperty('SUPABASE_URL'),
key: PropertiesService.getScriptProperties().getProperty('SUPABASE_KEY'),
fetch(table, options = {}) {
const { select = '*', filter = '', order = '', limit = 100 } = options;
let url = `${this.url}/rest/v1/${table}?select=${select}&limit=${limit}`;
if (filter) url += `&${filter}`;
if (order) url += `&order=${order}`;
const response = UrlFetchApp.fetch(url, {
headers: {
'apikey': this.key,
'Authorization': `Bearer ${this.key}`,
'Content-Type': 'application/json'
},
muteHttpExceptions: true
});
if (response.getResponseCode() !== ) {
();
}
.(response.());
},
() {
.(, {
: ,
: {
: .,
: ,
:
},
: .(data),
:
});
}
};
() {
ss = .();
sheet = ss.(sheetName) || ss.(sheetName);
data = .(tableName, { : });
sheet.();
sheet.(, , , columns.).([columns])
.()
.()
.();
(data. > ) {
rows = data.( columns.( row[col] || ));
sheet.(, , rows., columns.).(rows);
}
sheet.(, columns.);
data.;
}
() {
ui = .();
ui.()
.(, )
.(, )
.();
}
Playwright E2E Tests
import { test, expect } from '@playwright/test';
test.describe('User Workflow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('complete user flow', async ({ page }) => {
await page.click('[data-testid="login-btn"]');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="submit-btn"]');
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('h1')).toContainText('Welcome');
await page.click('[data-testid="create-btn"]');
await page.fill('[data-testid="item-name"]', );
page.(, );
page.();
(page.()).();
(page.()).();
(page.()).();
});
(, ({ page }) => {
page.();
page.();
(page.()).();
(page.()).();
});
(, ({ page }) => {
page.({ : , : });
page.();
(page.()).();
(page.())..();
});
});
PART II: DEEP DIVES
Database - Advanced PostgreSQL Patterns
PostGIS for Location-Based Features
CREATE EXTENSION IF NOT EXISTS postgis;
ALTER TABLE locations ADD COLUMN geom GEOMETRY(Point, 4326);
CREATE INDEX idx_locations_geom ON locations USING GIST(geom);
UPDATE locations
SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326)
WHERE geom IS NULL;
SELECT
id,
name,
ST_Distance(geom, ST_MakePoint($1, $2)::geography) AS distance_meters
FROM locations
WHERE ST_DWithin(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3)
ORDER BY distance_meters;
SELECT
ST_Distance(
ST_MakePoint(100.5018, 13.7563)::geography,
ST_MakePoint(100.5218, 13.7263)::geography
) / 1000 AS distance_km;
Recursive CTE for Hierarchical Data
WITH RECURSIVE tree AS (
SELECT id, name, parent_id, 1 AS level, ARRAY[id] AS path
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, t.level + 1, t.path || c.id
FROM categories c
INNER JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree ORDER BY level, name;
Materialized Views for Performance
CREATE MATERIALIZED VIEW mv_daily_stats AS
SELECT
created_at::date AS date,
COUNT(*) AS total_count,
COUNT(*) FILTER (WHERE status = 'active') AS active_count,
COUNT(*) FILTER (WHERE status = 'completed') AS completed_count,
AVG(amount) AS avg_amount
FROM transactions
GROUP BY created_at::date;
CREATE UNIQUE INDEX idx_mv_daily_stats_date ON mv_daily_stats(date);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_stats;
Full-Text Search
ALTER TABLE articles ADD COLUMN tsv tsvector GENERATED ALWAYS AS (
to_tsvector('english', coalesce(title, '') || ' ' || coalesce(content, ''))
) STORED;
CREATE INDEX idx_articles_tsv ON articles USING GIN(tsv);
SELECT
id,
title,
ts_headline('english', tsv, plainto_tsquery('english', $1)) AS highlight,
ranking
FROM articles,
to_tsquery('english', $1) query
WHERE tsv @@ query
ORDER BY ts_rank(tsv, query) DESC;
Security - Production Best Practices
RLS with Service Role Token Swap
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
const { userId, provider, providerToken } = await req.json();
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const { data: profile } = await supabase
.from('user_profiles')
.select('*')
.eq('id', userId)
.eq('provider_user_id', providerToken)
.single();
if (!profile) {
return new Response('User not found', { status: 404 });
}
const { : { session } } = supabase...({
: userId,
: profile.,
:
});
(.({
: session.,
: { : profile., : profile. }
}));
});
XSS Prevention Utilities
export function sanitizeHTML(str: string): string {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
export function sanitizeInput(obj: Record<string, any>): Record<string, any> {
const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
sanitized[key] = sanitizeHTML(value.trim());
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = sanitizeInput(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
export (): {
param.(, );
}
Rate Limiting Middleware
import { supabase } from './supabase';
export async function checkRateLimit(
identifier: string,
limit: number = 100,
windowMs: number = 60000
): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
const now = new Date();
const windowStart = new Date(now.getTime() - windowMs);
await supabase
.from('rate_limits')
.delete()
.lt('window_start', windowStart);
const { data: current } = await supabase
.from('rate_limits')
.select('count')
.eq('identifier', identifier)
.gte('window_start', windowStart)
.single();
count = current?. || ;
(count >= limit) {
{
: ,
: ,
: (windowStart.() + windowMs)
};
}
supabase
.()
.({
identifier,
: count + ,
: windowStart
}, {
:
});
{
: ,
: limit - count - ,
: (windowStart.() + windowMs)
};
}
Offline-First Architecture
interface QueuedAction {
id: string;
type: string;
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
payload?: any;
timestamp: number;
retries: number;
}
class OfflineQueue {
private queue: QueuedAction[] = [];
private storageKey = 'offline_queue';
private isOnline: boolean = navigator.onLine;
constructor() {
this.loadFromStorage();
this.setupEventListeners();
}
private loadFromStorage() {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) this.queue = JSON.parse(stored);
} catch (e) {
.(, e);
}
}
() {
.(., .(.));
}
() {
.(, {
. = ;
.();
});
.(, {
. = ;
});
}
(: <, | | >): {
: = {
...action,
: ,
: .(),
:
};
..(queued);
.();
(.) {
.();
}
queued.;
}
(): <{ : ; : }> {
(!. || .. === ) {
{ : , : };
}
success = ;
failed = ;
( i = .. - ; i >= ; i--) {
action = .[i];
{
.(action);
..(i, );
success++;
} (error) {
action.++;
(action. >= ) {
..(i, );
.(, action);
}
failed++;
}
}
.();
{ success, failed };
}
(: ): <> {
{ endpoint, method, payload } = action;
response = (endpoint, {
method,
: { : },
: payload ? .(payload) :
});
(!response.) {
();
}
response;
}
() {
{
: ..,
: .,
: ..( ({ : a., : a. }))
};
}
}
offlineQueue = ();
Mobile-First Design System
:root {
--color-primary: #00B900;
--color-primary-dark: #009100;
--color-secondary: #0066FF;
--color-success: #22C55E;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
--status-pending: #F59E0B;
--status-active: #3B82F6;
--status-completed: #22C55E;
--status-cancelled: #EF4444;
--font-sans: system-ui, -apple-system, sans-serif;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
: ;
: ;
: ;
: ;
: ;
: ;
}
(: dark) {
{
: ;
: ;
: ;
}
}
*, *, * {
: border-box;
}
{
: (--font-sans);
: (--text-base);
: ;
: (--color-text, );
: (--color-bg, );
}
{
: inline-flex;
: center;
: center;
: (--space-);
: (--space-) (--space-);
: inherit;
: (--text-base);
: ;
: ;
: none;
: (--radius-md);
: pointer;
: none;
: all ;
: (--touch-target);
: (--touch-target);
}
{
: (--color-primary);
: white;
}
{ : (--color-primary-dark); }
{
: transparent;
: solid (--color-primary);
: (--color-primary);
}
{
: white;
: (--radius-lg);
: ( / );
: (--space-);
}
{
: ;
: (--space-) (--space-);
: inherit;
: (--text-base);
: solid (--color-border, );
: (--radius-md);
: (--touch-target);
}
{
: none;
: (--color-primary);
: (, , , );
}
{
: ;
: (--space-);
: auto;
}
(: ) { { : ; } }
(: ) { { : ; } }
(: ) { { : ; } }
{
: grid;
: (--space-);
}
{ : (, fr); }
(: ) {
{ : (, fr); }
}
(: ) {
{ : (, fr); }
}
Lean Six Sigma - Process Improvement
"""
Six Sigma DMAIC Framework
Apply to any process improvement project
"""
from dataclasses import dataclass
from typing import List, Dict
import pandas as pd
@dataclass
class ProblemStatement:
"""Define Phase"""
what: str
where: str
when: str
who: str
impact: str
def to_statement(self) -> str:
return f"""Problem: {self.what}
Location: {self.where}
Timing: {self.when}
Affected: {self.who}
Impact: {self.impac}"""
class MeasurePhase:
"""Measure Phase - Data collection & baseline metrics"""
@staticmethod
def calculate_dpmo(defects: int, opportunities: int, units: int) -> float:
"""
Defects Per Million Opportunities
6σ = 3.4 DPMO, 5σ = 233, 4σ = 6,210, 3σ = 66,807
"""
(defects / (opportunities * units)) *
() -> :
sigma_map = {: , : , : , : , : }
(sigma_map.items(), key= x: (x[] - dpmo))[]
() -> :
(usl - lsl) / ( * std_dev)
() -> :
cpu = (usl - mean) / ( * std_dev)
cpl = (mean - lsl) / ( * std_dev)
(cpu, cpl)
:
() -> [, []]:
{
: [, , ],
: [, , ],
: [, , ],
: [, , ],
: [, ],
: [, , ]
}
:
() -> :
(annual_savings - cost) / cost cost >
() -> []:
idea ideas:
idea[] = .calculate_roi(idea[], idea[])
quick_wins = (
[i i ideas i.get() == ],
key= x: x[], reverse=
)
others = (
[i i ideas i.get() != ],
key= x: x[], reverse=
)
quick_wins + others
SaaS Metrics & Business Analytics
"""
SaaS Metrics Calculator
Track MRR, ARR, LTV, CAC, Churn, NRR
"""
import pandas as pd
from datetime import date, timedelta
from typing import Dict
class SaaSMetrics:
"""Calculate SaaS key performance indicators"""
def __init__(self, df: pd.DataFrame):
"""
DataFrame columns: customer_id, subscription_start, subscription_end,
mrr, plan_tier, expansion_amount, downgrade_amount
"""
self.df = df
def calculate_mrr(self) -> Dict:
"""Monthly Recurring Revenue breakdown"""
active = self.df[
self.df['subscription_end'].isna() |
(self.df['subscription_end'] > date.today())
]
return {
'total_mrr': active['mrr'].sum(),
'new_mrr': self._new_mrr(),
'expansion_mrr': self._expansion_mrr(),
'churn_mrr': self._churn_mrr(),
'net_new_mrr': (
self._new_mrr() + self._expansion_mrr() - self._churn_mrr()
)
}
def () -> :
.calculate_mrr()[] *
() -> :
marketing_spend / new_customers new_customers >
() -> :
churn_rate == :
arpu *
(arpu * gross_margin) / churn_rate
() -> :
ltv / cac cac >
() -> :
cutoff = date.today() - timedelta(days=days)
total = .df[.df[] <= cutoff]
churned = .df[
(.df[] >= cutoff) &
(.df[] <= date.today())
]
((churned) / (total)) * (total) >
() -> :
active = .df[
.df[].isna() |
(.df[] > date.today())
]
active[].mean() (active) >
() -> :
cutoff = date.today().replace(day=)
.df[.df[] >= cutoff][].()
() -> :
cutoff = date.today().replace(day=)
.df[
(.df[] >= cutoff) &
(.df[] <= date.today())
][].()
() -> :
.df[.df[].isna()][].()
sql_templates = {
: ,
: ,
:
}
Thailand-Specific Patterns
Timezone & Date Handling
export const THAI_TIMEZONE = 'Asia/Bangkok';
export const THAI_LOCALE = 'th-TH';
export const BUDDHIST_YEAR_OFFSET = 543;
export function formatThaiDate(
date: Date | string,
format: 'full' | 'short' | 'time' = 'short'
): string {
const d = typeof date === 'string' ? new Date(date) : date;
const options: Intl.DateTimeFormatOptions = {
timeZone: THAI_TIMEZONE,
calendar: 'buddhist'
};
switch (format) {
case 'full':
return d.toLocaleDateString(THAI_LOCALE, {
...options,
weekday: 'long',
year: ,
: ,
:
});
:
d.(, {
...options,
: ,
: ,
:
});
:
d.(, {
: ,
: ,
:
});
}
}
(): {
year + ;
}
(): {
buddhistYear - ;
}
(): {
months = [
, , , ,
, , , ,
, , ,
];
months[month - ] || ;
}
(): {
d = date === ? (date) : date;
;
}
(): | {
{
standard = (dateStr);
(!(standard.())) {
standard;
}
parts = dateStr.();
(parts. === ) {
[day, month, year] = parts.();
(year < ) {
year += year > ? : ;
}
(year > ) {
year = (year);
}
(year, month - , day);
}
} (e) {
.(, e);
}
;
}
(): {
( ().(, { : }));
}
(): {
d = date === ? (date) : date;
today = ();
d.() === today.();
}
Thai Mobile Number Validation
export type MobileProvider = 'AIS' | 'DTAC' | 'TRUE' | 'NT' | 'other';
const VALID_PREFIXES = [
'08', '09', '06',
'061', '062', '063', '064', '065', '081', '082', '083', '084', '085', '086', '087', '088', '089',
'091', '092', '093', '094', '095', '096', '097', '098', '099'
];
const PROVIDER_RANGES: Record<string, MobileProvider> = {
'08': 'AIS', '09': 'AIS',
'061': 'TRUE', '062': , : , : , : ,
: , : , : , : , : , : , : , : , : ,
: , : , : , : , : , : , : , : , :
};
(): {
mobile
.(, )
.(, )
.(, )
.();
}
(): {
cleaned = (mobile);
(!.(cleaned)) {
;
}
.( cleaned.(prefix));
}
(): {
cleaned = (mobile);
(cleaned. !== ) cleaned;
;
}
(): {
cleaned = (mobile);
cleaned.() ? + cleaned.() : cleaned;
}
(): {
cleaned = (mobile);
( [prefix, provider] .()) {
(cleaned.(prefix)) {
provider;
}
}
;
}
{ z } ;
thaiMobileSchema = z.()
.(cleanMobileNumber)
.(isValidThaiMobile, {
:
});
Thai Address Components
export interface ThaiAddress {
houseNumber?: string;
villageNumber?: string;
village?: string;
building?: string;
floor?: string;
room?: string;
alley?: string;
road?: string;
subdistrict?: string;
district?: string;
province?: string;
postalCode?: string;
lat?: number;
lng?: number;
}
export function (): {
: [] = [];
(address.) parts.();
(address.) parts.();
(address.) parts.(address.);
(address.) parts.(address.);
(address.) parts.();
(address.) parts.();
(address.) parts.();
(address.) parts.();
(address.) parts.();
(address.) parts.();
(address.) parts.();
(address.) parts.(address.);
parts.();
}
(): <> {
: <> = {};
patterns = {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
:
};
( [key, pattern] .(patterns)) {
match = addressStr.(pattern);
(match) {
result[key keyof ] = match[];
}
}
result;
}
Google Maps Thailand Geocoding
interface GeocodeResult {
formattedAddress: string;
lat: number;
lng: number;
placeId?: string;
components: ThaiAddress;
}
export async function geocodeThaiAddress(
address: string,
apiKey: string
): Promise<GeocodeResult | null> {
const url = new URL('https://maps.googleapis.com/maps/api/geocode/json');
url.searchParams.set('address', `${address}, Thailand`);
url.searchParams.set('key', apiKey);
url.searchParams.set('language', 'th');
const response = await fetch(url.toString());
const data = await response.json();
if (data.status !== || !data.?.[]) {
;
}
result = data.[];
{
: result.,
: result...,
: result...,
: result.,
: (result.)
};
}
(): < | > {
url = ();
url..(, );
url..(, apiKey);
url..(, );
response = (url.());
data = response.();
(data. !== || !data.?.[]) {
;
}
result = data.[];
{
: result.,
: result...,
: result...,
: result.,
: (result.)
};
}
(): {
: = {};
: <, []> = {
: [, ],
: [, ],
: [],
: [],
: []
};
( component components) {
( [key, typeList] .(types)) {
(component..( typeList.(t))) {
result[key keyof ] = component.;
}
}
}
result;
}
Haversine Distance (Thailand Coordinates)
export function haversineDistance(
lat1: number,
lng1: number,
lat2: number,
lng2: number
): number {
const R = 6371000;
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lng2 - lng1) * Math.PI) / 180;
const a =
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.(Δλ / );
c = * .(.(a), .( - a));
R * c;
}
(): {
(centerLat, centerLng, pointLat, pointLng) <= radiusMeters;
}
(): {
(meters < ) {
;
}
;
}
Tailwind CSS Patterns
Project Setup & Configuration
export default {
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./PTGLG/driverconnect/**/*.html',
'./PTGLG/driverconnect/**/*.js'
],
darkMode: 'class',
theme: {
extend: {
colors: {
line: {
green: '#00B900',
'green-dark': '#009100',
'green-light': '#00FF00'
},
status: {
pending: '#F59E0B',
active: '#3B82F6',
completed: '#22C55E',
cancelled: '#EF4444'
}
},
fontFamily: {
sans: ['Sarabun', 'system-ui', 'sans-serif'],
thai: ['Sarabun', 'sans-serif']
},
spacing: {
'safe-top': 'env(safe-area-inset-top)',
'safe-bottom': 'env(safe-area-inset-bottom)',
: ,
:
},
: {
: {: },
: {: , : },
: {: }
}
}
},
: [
(),
(),
()
]
}
Responsive Design Patterns
<div class="p-4 sm:p-6 md:p-8">
<div class="flex flex-col md:flex-row gap-4">
<div class="w-full md:w-1/2">
<h2 class="text-lg sm:text-xl md:text-2xl font-bold">
ขนาดตัวอักษรปรับตามหน้าจอ
</h2>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
</div>
<div class="hidden md:block">
Desktop-only content
</div>
<div class="md:hidden">
Mobile-only content
</div>
Dark Mode Implementation
<html class="dark">
</html>
<button class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white px-4 py-2 rounded">
ปุ่มที่รองรับ Dark Mode
</button>
<div class="bg-white dark:bg-slate-900 border-gray-200 dark:border-slate-700">
<p class="text-gray-900 dark:text-slate-100">
เนื้อหาที่ปรับสีตามธีม
</p>
</div>
function toggleDarkMode() {
document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
}
function initDarkMode() {
const stored = localStorage.getItem('darkMode');
if (stored !== null) {
if (stored === 'true') {
document.documentElement.classList.add('dark');
}
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
}
initDarkMode();
Custom Component Patterns
<button class="inline-flex items-center justify-center gap-2 px-4 py-2
bg-line-green hover:bg-line-green-dark text-white
font-medium rounded-lg transition-colors
disabled:opacity-50 disabled:cursor-not-allowed
focus:outline-none focus:ring-2 focus:ring-line-green focus:ring-offset-2">
<span>บันทึก</span>
</button>
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-danger">Danger</button>
<style>
@layer components {
.btn {
@apply inline-flex items-center justify-center gap-2 px-4 py-2
font-medium rounded-lg transition-colors
disabled:opacity-50 disabled:cursor-not-allowed
focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
bg-line-green :bg-line-green-dark text-white
:ring-line-green;
}
{
bg-gray- :bg-gray- text-gray-
:ring-gray-;
}
{
bg-red- :bg-red- text-white
:ring-red-;
}
}
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-sm
border border-gray-200 dark:border-slate-700
overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-slate-700
bg-gray-50 dark:bg-slate-900/50">
<h3 class="font-semibold text-gray-900 dark:text-white">
หัวข้อการ์ด
</h3>
</div>
<div class="p-4">
<p class="text-gray-700 dark:text-slate-300">
เนื้อหาในการ์ด
</p>
</div>
<div class="px-4 py-3 border-t border-gray-200 dark:border-slate-700
bg-gray-50 dark:bg-slate-900/50 flex justify-end gap-2">
<button class="btn btn-secondary text-sm">ยกเลิก</button>
<button class="btn btn-primary text-sm">ยืนยัน</button>
</div>
</div>
<div class="relative">
<label class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">
ชื่อ
</label>
<input type="text"
class="w-full px-3 py-2 rounded-lg border
border-gray-300 dark:border-slate-600
bg-white dark:bg-slate-800
text-gray-900 dark:text-white
placeholder-gray-400 dark:placeholder-slate-500
focus:outline-none focus:ring-2 focus:ring-line-green focus:border-transparent
disabled:bg-gray-100 dark:disabled:bg-slate-900
disabled:cursor-not-allowed"
placeholder="กรอกชื่อ">
<p class="mt-1 text-sm text-red-500 hidden" id="name-error">
กรุณากรอกชื่อ
</p>
</div>
Utility-First Best Practices
<style>
@layer components {
.card {
@apply bg-white dark:bg-slate-800 rounded-xl shadow-sm
border border-gray-200 dark:border-slate-700 p-4;
}
.input {
@apply w-full px-3 py-2 rounded-lg border
border-gray-300 dark:border-slate-600
bg-white dark:bg-slate-800
text-gray-900 dark:text-white
focus:outline-none focus:ring-2 focus:ring-line-green;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.safe-area-inset {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
: (safe-area-inset-right);
}
}
JIT / Production Build
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {}
}
}
{
"scripts": {
"dev": "concurrently \"npm run dev:css\" \"vite\"",
"dev:css": "tailwindcss -i ./src/css/input.css -o ./src/css/output.css --watch",
"build:css": "tailwindcss -i ./src/css/input.css -o ./dist/css/output.css --minify"
}
}
Integrating with Existing CSS
<div class="tw-flex tw-px-4 tw-py-2">
Content with prefixed classes
</div>
export default {
prefix: 'tw-',
}
LINE LIFF Specific Patterns
<div class="min-h-screen bg-gray-50 dark:bg-slate-900
safe-area-inset">
<header class="fixed top-0 left-0 right-0 z-50
bg-white dark:bg-slate-800
border-b border-gray-200 dark:border-slate-700
safe-top">
<div class="flex items-center justify-between px-4 py-3">
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">
DriverConnect
</h1>
<button class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700">
</button>
</div>
</header>
<main class="pt-14 pb-20 px-4">
</main>
<nav class="fixed bottom-0 left-0 right-0 z-50
bg-white dark:bg-slate-800
border-t border-gray-200 dark:border-slate-700
safe-bottom">
<div class="flex justify-around py-2">
< = =>
หน้าแรก
งานของฉัน
โปรไฟล์
Admin Dashboard Patterns
<div class="flex h-screen bg-gray-100 dark:bg-slate-900">
<aside class="hidden md:flex md:w-64 md:flex-col
bg-white dark:bg-slate-800
border-r border-gray-200 dark:border-slate-700">
<div class="p-4 border-b border-gray-200 dark:border-slate-700">
<h1 class="text-xl font-bold text-line-green">DriverConnect</h1>
<p class="text-sm text-gray-500 dark:text-slate-400">Admin Panel</p>
</div>
<nav class="flex-1 p-4 space-y-1 overflow-y-auto">
<a href="#" class="flex items-center gap-3 px-3 py-2
rounded-lg bg-line-green/10 text-line-green
font-medium">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin= =
=/>
แดชบอร์ด
จัดการงาน
พนักงานขับรถ
แดชบอร์ด
Status & Badge Components
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
รอดำเนินการ
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
กำลังดำเนินการ
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
เสร็จสิ้น
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
ยกเลิก
</span>
<style>
@layer components {
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
.badge-pending {
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
}
.badge-active {
@apply bg-blue-100 text-blue- :bg-blue-/ :text-blue-;
}
{
bg-green- text-green- :bg-green-/ :text-green-;
}
{
bg-red- text-red- :bg-red-/ :text-red-;
}
}
LINE OA Advanced Features
Rich Menu Management
interface RichMenuItem {
type: 'message' | 'uri' | 'datetimepicker' | 'postback';
label: string;
data?: string;
uri?: string;
area: {
x: number;
y: number;
width: number;
height: number;
};
}
interface RichMenuConfig {
name: string;
size: 'richmenu' | 'richmenu-album';
chatBarText: string;
items: RichMenuItem[];
}
export class LineRichMenu {
constructor(
private accessToken: string,
private apiBase = 'https://api.line.me/v2/bot'
) {}
async createRichMenu(config: RichMenuConfig): <> {
[width, height] = config. === ? [, ] : [, ];
richMenu = {
: { width, height },
: ,
: config.,
: config.,
: config..( ({
: item.,
: {
: item.,
: item.,
: item.,
: item.
}
}))
};
response = (, {
: ,
: {
: ,
:
},
: .(richMenu)
});
data = response.();
data.;
}
(
: ,
: ,
contentType =
): <> {
(, {
: ,
: {
: ,
: contentType
},
: imageBuffer
});
}
(: , : ): <> {
(, {
: ,
: {
:
}
});
}
(: ): <> {
(, {
: ,
: {
: ,
:
},
: .({ : })
});
}
(: ): <> {
(, {
: ,
: {
:
}
});
}
(: ): < | > {
response = (, {
: {
:
}
});
(response. === ) ;
data = response.();
data.;
}
(: ): <> {
(, {
: ,
: {
:
}
});
}
(): <[]> {
response = (, {
: {
:
}
});
data = response.();
data. || [];
}
}
Flex Message Templates
export function createCardFlexMessage({
title,
description,
imageUrl,
buttons
}: {
title: string;
description?: string;
imageUrl?: string;
buttons: Array<{ label: string; data: string; uri?: string }>;
}) {
return {
type: 'flex',
altText: title,
contents: {
type: 'bubble',
hero: imageUrl ? {
type: 'image',
url: imageUrl,
size: 'full',
aspectRatio: '20:13',
aspectMode: 'cover'
} : undefined,
body: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: title,
weight: 'bold',
size: 'xl',
wrap: true
},
...(description ? [{
type: 'text',
: description,
: ,
: ,
: ,
:
}] : [])
]
},
: buttons. > ? {
: ,
: ,
: ,
: buttons.( ({
: ,
: {
: btn. ? : ,
: btn.,
: btn.,
: btn.
},
:
}))
} :
}
};
}
() {
: <, > = {
: ,
: ,
: ,
:
};
{
: ,
: ,
: {
: ,
: {
: ,
: ,
: [
{
: ,
: ,
: ,
: ,
: ,
:
},
{
: ,
: job. === ? :
job. === ? :
job. === ? : ,
: ,
: ,
: ,
:
}
],
: statusColors[job.] || ,
: ,
:
},
: {
: ,
: ,
: [
{
: ,
: ,
: [
{
: ,
: ,
: ,
:
},
{
: ,
: job.,
: ,
: ,
:
}
],
:
},
{
: ,
: ,
: [
{
: ,
: ,
: ,
:
},
{
: ,
: job.,
: ,
: ,
:
}
],
:
}
],
:
},
: {
: ,
: ,
: [
{
: ,
: {
: ,
: ,
: job.
},
:
}
]
}
}
};
}
() {
{
: ,
: ,
: {
: ,
: jobs.( {
card = (job);
card.;
})
}
};
}
() {
{
: ,
text,
: {
: items.( ({
: ,
: {
: item. ? : ,
: item.,
: item.,
: item.
}
}))
}
};
}
Quick Reply & Message Templates
export function createButtonMessage({
title,
text,
thumbnailImageUrl,
buttons
}: {
title: string;
text: string;
thumbnailImageUrl?: string;
buttons: Array<{ label: string; data: string; uri?: string }>;
}) {
return {
type: 'template',
altText: title,
template: {
type: 'buttons',
thumbnailImageUrl,
title,
text,
actions: buttons.map(btn => ({
type: btn.uri ? 'uri' : 'message',
label: btn.label,
uri: btn.uri,
text: btn.data
}))
}
};
}
export function createConfirmMessage({
text,
okText,
okData,
cancelText = 'ยกเลิก',
cancelData = 'cancel'
}: {
text: string;
okText: string;
okData: string;
cancelText?: string;
cancelData?: string;
}) {
{
: ,
: text,
: {
: ,
text,
: [
{
: ,
: okText,
: okData
},
{
: ,
: cancelText,
: cancelData
}
]
}
};
}
() {
{
: ,
text,
: {
: [
{
: ,
: {
: ,
: ,
data,
mode,
min,
max
}
}
]
}
};
}
LINE Notify Integration
const LINE_NOTIFY_API = 'https://notify-api.line.me/api/notify';
export interface LineNotifyConfig {
accessToken: string;
}
export class LineNotify {
constructor(private config: LineNotifyConfig) {}
async sendMessage(message: string): Promise<boolean> {
const response = await fetch(LINE_NOTIFY_API, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.accessToken}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({ message })
});
return response.ok;
}
async sendMessageWithImage(
message: string,
: { ?: ; ?: }
): <> {
body = ();
body.(, message);
(image.) {
body.(, image.);
body.(, image.);
} (image.) {
body.(, image.);
}
response = (, {
: ,
: {
:
},
body
});
response.;
}
(
: ,
: ,
: =
): <> {
response = (, {
: ,
: {
: ,
:
},
: ({
message,
stickerId,
packageId
})
});
response.;
}
(: ): { : ; : } {
{
: (response..() || ),
: (response..() || )
};
}
}
Supabase Edge Functions Patterns
Authentication Flows
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
interface AuthenticatedRequest extends Request {
user?: {
id: string;
email?: string;
role?: string;
};
}
async function withAuth(
req: AuthenticatedRequest,
handler: (req: AuthenticatedRequest) => Promise<Response>
): Promise<Response> {
try {
const authHeader = req.headers.get('Authorization');
if (!authHeader?.()) {
(, { : });
}
token = authHeader.();
supabase = (supabaseUrl, supabaseServiceKey);
{ : { user }, error } = supabase..(token);
(error || !user) {
(, { : });
}
req. = {
: user.,
: user.,
: user.?. ||
};
(req);
} (error) {
.(, error);
(, { : });
}
}
() {
(: ): | {
(!req.) {
(, { : });
}
(!roles.(req.. || )) {
(, { : });
}
;
};
}
( (req) => {
(req. === ) {
(, {
: {
: ,
: ,
:
}
});
}
(req, (req) => {
roleCheck = ()(req);
(roleCheck) roleCheck;
(.({
: + req.?.,
: req.?.
}), {
: { : }
});
});
});
File Upload with Supabase Storage