diff --git a/.dockerignore b/.dockerignore
index 49df68b..913f304 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,4 +1,6 @@
.cursor
+.claude
+.kiro
.github
.next
.open-next
@@ -10,4 +12,4 @@
node_modules
**/node_modules
Dockerfile
-LICENSE
\ No newline at end of file
+LICENSE
diff --git a/README.md b/README.md
index a56b1c4..7fb8f22 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ If you found anything that could be improved, please let me know.
- 📚 documentation: [mksaas.com/docs](https://mksaas.com/docs)
- 🗓️ roadmap: [mksaas roadmap](https://mksaas.link/roadmap)
- 👨💻 discord: [mksaas.link/discord](https://mksaas.link/discord)
-- 📹 video (WIP): [mksaas.link/youtube](https://mksaas.link/youtube)
+- 📹 video: [mksaas.link/youtube](https://mksaas.link/youtube)
## Repositories
diff --git a/biome.json b/biome.json
index fa3eef5..bcb08bb 100644
--- a/biome.json
+++ b/biome.json
@@ -12,6 +12,8 @@
".open-next/**",
".wrangler/**",
".cursor/**",
+ ".claude/**",
+ ".kiro/**",
".vscode/**",
".source/**",
"node_modules/**",
@@ -27,8 +29,7 @@
"src/app/[[]locale]/preview/**",
"src/payment/types.ts",
"src/credits/types.ts",
- "src/types/index.d.ts",
- "public/sw.js"
+ "src/types/index.d.ts"
]
},
"formatter": {
@@ -75,6 +76,8 @@
".open-next/**",
".wrangler/**",
".cursor/**",
+ ".claude/**",
+ ".kiro/**",
".vscode/**",
".source/**",
"node_modules/**",
@@ -90,8 +93,7 @@
"src/app/[[]locale]/preview/**",
"src/payment/types.ts",
"src/credits/types.ts",
- "src/types/index.d.ts",
- "public/sw.js"
+ "src/types/index.d.ts"
]
},
"javascript": {
diff --git a/package.json b/package.json
index a95b14e..94ba18c 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,6 @@
"@types/canvas-confetti": "^1.9.0",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
- "@widgetbot/react-embed": "^1.9.0",
"ai": "^5.0.0",
"better-auth": "^1.1.19",
"canvas-confetti": "^1.9.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7da14c8..d8e4447 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -191,9 +191,6 @@ importers:
'@vercel/speed-insights':
specifier: ^1.2.0
version: 1.2.0(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
- '@widgetbot/react-embed':
- specifier: ^1.9.0
- version: 1.9.0(react@19.0.0)
ai:
specifier: ^5.0.0
version: 5.0.0(zod@4.0.17)
@@ -4824,14 +4821,6 @@ packages:
vue-router:
optional: true
- '@widgetbot/embed-api@1.2.17':
- resolution: {integrity: sha512-qoiFLMak+mBG64pgKN5xFv3amPHcG2qcurPefAbof4DI/eip5OU59pbM+ak4d9d9OIkwA1QhoDzo9KWD/cOn0w==}
-
- '@widgetbot/react-embed@1.9.0':
- resolution: {integrity: sha512-+Qgqy7lwLy++lIiHmSsgxUjwcX80iFIHR0QJpKq4W82ePUmq4bTuxvUbxcE+VQH5IjNrWaydGNR8zROV5vUQsA==}
- peerDependencies:
- react: '>= 15'
-
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@@ -5157,12 +5146,6 @@ packages:
crisp-sdk-web@1.0.25:
resolution: {integrity: sha512-CWTHFFeHRV0oqiXoPh/aIAKhFs6xcIM4NenGPnClAMCZUDQgQsF1OWmZWmnVNjJriXUmWRgDfeUxcxygS0dCRA==}
- cross-domain-safe-weakmap@1.0.29:
- resolution: {integrity: sha512-VLoUgf2SXnf3+na8NfeUFV59TRZkIJqCIATaMdbhccgtnTlSnHXkyTRwokngEGYdQXx8JbHT9GDYitgR2sdjuA==}
-
- cross-domain-utils@2.0.38:
- resolution: {integrity: sha512-zZfi3+2EIR9l4chrEiXI2xFleyacsJf8YMLR1eJ0Veb5FTMXeJ3DpxDjZkto2FhL/g717WSELqbptNSo85UJDw==}
-
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -6947,9 +6930,6 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
- post-robot@8.0.32:
- resolution: {integrity: sha512-PMOdDAt3pyuKUxZcTzdcXXFxLqkdeLpRlcCQl7QAJpI+e7J1YHH+PfC7KAbcL8hRVQ1LknQYGoirbA1/eO/a1g==}
-
postcss-selector-parser@7.1.0:
resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==}
engines: {node: '>=4'}
@@ -7918,9 +7898,6 @@ packages:
youch@4.1.0-beta.10:
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
- zalgo-promise@1.0.48:
- resolution: {integrity: sha512-LLHANmdm53+MucY9aOFIggzYtUdkSBFxUsy4glTTQYNyK6B3uCPWTbfiGvSrEvLojw0mSzyFJ1/RRLv+QMNdzQ==}
-
zod-to-json-schema@3.24.2:
resolution: {integrity: sha512-pNUqrcSxuuB3/+jBbU8qKUbTbDqYUaG1vf5cXFjbhGgoUuA1amO/y4Q8lzfOhHU8HNPK6VFJ18lBDKj3OHyDsg==}
peerDependencies:
@@ -13117,15 +13094,6 @@ snapshots:
next: 15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
- '@widgetbot/embed-api@1.2.17':
- dependencies:
- post-robot: 8.0.32
-
- '@widgetbot/react-embed@1.9.0(react@19.0.0)':
- dependencies:
- '@widgetbot/embed-api': 1.2.17
- react: 19.0.0
-
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@@ -13459,14 +13427,6 @@ snapshots:
crisp-sdk-web@1.0.25: {}
- cross-domain-safe-weakmap@1.0.29:
- dependencies:
- cross-domain-utils: 2.0.38
-
- cross-domain-utils@2.0.38:
- dependencies:
- zalgo-promise: 1.0.48
-
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -15667,12 +15627,6 @@ snapshots:
picomatch@4.0.2: {}
- post-robot@8.0.32:
- dependencies:
- cross-domain-safe-weakmap: 1.0.29
- cross-domain-utils: 2.0.38
- zalgo-promise: 1.0.48
-
postcss-selector-parser@7.1.0:
dependencies:
cssesc: 3.0.0
@@ -16851,8 +16805,6 @@ snapshots:
cookie: 1.0.2
youch-core: 0.3.3
- zalgo-promise@1.0.48: {}
-
zod-to-json-schema@3.24.2(zod@3.25.64):
dependencies:
zod: 3.25.64
diff --git a/public/sw.js b/public/sw.js
deleted file mode 100644
index 3ac3175..0000000
--- a/public/sw.js
+++ /dev/null
@@ -1,129 +0,0 @@
-// Service Worker for caching iframe content
-const CACHE_NAME = 'cnblocks-iframe-cache-v1'
-
-// Add iframe URLs to this list to prioritize caching
-const URLS_TO_CACHE = [
- // Default assets that should be cached
- '/favicon.ico',
- // Images used in iframes
- '/payments.png',
- '/payments-light.png',
- '/origin-cal.png',
- '/origin-cal-dark.png',
- '/exercice.png',
- '/exercice-dark.png',
- '/charts-light.png',
- '/charts.png',
- '/music-light.png',
- '/music.png',
- '/mail-back-light.png',
- '/mail-upper.png',
- '/mail-back.png',
- '/card.png',
- '/dark-card.webp',
-]
-
-// Install event - cache resources
-self.addEventListener('install', (event) => {
- event.waitUntil(
- caches
- .open(CACHE_NAME)
- .then((cache) => {
- console.log('Opened cache')
- return cache.addAll(URLS_TO_CACHE)
- })
- .then(() => self.skipWaiting()) // Activate SW immediately
- )
-})
-
-// Activate event - clean up old caches
-self.addEventListener('activate', (event) => {
- const currentCaches = [CACHE_NAME]
- event.waitUntil(
- caches
- .keys()
- .then((cacheNames) => {
- return cacheNames.filter((cacheName) => !currentCaches.includes(cacheName))
- })
- .then((cachesToDelete) => {
- return Promise.all(
- cachesToDelete.map((cacheToDelete) => {
- return caches.delete(cacheToDelete)
- })
- )
- })
- .then(() => self.clients.claim()) // Take control of clients immediately
- )
-})
-
-// Fetch event - serve from cache or fetch from network and cache
-self.addEventListener('fetch', (event) => {
- // Check if this is an iframe request - typically they'll be HTML or have 'preview' in the URL
- const isIframeRequest = event.request.url.includes('/preview/') || event.request.url.includes('/examples/')
-
- if (isIframeRequest) {
- event.respondWith(
- caches.match(event.request, { ignoreSearch: true }).then((response) => {
- // Return cached response if found
- if (response) {
- return response
- }
-
- // Clone the request (requests are one-time use)
- const fetchRequest = event.request.clone()
-
- return fetch(fetchRequest).then((response) => {
- // Check if we received a valid response
- if (!response || response.status !== 200 || response.type !== 'basic') {
- return response
- }
-
- // Clone the response (responses are one-time use)
- const responseToCache = response.clone()
-
- caches.open(CACHE_NAME).then((cache) => {
- cache.put(event.request, responseToCache)
- })
-
- return response
- })
- })
- )
- } else {
- // For non-iframe requests, use a standard cache-first strategy
- event.respondWith(
- caches.match(event.request).then((response) => {
- if (response) {
- return response
- }
- return fetch(event.request)
- })
- )
- }
-})
-
-// Listen for messages from clients (to force cache update, etc)
-self.addEventListener('message', (event) => {
- if (event.data && event.data.type === 'SKIP_WAITING') {
- self.skipWaiting()
- }
-
- // Handle cache clearing
- if (event.data && event.data.type === 'CLEAR_IFRAME_CACHE') {
- const url = event.data.url
-
- if (url) {
- // Clear specific URL from cache
- caches.open(CACHE_NAME).then((cache) => {
- cache.delete(url).then(() => {
- console.log(`Cleared cache for: ${url}`)
- })
- })
- } else {
- // Clear the entire cache
- caches.delete(CACHE_NAME).then(() => {
- console.log('Cleared entire iframe cache')
- })
- }
- }
-})
diff --git a/src/actions/check-web-content-analysis-credits.ts b/src/actions/check-web-content-analysis-credits.ts
deleted file mode 100644
index 3ca2c7f..0000000
--- a/src/actions/check-web-content-analysis-credits.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-'use server';
-
-import { getWebContentAnalysisCost } from '@/ai/text/utils/web-content-analyzer-config';
-import { getUserCredits, hasEnoughCredits } from '@/credits/credits';
-import type { User } from '@/lib/auth-types';
-import { userActionClient } from '@/lib/safe-action';
-
-/**
- * Check if user has enough credits for web content analysis
- */
-export const checkWebContentAnalysisCreditsAction = userActionClient.action(
- async ({ ctx }) => {
- const currentUser = (ctx as { user: User }).user;
-
- try {
- const requiredCredits = getWebContentAnalysisCost();
- const currentCredits = await getUserCredits(currentUser.id);
- const hasCredits = await hasEnoughCredits({
- userId: currentUser.id,
- requiredCredits,
- });
-
- return {
- success: true,
- hasEnoughCredits: hasCredits,
- currentCredits,
- requiredCredits,
- };
- } catch (error) {
- console.error('check web content analysis credits error:', error);
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Something went wrong',
- };
- }
- }
-);
diff --git a/src/ai/chat/components/ChatBot.tsx b/src/ai/chat/components/ChatBot.tsx
index 10408ee..9ac8aac 100644
--- a/src/ai/chat/components/ChatBot.tsx
+++ b/src/ai/chat/components/ChatBot.tsx
@@ -70,7 +70,7 @@ export default function ChatBot() {
};
return (
-
+
diff --git a/src/ai/image/components/ImagePlayground.tsx b/src/ai/image/components/ImagePlayground.tsx
index a88887d..105e3db 100644
--- a/src/ai/image/components/ImagePlayground.tsx
+++ b/src/ai/image/components/ImagePlayground.tsx
@@ -76,9 +76,9 @@ export function ImagePlayground({
return (
-
+
{/* header */}
-
+ {/*
*/}
{/* input prompt */}
balance >= amount;
-
- const handleConsume = async () => {
- if (!hasEnoughCredits(CONSUME_CREDITS)) {
- toast.error('Insufficient credits, please buy more credits.');
- return;
- }
- setLoading(true);
- try {
- await consumeCreditsMutation.mutateAsync({
- amount: CONSUME_CREDITS,
- description: `AI Text Credit Consumption (${CONSUME_CREDITS} credits)`,
- });
- toast.success(`${CONSUME_CREDITS} credits have been consumed.`);
- } catch (error) {
- toast.error('Failed to consume credits, please try again later.');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/ai/text/components/error-display.tsx b/src/ai/text/components/error-display.tsx
index 489ddd6..43a9435 100644
--- a/src/ai/text/components/error-display.tsx
+++ b/src/ai/text/components/error-display.tsx
@@ -34,7 +34,6 @@ interface ErrorDisplayProps {
const errorIcons = {
[ErrorType.VALIDATION]: AlertCircleIcon,
[ErrorType.NETWORK]: WifiOffIcon,
- [ErrorType.CREDITS]: CreditCardIcon,
[ErrorType.SCRAPING]: ServerIcon,
[ErrorType.ANALYSIS]: HelpCircleIcon,
[ErrorType.TIMEOUT]: ClockIcon,
@@ -84,7 +83,6 @@ const severityColors = {
const errorTitles = {
[ErrorType.VALIDATION]: 'Invalid Input',
[ErrorType.NETWORK]: 'Connection Error',
- [ErrorType.CREDITS]: 'Insufficient Credits',
[ErrorType.SCRAPING]: 'Unable to Access Website',
[ErrorType.ANALYSIS]: 'Analysis Failed',
[ErrorType.TIMEOUT]: 'Request Timed Out',
diff --git a/src/ai/text/components/index.ts b/src/ai/text/components/index.ts
index d0cea3d..de8876e 100644
--- a/src/ai/text/components/index.ts
+++ b/src/ai/text/components/index.ts
@@ -1,5 +1,4 @@
export { AnalysisResults } from './analysis-results';
-export { ConsumeCreditCard } from './consume-credit-card';
export { LoadingStates } from './loading-states';
export { UrlInputForm } from './url-input-form';
export { WebContentAnalyzer } from './web-content-analyzer';
diff --git a/src/ai/text/components/url-input-form.tsx b/src/ai/text/components/url-input-form.tsx
index 504d7de..39c33dd 100644
--- a/src/ai/text/components/url-input-form.tsx
+++ b/src/ai/text/components/url-input-form.tsx
@@ -1,9 +1,7 @@
'use client';
-import { checkWebContentAnalysisCreditsAction } from '@/actions/check-web-content-analysis-credits';
import type { UrlInputFormProps } from '@/ai/text/utils/web-content-analyzer';
import { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-config';
-import { LoginWrapper } from '@/components/auth/login-wrapper';
import { Button } from '@/components/ui/button';
import {
Form,
@@ -20,21 +18,10 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
-import { useLocalePathname } from '@/i18n/navigation';
-import { authClient } from '@/lib/auth-client';
import { zodResolver } from '@hookform/resolvers/zod';
-import {
- AlertCircleIcon,
- CoinsIcon,
- LinkIcon,
- Loader2Icon,
- LogInIcon,
- SparklesIcon,
-} from 'lucide-react';
-import { useAction } from 'next-safe-action/hooks';
+import { LinkIcon, Loader2Icon, SparklesIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
-import { toast } from 'sonner';
import { z } from 'zod';
import { useDebounce } from '../utils/performance';
@@ -52,19 +39,9 @@ export function UrlInputForm({
modelProvider,
setModelProvider,
}: UrlInputFormProps) {
- const [creditInfo, setCreditInfo] = useState<{
- hasEnoughCredits: boolean;
- currentCredits: number;
- requiredCredits: number;
- } | null>(null);
const [mounted, setMounted] = useState(false);
- // Get authentication status and current path for callback
- const { data: session, isPending: isAuthLoading } = authClient.useSession();
- const isAuthenticated = !!session?.user;
- const currentPath = useLocalePathname();
-
- // Prevent hydration mismatch by only rendering auth-dependent content after mount
+ // Prevent hydration mismatch by only rendering content after mount
useEffect(() => {
setMounted(true);
}, []);
@@ -84,42 +61,6 @@ export function UrlInputForm({
webContentAnalyzerConfig.performance.urlInputDebounceMs
);
- const { execute: checkCredits, isExecuting: isCheckingCredits } = useAction(
- checkWebContentAnalysisCreditsAction,
- {
- onSuccess: (result) => {
- if (result.data?.success) {
- setCreditInfo({
- hasEnoughCredits: result.data.hasEnoughCredits ?? false,
- currentCredits: result.data.currentCredits ?? 0,
- requiredCredits: result.data.requiredCredits ?? 0,
- });
- } else {
- // Only show error toast if it's not an auth error
- if (result.data?.error !== 'Unauthorized') {
- setTimeout(() => {
- toast.error(result.data?.error || 'Failed to check credits');
- }, 0);
- }
- }
- },
- onError: (error) => {
- console.error('Credit check error:', error);
- // Only show error toast for non-auth errors
- setTimeout(() => {
- toast.error('Failed to check credits');
- }, 0);
- },
- }
- );
-
- // Check credits only when user is authenticated
- useEffect(() => {
- if (isAuthenticated && !isAuthLoading) {
- checkCredits();
- }
- }, [isAuthenticated, isAuthLoading, checkCredits]);
-
// Debounced URL validation effect
useEffect(() => {
if (debouncedUrl && debouncedUrl !== urlValue) {
@@ -129,23 +70,12 @@ export function UrlInputForm({
}, [debouncedUrl, urlValue, form]);
const handleSubmit = (data: UrlFormData) => {
- // For authenticated users, check credits before submitting
- if (creditInfo && !creditInfo.hasEnoughCredits) {
- // Defer toast to avoid flushSync during render
- setTimeout(() => {
- toast.error(
- `Insufficient credits. You need ${creditInfo.requiredCredits} credits but only have ${creditInfo.currentCredits}.`
- );
- }, 0);
- return;
- }
onSubmit(data.url ?? '', modelProvider);
};
const handleFormSubmit = form.handleSubmit(handleSubmit);
- const isInsufficientCredits = creditInfo && !creditInfo.hasEnoughCredits;
- const isFormDisabled = isLoading || disabled || !!isInsufficientCredits;
+ const isFormDisabled = isLoading || disabled;
return (
<>
@@ -161,10 +91,10 @@ export function UrlInputForm({
+ OpenRouter
OpenAI GPT-4o
Google Gemini
- DeepSeek
- OpenRouter
+ DeepSeek R1
@@ -194,67 +124,20 @@ export function UrlInputForm({
)}
/>
- {/* Credit Information - Only show for authenticated users */}
- {isAuthenticated && creditInfo && (
-
-
-
-
- Cost: {creditInfo.requiredCredits} credits
-
-
-
-
- Balance: {creditInfo.currentCredits}
-
- {!creditInfo.hasEnoughCredits && (
-
- )}
-
-
- )}
-
- {/* Insufficient Credits Warning */}
- {isAuthenticated && isInsufficientCredits && (
-
-
-
- Insufficient credits. You need {creditInfo.requiredCredits}{' '}
- credits but only have {creditInfo.currentCredits}.
-
-
- )}
-
{!mounted ? (
// Show loading state during hydration to prevent mismatch
- ) : isAuthenticated ? (
+ ) : (
- ) : (
-
-
-
)}
diff --git a/src/ai/text/components/web-content-analyzer.tsx b/src/ai/text/components/web-content-analyzer.tsx
index e2b23d2..4e7f769 100644
--- a/src/ai/text/components/web-content-analyzer.tsx
+++ b/src/ai/text/components/web-content-analyzer.tsx
@@ -194,7 +194,8 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) {
const [state, dispatch] = useReducer(analysisReducer, initialState);
// Model provider state
- const [modelProvider, setModelProvider] = useState
('openai');
+ const [modelProvider, setModelProvider] =
+ useState('openrouter');
// Enhanced error state
const [analyzedError, setAnalyzedError] =
@@ -232,16 +233,6 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) {
errorType = ErrorType.VALIDATION;
retryable = false;
break;
- case 401:
- errorType = ErrorType.AUTHENTICATION;
- severity = ErrorSeverity.HIGH;
- retryable = false;
- break;
- case 402:
- errorType = ErrorType.CREDITS;
- severity = ErrorSeverity.HIGH;
- retryable = false;
- break;
case 408:
errorType = ErrorType.TIMEOUT;
break;
diff --git a/src/ai/text/utils/error-handling.ts b/src/ai/text/utils/error-handling.ts
index 8896d5e..ea648a6 100644
--- a/src/ai/text/utils/error-handling.ts
+++ b/src/ai/text/utils/error-handling.ts
@@ -9,7 +9,6 @@ import { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-c
export enum ErrorType {
VALIDATION = 'validation',
NETWORK = 'network',
- CREDITS = 'credits',
SCRAPING = 'scraping',
ANALYSIS = 'analysis',
TIMEOUT = 'timeout',
@@ -96,22 +95,6 @@ export function classifyError(error: unknown): WebContentAnalyzerError {
);
}
- // Credit errors
- if (
- message.includes('credit') ||
- message.includes('insufficient') ||
- message.includes('balance')
- ) {
- return new WebContentAnalyzerError(
- ErrorType.CREDITS,
- error.message,
- 'Insufficient credits to perform analysis. Please purchase more credits.',
- ErrorSeverity.HIGH,
- false,
- error
- );
- }
-
// Scraping errors
if (
message.includes('scrape') ||
@@ -278,16 +261,6 @@ export function getRecoveryActions(error: WebContentAnalyzerError): Array<{
{ label: 'Try Simpler URL', action: 'simplify_url' },
];
- case ErrorType.CREDITS:
- return [
- {
- label: 'Purchase Credits',
- action: 'purchase_credits',
- primary: true,
- },
- { label: 'Check Balance', action: 'check_balance' },
- ];
-
case ErrorType.SCRAPING:
return [
{ label: 'Try Again', action: 'retry', primary: true },
diff --git a/src/ai/text/utils/web-content-analyzer-config.ts b/src/ai/text/utils/web-content-analyzer-config.ts
index 83b183c..1fb1c58 100644
--- a/src/ai/text/utils/web-content-analyzer-config.ts
+++ b/src/ai/text/utils/web-content-analyzer-config.ts
@@ -6,11 +6,6 @@
*/
export const webContentAnalyzerConfig = {
- /**
- * Credit cost for performing a web content analysis
- */
- creditsCost: 100,
-
/**
* Maximum content length for AI analysis (in characters)
* Optimized to prevent token limit issues while maintaining quality
@@ -118,21 +113,15 @@ export const webContentAnalyzerConfig = {
maxTokens: 2000,
},
openrouter: {
- model: 'openrouter/horizon-beta',
+ // model: 'openrouter/horizon-beta',
// model: 'x-ai/grok-3-beta',
// model: 'openai/gpt-4o-mini',
+ model: 'deepseek/deepseek-r1:free',
temperature: 0.1,
maxTokens: 2000,
},
} as const;
-/**
- * Get the credit cost for web content analysis
- */
-export function getWebContentAnalysisCost(): number {
- return webContentAnalyzerConfig.creditsCost;
-}
-
/**
* Validates if the Firecrawl API key is configured
*/
@@ -151,8 +140,6 @@ export function validateFirecrawlConfig(): boolean {
*/
export function validateWebContentAnalyzerConfig(): boolean {
return (
- typeof webContentAnalyzerConfig.creditsCost === 'number' &&
- webContentAnalyzerConfig.creditsCost > 0 &&
typeof webContentAnalyzerConfig.maxContentLength === 'number' &&
webContentAnalyzerConfig.maxContentLength > 0 &&
typeof webContentAnalyzerConfig.timeoutMillis === 'number' &&
diff --git a/src/ai/text/utils/web-content-analyzer.ts b/src/ai/text/utils/web-content-analyzer.ts
index 9e9e07b..a49d8c5 100644
--- a/src/ai/text/utils/web-content-analyzer.ts
+++ b/src/ai/text/utils/web-content-analyzer.ts
@@ -67,7 +67,7 @@ export interface AnalysisState {
}
// Component Props Interfaces
-export type ModelProvider = 'openai' | 'gemini' | 'deepseek';
+export type ModelProvider = 'openai' | 'gemini' | 'deepseek' | 'openrouter';
export interface WebContentAnalyzerProps {
className?: string;
diff --git a/src/app/[locale]/(marketing)/(pages)/test/page.tsx b/src/app/[locale]/(marketing)/(pages)/test/page.tsx
index 8d5a2a6..abea424 100644
--- a/src/app/[locale]/(marketing)/(pages)/test/page.tsx
+++ b/src/app/[locale]/(marketing)/(pages)/test/page.tsx
@@ -1,12 +1,12 @@
import Container from '@/components/layout/container';
-import { CreditsTest } from '@/components/test/credits-test';
+import { ConsumeCreditsCard } from '@/components/test/consume-credits-card';
export default async function TestPage() {
return (
{/* credits test */}
-
+
);
diff --git a/src/app/[locale]/(marketing)/ai/chat/page.tsx b/src/app/[locale]/(marketing)/ai/chat/page.tsx
index ba4041d..bbafd80 100644
--- a/src/app/[locale]/(marketing)/ai/chat/page.tsx
+++ b/src/app/[locale]/(marketing)/ai/chat/page.tsx
@@ -37,7 +37,9 @@ export default async function AIChatPage() {
{/* Chat Bot */}
-
+
+
+
);
diff --git a/src/app/[locale]/(marketing)/ai/image/page.tsx b/src/app/[locale]/(marketing)/ai/image/page.tsx
index 09a440f..fa34aa0 100644
--- a/src/app/[locale]/(marketing)/ai/image/page.tsx
+++ b/src/app/[locale]/(marketing)/ai/image/page.tsx
@@ -2,6 +2,7 @@ import { ImagePlayground } from '@/ai/image/components/ImagePlayground';
import { getRandomSuggestions } from '@/ai/image/lib/suggestions';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
+import { ImageIcon } from 'lucide-react';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
@@ -26,8 +27,21 @@ export default async function AIImagePage() {
const t = await getTranslations('AIImagePage');
return (
-
-
+
+
+ {/* Header Section */}
+
+
+ {/* Image Playground Component */}
+
+
+
+
);
}
diff --git a/src/app/[locale]/(marketing)/blocks/[category]/layout.tsx b/src/app/[locale]/(marketing)/blocks/[category]/layout.tsx
deleted file mode 100644
index c988f6f..0000000
--- a/src/app/[locale]/(marketing)/blocks/[category]/layout.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { categories } from '@/components/tailark/blocks';
-import BlocksNav from '@/components/tailark/blocks-nav';
-import type { PropsWithChildren } from 'react';
-
-/**
- * The locale inconsistency issue has been fixed in the BlocksNav component
- */
-export default function BlockCategoryLayout({ children }: PropsWithChildren) {
- return (
- <>
-
-
-
{children}
- >
- );
-}
diff --git a/src/app/[locale]/(marketing)/blocks/[category]/page.tsx b/src/app/[locale]/(marketing)/blocks/[category]/page.tsx
deleted file mode 100644
index cd893d1..0000000
--- a/src/app/[locale]/(marketing)/blocks/[category]/page.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import BlockPreview from '@/components/tailark/block-preview';
-import { blocks, categories } from '@/components/tailark/blocks';
-import { constructMetadata } from '@/lib/metadata';
-import { getUrlWithLocale } from '@/lib/urls/urls';
-import type { Metadata } from 'next';
-import type { Locale } from 'next-intl';
-import { getTranslations } from 'next-intl/server';
-import { notFound } from 'next/navigation';
-
-export const dynamic = 'force-static';
-export const revalidate = 3600;
-
-export async function generateStaticParams() {
- return categories.map((category) => ({
- category: category,
- }));
-}
-
-export async function generateMetadata({
- params,
-}: {
- params: Promise<{ locale: Locale; category: string }>;
-}): Promise
{
- const { locale, category } = await params;
- const t = await getTranslations({ locale, namespace: 'Metadata' });
- return constructMetadata({
- title: category + ' | ' + t('title'),
- description: t('description'),
- canonicalUrl: getUrlWithLocale(`/blocks/${category}`, locale),
- });
-}
-
-interface BlockCategoryPageProps {
- params: Promise<{ category: string }>;
-}
-
-export default async function BlockCategoryPage({
- params,
-}: BlockCategoryPageProps) {
- const { category } = await params;
- const categoryBlocks = blocks.filter((b) => b.category === category);
-
- if (categoryBlocks.length === 0) {
- notFound();
- }
-
- return (
- <>
- {categoryBlocks.map((block, index) => (
-
- ))}
- >
- );
-}
diff --git a/src/app/[locale]/providers.tsx b/src/app/[locale]/providers.tsx
index 76ebae3..c3e89a9 100644
--- a/src/app/[locale]/providers.tsx
+++ b/src/app/[locale]/providers.tsx
@@ -29,7 +29,7 @@ interface ProvidersProps {
*/
export function Providers({ children, locale }: ProvidersProps) {
const theme = useTheme();
- const defaultMode = websiteConfig.metadata.mode?.defaultMode ?? 'system';
+ const defaultMode = websiteConfig.ui.mode?.defaultMode ?? 'system';
// available languages that will be displayed in the docs UI
// make sure `locale` is consistent with your i18n config
diff --git a/src/app/api/analyze-content/route.ts b/src/app/api/analyze-content/route.ts
index 91db24e..ef01183 100644
--- a/src/app/api/analyze-content/route.ts
+++ b/src/app/api/analyze-content/route.ts
@@ -13,12 +13,9 @@ import {
validateUrl,
} from '@/ai/text/utils/web-content-analyzer';
import {
- getWebContentAnalysisCost,
validateFirecrawlConfig,
webContentAnalyzerConfig,
} from '@/ai/text/utils/web-content-analyzer-config';
-import { consumeCredits, hasEnoughCredits } from '@/credits/credits';
-import { getSession } from '@/lib/server';
import { createDeepSeek } from '@ai-sdk/deepseek';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createOpenAI } from '@ai-sdk/openai';
@@ -30,7 +27,6 @@ import { z } from 'zod';
// Constants from configuration
const TIMEOUT_MILLIS = webContentAnalyzerConfig.timeoutMillis;
-const CREDITS_COST = getWebContentAnalysisCost();
const MAX_CONTENT_LENGTH = webContentAnalyzerConfig.maxContentLength;
// Initialize Firecrawl client
@@ -361,28 +357,6 @@ export async function POST(req: NextRequest) {
);
}
- // Check authentication
- const session = await getSession();
- if (!session) {
- const authError = new WebContentAnalyzerError(
- ErrorType.AUTHENTICATION,
- 'Authentication required',
- 'Please sign in to analyze web content.',
- ErrorSeverity.HIGH,
- false
- );
-
- logError(authError, { requestId });
-
- return NextResponse.json(
- {
- success: false,
- error: authError.userMessage,
- } satisfies AnalyzeContentResponse,
- { status: 401 }
- );
- }
-
// Check if Firecrawl is configured
if (!validateFirecrawlConfig()) {
const configError = new WebContentAnalyzerError(
@@ -404,39 +378,7 @@ export async function POST(req: NextRequest) {
);
}
- // Check if user has sufficient credits before starting analysis
- const hasCredits = await hasEnoughCredits({
- userId: session.user.id,
- requiredCredits: CREDITS_COST,
- });
-
- if (!hasCredits) {
- const creditError = new WebContentAnalyzerError(
- ErrorType.CREDITS,
- 'Insufficient credits to perform analysis',
- "You don't have enough credits to analyze this webpage. Please purchase more credits.",
- ErrorSeverity.HIGH,
- false
- );
-
- logError(creditError, {
- requestId,
- userId: session.user.id,
- requiredCredits: CREDITS_COST,
- });
-
- return NextResponse.json(
- {
- success: false,
- error: creditError.userMessage,
- } satisfies AnalyzeContentResponse,
- { status: 402 }
- );
- }
-
- console.log(
- `Starting analysis [requestId=${requestId}, url=${url}, userId=${session.user.id}]`
- );
+ console.log(`Starting analysis [requestId=${requestId}, url=${url}]`);
// Perform analysis with timeout and enhanced error handling
const analysisPromise = (async () => {
@@ -447,13 +389,6 @@ export async function POST(req: NextRequest) {
// Step 2: Analyze content with AI (pass provider)
const analysis = await analyzeContent(content, url, modelProvider);
- // Step 3: Consume credits (only on successful analysis)
- await consumeCredits({
- userId: session.user.id,
- amount: CREDITS_COST,
- description: `Web content analysis: ${url}`,
- });
-
return { analysis, screenshot };
} catch (error) {
// If it's already a WebContentAnalyzerError, just re-throw
@@ -477,7 +412,6 @@ export async function POST(req: NextRequest) {
return NextResponse.json({
success: true,
data: result,
- creditsConsumed: CREDITS_COST,
} satisfies AnalyzeContentResponse);
} catch (error) {
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
@@ -499,12 +433,6 @@ export async function POST(req: NextRequest) {
case ErrorType.VALIDATION:
statusCode = 400;
break;
- case ErrorType.AUTHENTICATION:
- statusCode = 401;
- break;
- case ErrorType.CREDITS:
- statusCode = 402;
- break;
case ErrorType.TIMEOUT:
statusCode = 408;
break;
diff --git a/src/components/dashboard/sidebar-user.tsx b/src/components/dashboard/sidebar-user.tsx
index 6a0831a..11317b4 100644
--- a/src/components/dashboard/sidebar-user.tsx
+++ b/src/components/dashboard/sidebar-user.tsx
@@ -71,7 +71,7 @@ export function SidebarUser({ user, className }: SidebarUserProps) {
});
};
- const showModeSwitch = websiteConfig.metadata.mode?.enableSwitch ?? false;
+ const showModeSwitch = websiteConfig.ui.mode?.enableSwitch ?? false;
const showLocaleSwitch = LOCALES.length > 1;
const handleSignOut = async () => {
diff --git a/src/components/layout/active-theme-provider.tsx b/src/components/layout/active-theme-provider.tsx
index 412e553..616677a 100644
--- a/src/components/layout/active-theme-provider.tsx
+++ b/src/components/layout/active-theme-provider.tsx
@@ -10,7 +10,7 @@ import {
} from 'react';
const COOKIE_NAME = 'active_theme';
-const DEFAULT_THEME = websiteConfig.metadata.theme?.defaultTheme ?? 'default';
+const DEFAULT_THEME = websiteConfig.ui.theme?.defaultTheme ?? 'default';
function setThemeCookie(theme: string) {
if (typeof window === 'undefined') return;
diff --git a/src/components/layout/mode-switcher-horizontal.tsx b/src/components/layout/mode-switcher-horizontal.tsx
index 0c707cf..e7f3dc7 100644
--- a/src/components/layout/mode-switcher-horizontal.tsx
+++ b/src/components/layout/mode-switcher-horizontal.tsx
@@ -12,7 +12,7 @@ import { useEffect, useState } from 'react';
* Mode switcher component, used in the footer
*/
export function ModeSwitcherHorizontal() {
- if (!websiteConfig.metadata.mode?.enableSwitch) {
+ if (!websiteConfig.ui.mode?.enableSwitch) {
return null;
}
diff --git a/src/components/layout/mode-switcher.tsx b/src/components/layout/mode-switcher.tsx
index 323d413..d98d1b9 100644
--- a/src/components/layout/mode-switcher.tsx
+++ b/src/components/layout/mode-switcher.tsx
@@ -16,7 +16,7 @@ import { useTheme } from 'next-themes';
* Mode switcher component, used in the navbar
*/
export function ModeSwitcher() {
- if (!websiteConfig.metadata.mode?.enableSwitch) {
+ if (!websiteConfig.ui.mode?.enableSwitch) {
return null;
}
diff --git a/src/components/layout/theme-selector.tsx b/src/components/layout/theme-selector.tsx
index ba8af28..2894fcb 100644
--- a/src/components/layout/theme-selector.tsx
+++ b/src/components/layout/theme-selector.tsx
@@ -21,7 +21,7 @@ import { useThemeConfig } from './active-theme-provider';
* https://github.com/TheOrcDev/orcish-dashboard/blob/main/components/theme-selector.tsx
*/
export function ThemeSelector() {
- if (!websiteConfig.metadata.theme?.enableSwitch) {
+ if (!websiteConfig.ui.theme?.enableSwitch) {
return null;
}
diff --git a/src/components/shared/discord-widget.tsx b/src/components/shared/discord-widget.tsx
deleted file mode 100644
index 6c42c5f..0000000
--- a/src/components/shared/discord-widget.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-'use client';
-
-import { DiscordIcon } from '@/components/icons/discord';
-import { websiteConfig } from '@/config/website';
-import { useMediaQuery } from '@/hooks/use-media-query';
-import WidgetBot from '@widgetbot/react-embed';
-import { useEffect, useRef, useState } from 'react';
-
-/**
- * Discord Widget, shows the channels and messages in the discord server
- *
- * @deprecated
- * This feature is deprecated for Discord Widget can not be used anymore.
- *
- * https://docs.widgetbot.io/embed/react-embed/
- */
-export default function DiscordWidget() {
- if (!websiteConfig.features.enableDiscordWidget) {
- return null;
- }
-
- const serverId = process.env.NEXT_PUBLIC_DISCORD_WIDGET_SERVER_ID as string;
- const channelId = process.env.NEXT_PUBLIC_DISCORD_WIDGET_CHANNEL_ID as string;
- if (!serverId || !channelId) {
- return null;
- }
-
- const [open, setOpen] = useState(false);
- const widgetRef = useRef(null);
- const { device, width: windowWidth, height: windowHeight } = useMediaQuery();
-
- let widgetWidth = 800;
- let widgetHeight = 600;
- if (device === 'mobile') {
- widgetWidth = windowWidth ? Math.floor(windowWidth * 0.9) : 320;
- widgetHeight = windowHeight ? Math.floor(windowHeight * 0.8) : 400;
- } else if (device === 'tablet' || device === 'sm') {
- widgetWidth = windowWidth ? Math.floor(windowWidth * 0.9) : 600;
- widgetHeight = windowHeight ? Math.floor(windowHeight * 0.8) : 480;
- }
-
- useEffect(() => {
- if (!open) return;
- function handleClick(e: MouseEvent) {
- if (widgetRef.current && !widgetRef.current.contains(e.target as Node)) {
- setOpen(false);
- }
- }
- document.addEventListener('mousedown', handleClick);
- return () => document.removeEventListener('mousedown', handleClick);
- }, [open]);
-
- return (
-
- {/* discord icon button, show in bottom right corner */}
- {!open && (
-
- )}
-
- {/* discord widget expand layer */}
- {open && (
-
- )}
-
- );
-}
diff --git a/src/components/tailark/block-preview.tsx b/src/components/tailark/block-preview.tsx
deleted file mode 100644
index 797e49e..0000000
--- a/src/components/tailark/block-preview.tsx
+++ /dev/null
@@ -1,368 +0,0 @@
-'use client';
-
-import { Button } from '@/components/ui/button';
-import { Separator } from '@/components/ui/separator';
-import { useCopyToClipboard } from '@/hooks/use-clipboard';
-import { isUrlCached } from '@/lib/serviceWorker';
-import { cn } from '@/lib/utils';
-import * as RadioGroup from '@radix-ui/react-radio-group';
-import { Check, Code2, Copy, Eye, Maximize, Terminal } from 'lucide-react';
-import Link from 'next/link';
-import type React from 'react';
-import { useEffect, useRef, useState } from 'react';
-import {
- Panel,
- PanelGroup,
- PanelResizeHandle,
- type ImperativePanelGroupHandle,
-} from 'react-resizable-panels';
-import { useMedia } from 'use-media';
-
-export interface BlockPreviewProps {
- code?: string;
- preview: string;
- title: string;
- category: string;
- previewOnly?: boolean;
-}
-
-const radioItem =
- 'rounded-(--radius) duration-200 flex items-center justify-center h-8 px-2.5 gap-2 transition-[color] data-[state=checked]:bg-muted';
-
-const DEFAULTSIZE = 100;
-const SMSIZE = 30;
-const MDSIZE = 62;
-const LGSIZE = 82;
-
-const getCacheKey = (src: string) => `iframe-cache-${src}`;
-
-const titleToNumber = (title: string): number => {
- const titles = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"];
- return titles.indexOf(title.toLowerCase()) + 1;
-};
-
-export const BlockPreview: React.FC = ({
- code,
- preview,
- title,
- category,
- previewOnly,
-}) => {
- const [width, setWidth] = useState(DEFAULTSIZE);
- const [mode, setMode] = useState<'preview' | 'code'>('preview');
- const [iframeHeight, setIframeHeight] = useState(0);
- const [shouldLoadIframe, setShouldLoadIframe] = useState(false);
- const [cachedHeight, setCachedHeight] = useState(null);
- const [isIframeCached, setIsIframeCached] = useState(false);
-
- const terminalCode = `pnpm dlx shadcn@canary add https://nsui.irung.me/r/${category}-${titleToNumber(title)}.json`;
- const { copied, copy } = useCopyToClipboard({ code: code as string, title, category, eventName: 'block_copy' })
- const { copied: cliCopied, copy: cliCopy } = useCopyToClipboard({ code: terminalCode, title, category, eventName: 'block_cli_copy' })
-
- const ref = useRef(null);
- const isLarge = useMedia('(min-width: 1024px)');
-
- const iframeRef = useRef(null);
- const observer = useRef(null);
- const blockRef = useRef(null);
-
- useEffect(() => {
- observer.current = new IntersectionObserver(
- (entries) => {
- if (entries[0].isIntersecting) {
- setShouldLoadIframe(true);
- observer.current?.disconnect();
- }
- },
- { threshold: 0.1 }
- );
-
- if (blockRef.current) {
- observer.current.observe(blockRef.current);
- }
-
- return () => {
- observer.current?.disconnect();
- };
- }, []);
-
- useEffect(() => {
- const checkCache = async () => {
- try {
- const isCached = await isUrlCached(preview);
- setIsIframeCached(isCached);
- if (isCached) {
- setShouldLoadIframe(true);
- }
- } catch (error) {
- console.error('Error checking cache status:', error);
- }
- };
-
- checkCache();
-
- try {
- const cacheKey = getCacheKey(preview);
- const cached = localStorage.getItem(cacheKey);
- if (cached) {
- const { height, timestamp } = JSON.parse(cached);
- const now = Date.now();
- if (now - timestamp < 24 * 60 * 60 * 1000) {
- setCachedHeight(height);
- setIframeHeight(height);
- }
- }
- } catch (error) {
- console.error('Error retrieving cache:', error);
- }
- }, [preview]);
-
- useEffect(() => {
- const iframe = iframeRef.current;
- if (!iframe || !shouldLoadIframe) return;
-
- const handleLoad = () => {
- try {
- const contentHeight = iframe.contentWindow!.document.body.scrollHeight;
- setIframeHeight(contentHeight);
-
- const cacheKey = getCacheKey(preview);
- const cacheValue = JSON.stringify({
- height: contentHeight,
- timestamp: Date.now(),
- });
- localStorage.setItem(cacheKey, cacheValue);
- } catch (e) {
- console.error('Error accessing iframe content:', e);
- }
- };
-
- iframe.addEventListener('load', handleLoad);
- return () => {
- iframe.removeEventListener('load', handleLoad);
- };
- }, [shouldLoadIframe, preview]);
-
- useEffect(() => {
- if (!blockRef.current || shouldLoadIframe) return;
-
- const linkElement = document.createElement('link');
- linkElement.rel = 'preload';
- linkElement.href = preview;
- linkElement.as = 'document';
-
- if (
- !document.head.querySelector(`link[rel="preload"][href="${preview}"]`)
- ) {
- document.head.appendChild(linkElement);
- }
-
- return () => {
- const existingLink = document.head.querySelector(
- `link[rel="preload"][href="${preview}"]`
- );
- if (existingLink) {
- document.head.removeChild(existingLink);
- }
- };
- }, [preview, shouldLoadIframe]);
-
- return (
-
-
-
-
-
-
- {code && (
- <>
-
- setMode('preview')}
- aria-label="Block preview"
- value="100"
- checked={mode == 'preview'}
- className={radioItem}
- >
-
- Preview
-
-
- setMode('code')}
- aria-label="Code"
- value="0"
- checked={mode == 'code'}
- className={radioItem}
- >
-
- Code
-
-
-
-
- >
- )}
- {previewOnly && (
- <>
- {' '}
- {title}
- {' '}
- >
- )}
- {/* */}
-
-
- {width < MDSIZE
- ? 'Mobile'
- : width < LGSIZE
- ? 'Tablet'
- : 'Desktop'}
- {' '}
-
-
-
- {code && (
- <>
-
-
- {/* */}
-
-
-
- >
- )}
- {!code && (
-
- {/* pnpm dlx shadcn@canary add */}{category}-{titleToNumber(title)}
-
- )}
-
-
-
-
-
-
-
-
-
-
- {
- setWidth(Number(size));
- }}
- defaultSize={DEFAULTSIZE}
- minSize={SMSIZE}
- className="h-fit border-x"
- >
-
- {shouldLoadIframe ? (
-
- ) : (
-
- )}
-
-
-
- {isLarge && (
- <>
-
-
- >
- )}
-
-
-
-
- {/* {mode == 'code' && (
-
- )} */}
-
-
-
-
- );
-};
-
-export default BlockPreview;
diff --git a/src/components/test/credits-test.tsx b/src/components/test/consume-credits-card.tsx
similarity index 62%
rename from src/components/test/credits-test.tsx
rename to src/components/test/consume-credits-card.tsx
index f3bf510..129588f 100644
--- a/src/components/test/credits-test.tsx
+++ b/src/components/test/consume-credits-card.tsx
@@ -6,19 +6,27 @@ import { CoinsIcon } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
-export function CreditsTest() {
- const { data: balance = 0, isLoading } = useCreditBalance();
+const CONSUME_CREDITS = 10;
+
+export function ConsumeCreditsCard() {
+ const { data: balance = 0, isLoading: isLoadingBalance } = useCreditBalance();
const consumeCreditsMutation = useConsumeCredits();
const [loading, setLoading] = useState(false);
+ const hasEnoughCredits = (amount: number) => balance >= amount;
+
const handleConsume = async () => {
+ if (!hasEnoughCredits(CONSUME_CREDITS)) {
+ toast.error('Insufficient credits, please buy more credits.');
+ return;
+ }
setLoading(true);
try {
await consumeCreditsMutation.mutateAsync({
- amount: 10,
- description: 'Test credit consumption',
+ amount: CONSUME_CREDITS,
+ description: `Test credit consumption (${CONSUME_CREDITS} credits)`,
});
- toast.success('10 credits consumed successfully!');
+ toast.success(`${CONSUME_CREDITS} credits consumed successfully!`);
} catch (error) {
toast.error('Failed to consume credits');
} finally {
@@ -39,11 +47,13 @@ export function CreditsTest() {
diff --git a/src/config/website.tsx b/src/config/website.tsx
index 5bd39f0..bb0aa21 100644
--- a/src/config/website.tsx
+++ b/src/config/website.tsx
@@ -8,15 +8,17 @@ import type { WebsiteConfig } from '@/types';
* https://mksaas.com/docs/config/website
*/
export const websiteConfig: WebsiteConfig = {
- metadata: {
+ ui: {
theme: {
defaultTheme: 'default',
enableSwitch: true,
},
mode: {
- defaultMode: 'system',
+ defaultMode: 'dark',
enableSwitch: true,
},
+ },
+ metadata: {
images: {
ogImage: '/og.png',
logoLight: '/logo.png',
@@ -33,7 +35,6 @@ export const websiteConfig: WebsiteConfig = {
},
},
features: {
- enableDiscordWidget: false,
enableUpgradeCard: true,
enableUpdateAvatar: true,
enableAffonsoAffiliate: false,
diff --git a/src/hooks/use-credits.ts b/src/hooks/use-credits.ts
index 2ccae9e..94a0d90 100644
--- a/src/hooks/use-credits.ts
+++ b/src/hooks/use-credits.ts
@@ -2,10 +2,8 @@ import { consumeCreditsAction } from '@/actions/consume-credits';
import { getCreditBalanceAction } from '@/actions/get-credit-balance';
import { getCreditStatsAction } from '@/actions/get-credit-stats';
import { getCreditTransactionsAction } from '@/actions/get-credit-transactions';
-import { useCreditsStore } from '@/stores/credits-store';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type { SortingState } from '@tanstack/react-table';
-import { useEffect } from 'react';
// Query keys
export const creditsKeys = {
@@ -23,9 +21,7 @@ export const creditsKeys = {
// Hook to fetch credit balance
export function useCreditBalance() {
- const updateTrigger = useCreditsStore((state) => state.updateTrigger);
-
- const query = useQuery({
+ return useQuery({
queryKey: creditsKeys.balance(),
queryFn: async () => {
console.log('Fetching credit balance...');
@@ -39,23 +35,11 @@ export function useCreditBalance() {
return result.data.credits || 0;
},
});
-
- // Refetch when updateTrigger changes
- useEffect(() => {
- if (updateTrigger > 0) {
- console.log('Credits update triggered, refetching balance...');
- query.refetch();
- }
- }, [updateTrigger, query]);
-
- return query;
}
// Hook to fetch credit statistics
export function useCreditStats() {
- const updateTrigger = useCreditsStore((state) => state.updateTrigger);
-
- const query = useQuery({
+ return useQuery({
queryKey: creditsKeys.stats(),
queryFn: async () => {
console.log('Fetching credit stats...');
@@ -67,22 +51,11 @@ export function useCreditStats() {
return result.data.data;
},
});
-
- // Refetch when updateTrigger changes
- useEffect(() => {
- if (updateTrigger > 0) {
- console.log('Credits update triggered, refetching stats...');
- query.refetch();
- }
- }, [updateTrigger, query]);
-
- return query;
}
// Hook to consume credits
export function useConsumeCredits() {
const queryClient = useQueryClient();
- const triggerUpdate = useCreditsStore((state) => state.triggerUpdate);
return useMutation({
mutationFn: async ({
@@ -102,9 +75,6 @@ export function useConsumeCredits() {
return result.data;
},
onSuccess: () => {
- // Trigger credits update in store to notify all components
- triggerUpdate();
-
// Invalidate credit balance and stats after consuming credits
queryClient.invalidateQueries({
queryKey: creditsKeys.balance(),
diff --git a/src/lib/serviceWorker.ts b/src/lib/serviceWorker.ts
deleted file mode 100644
index 861b647..0000000
--- a/src/lib/serviceWorker.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Service worker registration and management utilities
- */
-
-// Register the service worker
-export function registerServiceWorker() {
- if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
- window.addEventListener('load', () => {
- navigator.serviceWorker
- .register('/sw.js')
- .then((registration) => {
- console.log('SW registered: ', registration);
- })
- .catch((registrationError) => {
- console.log('SW registration failed: ', registrationError);
- });
- });
- }
-}
-
-// Send a message to the service worker
-type SWMessage = {
- type: string;
- url?: string;
-};
-
-export function sendMessageToSW(message: SWMessage) {
- if (
- typeof window !== 'undefined' &&
- 'serviceWorker' in navigator &&
- navigator.serviceWorker.controller
- ) {
- navigator.serviceWorker.controller.postMessage(message);
- }
-}
-
-// Clear iframe cache for a specific URL or all iframe caches if no URL provided
-export function clearIframeCache(url?: string) {
- sendMessageToSW({
- type: 'CLEAR_IFRAME_CACHE',
- url,
- });
-}
-
-// Update the service worker
-export function updateServiceWorker() {
- if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
- navigator.serviceWorker.ready.then((registration) => {
- registration.update();
- });
- }
-}
-
-// Check if a URL is already cached by the service worker
-export async function isUrlCached(url: string): Promise {
- if (typeof window === 'undefined' || !('caches' in window)) {
- return false;
- }
-
- try {
- const cache = await caches.open('cnblocks-iframe-cache-v1');
- const cachedResponse = await cache.match(url);
- return cachedResponse !== undefined;
- } catch (error) {
- console.error('Error checking cache:', error);
- return false;
- }
-}
diff --git a/src/stores/credits-store.ts b/src/stores/credits-store.ts
deleted file mode 100644
index 3c2f909..0000000
--- a/src/stores/credits-store.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { create } from 'zustand';
-
-interface CreditsState {
- // trigger for credit updates, incremented each time credits change
- updateTrigger: number;
- // method to trigger credit updates
- triggerUpdate: () => void;
-}
-
-/**
- * Credits store for managing credit balance updates.
- *
- * This store provides a simple trigger mechanism to notify components
- * when credits have been consumed or updated, ensuring UI components can
- * refetch the latest credit balance.
- */
-export const useCreditsStore = create((set) => ({
- updateTrigger: 0,
- triggerUpdate: () =>
- set((state) => ({
- updateTrigger: state.updateTrigger + 1,
- })),
-}));
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index beb7ac7..6d0754c 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -6,6 +6,7 @@ import type { CreditPackage } from '@/credits/types';
* website config, without translations
*/
export type WebsiteConfig = {
+ ui: UiConfig;
metadata: MetadataConfig;
features: FeaturesConfig;
routes: RoutesConfig;
@@ -22,12 +23,18 @@ export type WebsiteConfig = {
credits: CreditsConfig;
};
+/**
+ * UI configuration
+ */
+export interface UiConfig {
+ mode?: ModeConfig;
+ theme?: ThemeConfig;
+}
+
/**
* Website metadata
*/
export interface MetadataConfig {
- mode?: ModeConfig;
- theme?: ThemeConfig;
images?: ImagesConfig;
social?: SocialConfig;
}
@@ -69,7 +76,6 @@ export interface SocialConfig {
* Website features
*/
export interface FeaturesConfig {
- enableDiscordWidget?: boolean; // Whether to enable the discord widget, deprecated
enableCrispChat?: boolean; // Whether to enable the crisp chat
enableUpgradeCard?: boolean; // Whether to enable the upgrade card in the sidebar
enableUpdateAvatar?: boolean; // Whether to enable the update avatar in settings
diff --git a/src/types/object-values.ts b/src/types/object-values.ts
deleted file mode 100644
index ef78ef9..0000000
--- a/src/types/object-values.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * ObjectValues is a utility type that extracts a union type of all value types in an object
- *
- * For example, in the AppInfo use case:
- * const AppInfo = {
- * APP_NAME: string,
- * APP_DESCRIPTION: string,
- * PRODUCTION: boolean,
- * VERSION: string
- * } as const
- *
- * type AppInfo = ObjectValues
- * equals to: type AppInfo = string | boolean
- *
- * How it works:
- * 1. keyof T gets the union of all keys in object T
- * 2. T[keyof T] uses indexed access to get all value types
- *
- * Benefits:
- * - Automatically extracts all possible value types from an object
- * - Makes type definitions more precise and automated
- * - Reduces manual type maintenance work
- */
-export type ObjectValues = T[keyof T];