@korioinc/next-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/ads/components/AdContainer.d.ts +12 -0
- package/dist/ads/components/AdContainer.d.ts.map +1 -0
- package/dist/ads/components/AdContainer.js +287 -0
- package/dist/ads/components/GoogleAdSense.d.ts +6 -0
- package/dist/ads/components/GoogleAdSense.d.ts.map +1 -0
- package/dist/ads/components/GoogleAdSense.js +6 -0
- package/dist/ads/components/MockAdDisplay.d.ts +8 -0
- package/dist/ads/components/MockAdDisplay.d.ts.map +1 -0
- package/dist/ads/components/MockAdDisplay.js +13 -0
- package/dist/ads/config.d.ts +3 -0
- package/dist/ads/config.d.ts.map +1 -0
- package/dist/ads/config.js +134 -0
- package/dist/ads/index.d.ts +6 -0
- package/dist/ads/index.d.ts.map +1 -0
- package/dist/ads/index.js +7 -0
- package/dist/ads/types.d.ts +15 -0
- package/dist/ads/types.d.ts.map +1 -0
- package/dist/ads/types.js +1 -0
- package/dist/api-client/index.d.ts +31 -0
- package/dist/api-client/index.d.ts.map +1 -0
- package/dist/api-client/index.js +90 -0
- package/dist/auth/auth-manager.d.ts +45 -0
- package/dist/auth/auth-manager.d.ts.map +1 -0
- package/dist/auth/auth-manager.js +75 -0
- package/dist/auth/index.client.d.ts +7 -0
- package/dist/auth/index.client.d.ts.map +1 -0
- package/dist/auth/index.client.js +5 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +11 -0
- package/dist/auth/index.server.d.ts +7 -0
- package/dist/auth/index.server.d.ts.map +1 -0
- package/dist/auth/index.server.js +5 -0
- package/dist/auth/providers/auth-context-provider.d.ts +21 -0
- package/dist/auth/providers/auth-context-provider.d.ts.map +1 -0
- package/dist/auth/providers/auth-context-provider.js +74 -0
- package/dist/auth/routing.d.ts +3 -0
- package/dist/auth/routing.d.ts.map +1 -0
- package/dist/auth/routing.js +40 -0
- package/dist/auth/types/auth.d.ts +23 -0
- package/dist/auth/types/auth.d.ts.map +1 -0
- package/dist/auth/types/auth.js +1 -0
- package/dist/auth/types/user.d.ts +6 -0
- package/dist/auth/types/user.d.ts.map +1 -0
- package/dist/auth/types/user.js +1 -0
- package/dist/auth/utils/auth.d.ts +32 -0
- package/dist/auth/utils/auth.d.ts.map +1 -0
- package/dist/auth/utils/auth.js +116 -0
- package/dist/auth/utils/permission.d.ts +29 -0
- package/dist/auth/utils/permission.d.ts.map +1 -0
- package/dist/auth/utils/permission.js +44 -0
- package/dist/components/head-manifest/index.d.ts +6 -0
- package/dist/components/head-manifest/index.d.ts.map +1 -0
- package/dist/components/head-manifest/index.js +4 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +7 -0
- package/dist/components/json-ld/index.d.ts +7 -0
- package/dist/components/json-ld/index.d.ts.map +1 -0
- package/dist/components/json-ld/index.js +6 -0
- package/dist/components/locale-switcher/_components/china-flag.d.ts +2 -0
- package/dist/components/locale-switcher/_components/china-flag.d.ts.map +1 -0
- package/dist/components/locale-switcher/_components/china-flag.js +4 -0
- package/dist/components/locale-switcher/_components/japan-flag.d.ts +2 -0
- package/dist/components/locale-switcher/_components/japan-flag.d.ts.map +1 -0
- package/dist/components/locale-switcher/_components/japan-flag.js +4 -0
- package/dist/components/locale-switcher/_components/korea-flag.d.ts +4 -0
- package/dist/components/locale-switcher/_components/korea-flag.d.ts.map +1 -0
- package/dist/components/locale-switcher/_components/korea-flag.js +4 -0
- package/dist/components/locale-switcher/_components/taiwan-flag.d.ts +2 -0
- package/dist/components/locale-switcher/_components/taiwan-flag.d.ts.map +1 -0
- package/dist/components/locale-switcher/_components/taiwan-flag.js +4 -0
- package/dist/components/locale-switcher/_components/usa-flag.d.ts +4 -0
- package/dist/components/locale-switcher/_components/usa-flag.d.ts.map +1 -0
- package/dist/components/locale-switcher/_components/usa-flag.js +4 -0
- package/dist/components/locale-switcher/index.d.ts +11 -0
- package/dist/components/locale-switcher/index.d.ts.map +1 -0
- package/dist/components/locale-switcher/index.js +69 -0
- package/dist/components/theme-switcher/index.d.ts +3 -0
- package/dist/components/theme-switcher/index.d.ts.map +1 -0
- package/dist/components/theme-switcher/index.js +20 -0
- package/dist/components/tooltip/index.d.ts +8 -0
- package/dist/components/tooltip/index.d.ts.map +1 -0
- package/dist/components/tooltip/index.js +40 -0
- package/dist/i18n/init-lingui.d.ts +7 -0
- package/dist/i18n/init-lingui.d.ts.map +1 -0
- package/dist/i18n/init-lingui.js +7 -0
- package/dist/i18n/lingui.setup.d.ts +15 -0
- package/dist/i18n/lingui.setup.d.ts.map +1 -0
- package/dist/i18n/lingui.setup.js +29 -0
- package/dist/i18n/providers/lingui-client-provider.d.ts +9 -0
- package/dist/i18n/providers/lingui-client-provider.d.ts.map +1 -0
- package/dist/i18n/providers/lingui-client-provider.js +14 -0
- package/dist/i18n/routing.d.ts +25 -0
- package/dist/i18n/routing.d.ts.map +1 -0
- package/dist/i18n/routing.js +252 -0
- package/dist/local-storage/index.d.ts +4 -0
- package/dist/local-storage/index.d.ts.map +1 -0
- package/dist/local-storage/index.js +4 -0
- package/dist/local-storage/local-storage-manager.d.ts +41 -0
- package/dist/local-storage/local-storage-manager.d.ts.map +1 -0
- package/dist/local-storage/local-storage-manager.js +200 -0
- package/dist/local-storage/valtio-persist.d.ts +31 -0
- package/dist/local-storage/valtio-persist.d.ts.map +1 -0
- package/dist/local-storage/valtio-persist.js +84 -0
- package/dist/navigation/index.d.ts +4 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +4 -0
- package/dist/navigation/permissions.d.ts +25 -0
- package/dist/navigation/permissions.d.ts.map +1 -0
- package/dist/navigation/permissions.js +97 -0
- package/dist/navigation/types.d.ts +57 -0
- package/dist/navigation/types.d.ts.map +1 -0
- package/dist/navigation/types.js +1 -0
- package/dist/navigation/utils.d.ts +42 -0
- package/dist/navigation/utils.d.ts.map +1 -0
- package/dist/navigation/utils.js +107 -0
- package/dist/utils/cookie.d.ts +3 -0
- package/dist/utils/cookie.d.ts.map +1 -0
- package/dist/utils/cookie.js +22 -0
- package/dist/utils/helpers.d.ts +5 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +18 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/sitemap.d.ts +13 -0
- package/dist/utils/sitemap.d.ts.map +1 -0
- package/dist/utils/sitemap.js +19 -0
- package/package.json +126 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Korio Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @korioinc/next-core
|
|
2
|
+
|
|
3
|
+
Core utilities and components for Next.js applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @korioinc/next-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Authentication**: Complete auth system with JWT support
|
|
14
|
+
- **Internationalization**: i18n routing and locale management
|
|
15
|
+
- **Components**: Reusable UI components (Theme switcher, Locale switcher)
|
|
16
|
+
- **Ads Management**: Ad container components and configurations
|
|
17
|
+
- **API Client**: Configured fetch client with interceptors
|
|
18
|
+
- **Navigation**: Type-safe navigation system
|
|
19
|
+
- **Local Storage**: Enhanced local storage management
|
|
20
|
+
- **Utilities**: Common utility functions
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Authentication
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { useAuthContext } from '@korioinc/next-core/auth/client';
|
|
28
|
+
import { getCurrentUser } from '@korioinc/next-core/auth/server';
|
|
29
|
+
|
|
30
|
+
// Client-side
|
|
31
|
+
const { isLogin, user, login, logout } = useAuthContext();
|
|
32
|
+
|
|
33
|
+
// Server-side
|
|
34
|
+
const user = await getCurrentUser();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Internationalization
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { handleLocaleRouting } from '@korioinc/next-core/i18n/routing';
|
|
41
|
+
|
|
42
|
+
// In middleware.ts
|
|
43
|
+
export default function middleware(request: NextRequest) {
|
|
44
|
+
return handleLocaleRouting(request);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Components
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { ThemeSwitcher, LocaleSwitcher } from '@korioinc/next-core/components';
|
|
52
|
+
|
|
53
|
+
export function Header() {
|
|
54
|
+
return (
|
|
55
|
+
<header>
|
|
56
|
+
<ThemeSwitcher />
|
|
57
|
+
<LocaleSwitcher />
|
|
58
|
+
</header>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Ads
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { AdContainer } from '@korioinc/next-core/ads';
|
|
67
|
+
|
|
68
|
+
export function Page() {
|
|
69
|
+
return (
|
|
70
|
+
<AdContainer
|
|
71
|
+
adType="leaderboard"
|
|
72
|
+
className="my-4"
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Requirements
|
|
79
|
+
|
|
80
|
+
- Next.js 15+
|
|
81
|
+
- React 19+
|
|
82
|
+
- TypeScript 5.9+
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
87
|
+
|
|
88
|
+
## Contributing
|
|
89
|
+
|
|
90
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AdContainerProps } from '../../ads/types';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
adsbygoogle: Array<{
|
|
5
|
+
push?: (params: Record<string, unknown>) => void;
|
|
6
|
+
loaded?: boolean;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export default function AdContainer({ adType, className }: AdContainerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
//# sourceMappingURL=AdContainer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdContainer.d.ts","sourceRoot":"","sources":["../../../src/ads/components/AdContainer.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAY,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AA0B7D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,WAAW,EAAE,KAAK,CAAC;YACjB,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;YACjD,MAAM,CAAC,EAAE,OAAO,CAAC;YACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;SACxB,CAAC,CAAC;KACJ;CACF;AA+ED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,MAAM,EAAE,SAAc,EAAE,EAAE,gBAAgB,kDA2P/E"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import MockAdDisplay from '../../ads/components/MockAdDisplay';
|
|
6
|
+
import { AD_CONFIGS } from '../../ads/config';
|
|
7
|
+
import { cn } from '../../utils/helpers';
|
|
8
|
+
import { getConfig } from '@korioinc/next-conf';
|
|
9
|
+
// Constants for ad loading configuration
|
|
10
|
+
const AD_LOADING_CONFIG = {
|
|
11
|
+
MAX_RETRIES: 10,
|
|
12
|
+
RETRY_DELAY_MS: 200,
|
|
13
|
+
INITIAL_DELAY_MS: 100,
|
|
14
|
+
MIN_CONTAINER_WIDTH: 250,
|
|
15
|
+
CONTAINER_CHECK_DELAY_MS: 500,
|
|
16
|
+
SCRIPT_LOAD_RETRY_DELAY_MS: 1000,
|
|
17
|
+
INTERSECTION_THRESHOLD: 0.51,
|
|
18
|
+
};
|
|
19
|
+
// Ad status constants
|
|
20
|
+
const AD_STATUS = {
|
|
21
|
+
DONE: 'done',
|
|
22
|
+
};
|
|
23
|
+
// Ad formats
|
|
24
|
+
const AD_FORMAT = {
|
|
25
|
+
RECTANGLE: 'rectangle',
|
|
26
|
+
AUTO: 'auto',
|
|
27
|
+
};
|
|
28
|
+
// Helper function to check if environment is development
|
|
29
|
+
const isDevelopmentEnvironment = (config, publisherId) => {
|
|
30
|
+
return (!publisherId ||
|
|
31
|
+
!config.slotId ||
|
|
32
|
+
process.env.NEXT_PUBLIC_APP_ENV === 'local' ||
|
|
33
|
+
process.env.NEXT_PUBLIC_APP_ENV === 'development');
|
|
34
|
+
};
|
|
35
|
+
// Helper function to clean up ad element
|
|
36
|
+
const cleanupAdElement = (container) => {
|
|
37
|
+
if (!container)
|
|
38
|
+
return;
|
|
39
|
+
const adElement = container.querySelector('ins.adsbygoogle');
|
|
40
|
+
if (!adElement)
|
|
41
|
+
return;
|
|
42
|
+
// More thorough cleanup
|
|
43
|
+
adElement.innerHTML = '';
|
|
44
|
+
adElement.removeAttribute('data-adsbygoogle-status');
|
|
45
|
+
adElement.removeAttribute('data-ad-status');
|
|
46
|
+
// Clone to remove any event listeners
|
|
47
|
+
const clonedElement = adElement.cloneNode(true);
|
|
48
|
+
adElement.parentNode?.replaceChild(clonedElement, adElement);
|
|
49
|
+
};
|
|
50
|
+
// Helper function to create ad element
|
|
51
|
+
const createAdElement = (config, adInstanceId, publisherId) => {
|
|
52
|
+
const ins = document.createElement('ins');
|
|
53
|
+
ins.className = 'adsbygoogle';
|
|
54
|
+
ins.setAttribute('data-ad-client', `ca-pub-${publisherId}`);
|
|
55
|
+
ins.setAttribute('data-ad-slot', config.slotId);
|
|
56
|
+
if (config.layout === 'in-article') {
|
|
57
|
+
ins.setAttribute('data-ad-format', AD_FORMAT.RECTANGLE);
|
|
58
|
+
}
|
|
59
|
+
ins.setAttribute('data-ad-instance-id', adInstanceId);
|
|
60
|
+
// Apply inline styles as per Google AdSense guidelines
|
|
61
|
+
ins.style.display = 'inline-block';
|
|
62
|
+
ins.style.width = `${config.width}px`;
|
|
63
|
+
ins.style.height = `${config.height}px`;
|
|
64
|
+
return ins;
|
|
65
|
+
};
|
|
66
|
+
// Helper function to check if container is valid for ad loading
|
|
67
|
+
const isContainerValid = (container) => {
|
|
68
|
+
if (!container)
|
|
69
|
+
return false;
|
|
70
|
+
// Check if element is visible
|
|
71
|
+
if (window.getComputedStyle(container).display === 'none') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// Check if ins element already exists and is loaded
|
|
75
|
+
const existingIns = container.querySelector('ins.adsbygoogle');
|
|
76
|
+
if (existingIns?.getAttribute('data-adsbygoogle-status') === AD_STATUS.DONE) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
// Helper function to push ad to adsbygoogle
|
|
82
|
+
const pushAdToGoogle = () => {
|
|
83
|
+
if (window.adsbygoogle) {
|
|
84
|
+
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
export default function AdContainer({ adType, className = '' }) {
|
|
90
|
+
const config = AD_CONFIGS[adType];
|
|
91
|
+
if (!config) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
// Get publisher ID from config
|
|
95
|
+
const adsensePublisherId = getConfig().analytics?.adsensePublisherId;
|
|
96
|
+
// Check if dev environment early - check this BEFORE checking publisherId
|
|
97
|
+
const isDevEnvironment = isDevelopmentEnvironment(config, adsensePublisherId);
|
|
98
|
+
// Only check publisherId for production environment
|
|
99
|
+
if (!isDevEnvironment && !adsensePublisherId) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// Get alignment class for Tailwind
|
|
103
|
+
const getAlignmentClass = () => {
|
|
104
|
+
if (config.alignment === 'left')
|
|
105
|
+
return 'justify-start';
|
|
106
|
+
if (config.alignment === 'right')
|
|
107
|
+
return 'justify-end';
|
|
108
|
+
return 'justify-center';
|
|
109
|
+
};
|
|
110
|
+
// Dev environment: Return mock ad without any refs or hooks
|
|
111
|
+
if (isDevEnvironment) {
|
|
112
|
+
// Two divs: outer for size reservation (layout shift prevention), inner for flex alignment
|
|
113
|
+
const outerStyle = {
|
|
114
|
+
minHeight: `${config.height}px`,
|
|
115
|
+
'--ad-width': `${config.width}px`,
|
|
116
|
+
'--ad-height': `${config.height}px`,
|
|
117
|
+
};
|
|
118
|
+
const innerStyle = {
|
|
119
|
+
display: 'flex',
|
|
120
|
+
justifyContent: config.alignment === 'left' ? 'flex-start' : config.alignment === 'right' ? 'flex-end' : 'center',
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
};
|
|
123
|
+
return (_jsx("div", { className: cn('relative', className), style: outerStyle, children: _jsx("div", { style: innerStyle, children: _jsx(MockAdDisplay, { adType: adType, config: config }) }) }));
|
|
124
|
+
}
|
|
125
|
+
// Production-only setup (refs and hooks only created when needed)
|
|
126
|
+
const containerRef = useRef(null);
|
|
127
|
+
const pathname = usePathname();
|
|
128
|
+
const adInstanceRef = useRef(false);
|
|
129
|
+
const retryTimerRef = useRef(null);
|
|
130
|
+
const intersectionObserverRef = useRef(null);
|
|
131
|
+
// Unique identifier for debugging and tracking
|
|
132
|
+
const adInstanceId = adType;
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
// Reset on route change
|
|
135
|
+
adInstanceRef.current = false;
|
|
136
|
+
// Clean up existing ads and timers
|
|
137
|
+
if (retryTimerRef.current) {
|
|
138
|
+
clearTimeout(retryTimerRef.current);
|
|
139
|
+
retryTimerRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
cleanupAdElement(containerRef.current);
|
|
142
|
+
}, [pathname]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (adInstanceRef.current)
|
|
145
|
+
return;
|
|
146
|
+
// Helper function to handle container width validation
|
|
147
|
+
const validateContainerWidth = () => {
|
|
148
|
+
const containerWidth = containerRef.current?.offsetWidth || 0;
|
|
149
|
+
if (containerWidth === 0) {
|
|
150
|
+
retryTimerRef.current = setTimeout(() => {
|
|
151
|
+
if (!adInstanceRef.current && containerRef.current) {
|
|
152
|
+
const retryWidth = containerRef.current.offsetWidth;
|
|
153
|
+
if (retryWidth > 0) {
|
|
154
|
+
loadAd();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}, AD_LOADING_CONFIG.CONTAINER_CHECK_DELAY_MS);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// For in-article ads, ensure minimum width
|
|
161
|
+
if (config.layout === 'in-article' && containerWidth < AD_LOADING_CONFIG.MIN_CONTAINER_WIDTH) {
|
|
162
|
+
console.warn(`AdSense: Container too narrow for fluid ads (${containerWidth}px). Using fixed size.`);
|
|
163
|
+
}
|
|
164
|
+
return containerWidth;
|
|
165
|
+
};
|
|
166
|
+
// Helper function to handle existing ad element
|
|
167
|
+
const handleExistingAdElement = () => {
|
|
168
|
+
const existingIns = containerRef.current?.querySelector('ins.adsbygoogle');
|
|
169
|
+
if (!existingIns)
|
|
170
|
+
return true;
|
|
171
|
+
// Check if ad is already loaded
|
|
172
|
+
if (existingIns.getAttribute('data-adsbygoogle-status') === AD_STATUS.DONE) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
// Remove existing ins if it exists but hasn't loaded
|
|
176
|
+
existingIns.remove();
|
|
177
|
+
return true;
|
|
178
|
+
};
|
|
179
|
+
// Helper function to initialize ad with AdSense
|
|
180
|
+
const initializeAd = () => {
|
|
181
|
+
if (!window.adsbygoogle) {
|
|
182
|
+
// Retry after a delay
|
|
183
|
+
retryTimerRef.current = setTimeout(() => {
|
|
184
|
+
if (window.adsbygoogle && containerRef.current?.querySelector('ins.adsbygoogle')) {
|
|
185
|
+
if (pushAdToGoogle()) {
|
|
186
|
+
adInstanceRef.current = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}, AD_LOADING_CONFIG.SCRIPT_LOAD_RETRY_DELAY_MS);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Now push will only process this newly created ins element
|
|
193
|
+
if (pushAdToGoogle()) {
|
|
194
|
+
adInstanceRef.current = true;
|
|
195
|
+
// Disconnect observer after successful load
|
|
196
|
+
if (intersectionObserverRef.current) {
|
|
197
|
+
intersectionObserverRef.current.disconnect();
|
|
198
|
+
intersectionObserverRef.current = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const loadAd = () => {
|
|
203
|
+
try {
|
|
204
|
+
// Validate container
|
|
205
|
+
if (!isContainerValid(containerRef.current)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Handle existing ad element
|
|
209
|
+
if (!handleExistingAdElement()) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Validate container width
|
|
213
|
+
const containerWidth = validateContainerWidth();
|
|
214
|
+
if (containerWidth === null) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// Create and append ad element (publisherId is guaranteed to exist in production)
|
|
218
|
+
if (!adsensePublisherId) {
|
|
219
|
+
console.warn('AdSense: Publisher ID is missing in production environment');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const ins = createAdElement(config, adInstanceId, adsensePublisherId);
|
|
223
|
+
containerRef.current?.appendChild(ins);
|
|
224
|
+
// Initialize ad with AdSense
|
|
225
|
+
initializeAd();
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.error('AdSense loading error:', {
|
|
229
|
+
adType,
|
|
230
|
+
error: err,
|
|
231
|
+
adInstanceId,
|
|
232
|
+
containerWidth: containerRef.current?.offsetWidth || 0,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
// Set up Intersection Observer to load ad when threshold is met
|
|
237
|
+
if (containerRef.current) {
|
|
238
|
+
intersectionObserverRef.current = new IntersectionObserver((entries) => {
|
|
239
|
+
entries.forEach((entry) => {
|
|
240
|
+
if (entry.isIntersecting &&
|
|
241
|
+
entry.intersectionRatio >= AD_LOADING_CONFIG.INTERSECTION_THRESHOLD &&
|
|
242
|
+
!adInstanceRef.current) {
|
|
243
|
+
// Container is at least threshold% visible, load the ad
|
|
244
|
+
loadAd();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}, {
|
|
248
|
+
threshold: [AD_LOADING_CONFIG.INTERSECTION_THRESHOLD],
|
|
249
|
+
});
|
|
250
|
+
intersectionObserverRef.current.observe(containerRef.current);
|
|
251
|
+
}
|
|
252
|
+
return () => {
|
|
253
|
+
// Clean up observer
|
|
254
|
+
if (intersectionObserverRef.current) {
|
|
255
|
+
intersectionObserverRef.current.disconnect();
|
|
256
|
+
intersectionObserverRef.current = null;
|
|
257
|
+
}
|
|
258
|
+
// Clean up timer
|
|
259
|
+
if (retryTimerRef.current) {
|
|
260
|
+
clearTimeout(retryTimerRef.current);
|
|
261
|
+
retryTimerRef.current = null;
|
|
262
|
+
}
|
|
263
|
+
// Clean up dynamically created elements
|
|
264
|
+
if (containerRef.current) {
|
|
265
|
+
const insElement = containerRef.current.querySelector('ins.adsbygoogle');
|
|
266
|
+
if (insElement && insElement.getAttribute('data-adsbygoogle-status') !== AD_STATUS.DONE) {
|
|
267
|
+
insElement.remove();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Reset instance flag for Strict Mode
|
|
271
|
+
adInstanceRef.current = false;
|
|
272
|
+
};
|
|
273
|
+
}, [pathname]);
|
|
274
|
+
// Production environment: Return container for ads
|
|
275
|
+
// Use two divs structure: outer for size reservation (layout shift prevention), inner for flex alignment and ad insertion
|
|
276
|
+
const outerStyle = {
|
|
277
|
+
minHeight: `${config.height}px`,
|
|
278
|
+
'--ad-width': `${config.width}px`,
|
|
279
|
+
'--ad-height': `${config.height}px`,
|
|
280
|
+
};
|
|
281
|
+
const innerStyle = {
|
|
282
|
+
display: 'flex',
|
|
283
|
+
justifyContent: config.alignment === 'left' ? 'flex-start' : config.alignment === 'right' ? 'flex-end' : 'center',
|
|
284
|
+
alignItems: 'center',
|
|
285
|
+
};
|
|
286
|
+
return (_jsx("div", { className: cn('relative', className), style: outerStyle, children: _jsx("div", { ref: containerRef, style: innerStyle }) }));
|
|
287
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GoogleAdSense.d.ts","sourceRoot":"","sources":["../../../src/ads/components/GoogleAdSense.tsx"],"names":[],"mappings":"AAIA,UAAU,kBAAkB;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EAAE,WAAW,EAAE,EAAE,kBAAkB,2CASxE"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import Script from 'next/script';
|
|
4
|
+
export default function GoogleAdSense({ publisherId }) {
|
|
5
|
+
return (_jsx(Script, { async: true, src: `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-${publisherId}`, crossOrigin: 'anonymous', strategy: 'lazyOnload' }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AdConfig, AdType } from '../../ads/types';
|
|
2
|
+
interface MockAdDisplayProps {
|
|
3
|
+
adType: AdType;
|
|
4
|
+
config: AdConfig;
|
|
5
|
+
}
|
|
6
|
+
export default function MockAdDisplay({ adType, config }: MockAdDisplayProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=MockAdDisplay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MockAdDisplay.d.ts","sourceRoot":"","sources":["../../../src/ads/components/MockAdDisplay.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnD,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,kBAAkB,2CAqB3E"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export default function MockAdDisplay({ adType, config }) {
|
|
3
|
+
// Mock ad box styles
|
|
4
|
+
const mockAdStyles = {
|
|
5
|
+
width: `${config.width}px`,
|
|
6
|
+
height: `${config.height}px`,
|
|
7
|
+
maxWidth: '100%',
|
|
8
|
+
display: 'flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
};
|
|
12
|
+
return (_jsx("div", { className: 'border-border bg-secondary relative border-2 border-dashed', style: mockAdStyles, children: _jsxs("div", { className: 'text-center', children: [_jsx("p", { className: 'text-muted-foreground font-semibold', children: adType }), _jsxs("p", { className: 'text-muted-foreground text-sm', children: [config.width, " x ", config.height] })] }) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/ads/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnD,eAAO,MAAM,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CA4IxD,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const AD_CONFIGS = {
|
|
2
|
+
// Sticky 좌우 광고
|
|
3
|
+
// 좌측: left-rail-1, left-rail-2 세로로 순서대로 배치, 우측정렬
|
|
4
|
+
// 우측: right-rail-1, right-rail-2, 좌측정렬
|
|
5
|
+
'left-rail-1': {
|
|
6
|
+
slotId: '4944477902',
|
|
7
|
+
width: 300,
|
|
8
|
+
height: 250,
|
|
9
|
+
layout: 'display',
|
|
10
|
+
alignment: 'right',
|
|
11
|
+
},
|
|
12
|
+
'left-rail-2': {
|
|
13
|
+
slotId: '6756807933',
|
|
14
|
+
width: 300,
|
|
15
|
+
height: 600,
|
|
16
|
+
layout: 'display',
|
|
17
|
+
alignment: 'right',
|
|
18
|
+
},
|
|
19
|
+
'right-rail-1': {
|
|
20
|
+
slotId: '6134254736',
|
|
21
|
+
width: 300,
|
|
22
|
+
height: 250,
|
|
23
|
+
layout: 'display',
|
|
24
|
+
alignment: 'left',
|
|
25
|
+
},
|
|
26
|
+
'right-rail-2': {
|
|
27
|
+
slotId: '5299701120',
|
|
28
|
+
width: 300,
|
|
29
|
+
height: 600,
|
|
30
|
+
layout: 'display',
|
|
31
|
+
alignment: 'left',
|
|
32
|
+
},
|
|
33
|
+
// 페이지 최상단 광고
|
|
34
|
+
// 모든 페이지의 컨텐츠 최상단에 GNB 바로 아래에 있는 광고는 leaderboard
|
|
35
|
+
// in-feed 면 안됨
|
|
36
|
+
leaderboard: {
|
|
37
|
+
slotId: '7112316569',
|
|
38
|
+
width: 728,
|
|
39
|
+
height: 90,
|
|
40
|
+
layout: 'display',
|
|
41
|
+
},
|
|
42
|
+
// 페이지 중간중간에 삽입되는 가로로 길쭉한 광고
|
|
43
|
+
// in-feed-1 부터 5까지
|
|
44
|
+
// 동일한 숫자의 in feed 가 중복으로 보여지면 안됨. 항상 1부터 순서대로 유니크하게 보여야함
|
|
45
|
+
'in-feed-1': {
|
|
46
|
+
slotId: '1627624393',
|
|
47
|
+
width: 728,
|
|
48
|
+
height: 90,
|
|
49
|
+
layout: 'in-article',
|
|
50
|
+
},
|
|
51
|
+
'in-feed-2': {
|
|
52
|
+
slotId: '4328991117',
|
|
53
|
+
width: 728,
|
|
54
|
+
height: 90,
|
|
55
|
+
layout: 'in-article',
|
|
56
|
+
},
|
|
57
|
+
'in-feed-3': {
|
|
58
|
+
slotId: '3937290639',
|
|
59
|
+
width: 728,
|
|
60
|
+
height: 90,
|
|
61
|
+
layout: 'in-article',
|
|
62
|
+
},
|
|
63
|
+
'in-feed-4': {
|
|
64
|
+
slotId: '3015909448',
|
|
65
|
+
width: 728,
|
|
66
|
+
height: 90,
|
|
67
|
+
layout: 'in-article',
|
|
68
|
+
},
|
|
69
|
+
'in-feed-5': {
|
|
70
|
+
slotId: '2903410560',
|
|
71
|
+
width: 728,
|
|
72
|
+
height: 90,
|
|
73
|
+
layout: 'in-article',
|
|
74
|
+
},
|
|
75
|
+
// 페이지 중간중간에 가로가 짧은 단에 삽입되는 정사각형 광고
|
|
76
|
+
// in-feed-left-1 또는 in-feed-right-1
|
|
77
|
+
// left-rail-1 이나 right rail 은 쓰면 안됨
|
|
78
|
+
'in-feed-left-1': {
|
|
79
|
+
slotId: '4024920541',
|
|
80
|
+
width: 300,
|
|
81
|
+
height: 250,
|
|
82
|
+
layout: 'in-article',
|
|
83
|
+
},
|
|
84
|
+
'in-feed-right-1': {
|
|
85
|
+
slotId: '5945325110',
|
|
86
|
+
width: 300,
|
|
87
|
+
height: 250,
|
|
88
|
+
layout: 'in-article',
|
|
89
|
+
},
|
|
90
|
+
// 페이지 맨 하단에 나오는 광고
|
|
91
|
+
// 페이지 맨 하단에 푸터 상단에 있는 광고는 항상 footer 여야함
|
|
92
|
+
// in-feed 쓰면 안됨
|
|
93
|
+
footer: {
|
|
94
|
+
slotId: '1576078538',
|
|
95
|
+
width: 970,
|
|
96
|
+
height: 250,
|
|
97
|
+
layout: 'display',
|
|
98
|
+
},
|
|
99
|
+
// 매우 큰 광고
|
|
100
|
+
// 홈인 경우: home-billboard
|
|
101
|
+
// 홈 아닌경우: billboard
|
|
102
|
+
'home-billboard': {
|
|
103
|
+
slotId: '9277247223',
|
|
104
|
+
width: 970,
|
|
105
|
+
height: 250,
|
|
106
|
+
layout: 'display',
|
|
107
|
+
},
|
|
108
|
+
billboard: {
|
|
109
|
+
slotId: '9614572420',
|
|
110
|
+
width: 970,
|
|
111
|
+
height: 250,
|
|
112
|
+
layout: 'display',
|
|
113
|
+
},
|
|
114
|
+
// 페이지 제일 상단에 있는 광고 (정사각형)
|
|
115
|
+
'mobile-unit-1': {
|
|
116
|
+
slotId: '2376910921',
|
|
117
|
+
width: 300,
|
|
118
|
+
height: 250,
|
|
119
|
+
layout: 'display',
|
|
120
|
+
},
|
|
121
|
+
// 페이지 제일 하단에 있는 광고
|
|
122
|
+
'mobile-unit-2': {
|
|
123
|
+
slotId: '9238946133',
|
|
124
|
+
width: 300,
|
|
125
|
+
height: 250,
|
|
126
|
+
layout: 'display',
|
|
127
|
+
},
|
|
128
|
+
'mobile-in-feed-1': {
|
|
129
|
+
slotId: '7925864461',
|
|
130
|
+
width: 300,
|
|
131
|
+
height: 250,
|
|
132
|
+
layout: 'in-article',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as GoogleAdSense } from './components/GoogleAdSense';
|
|
2
|
+
export { default as AdContainer } from './components/AdContainer';
|
|
3
|
+
export { default as MockAdDisplay } from './components/MockAdDisplay';
|
|
4
|
+
export * from './types';
|
|
5
|
+
export * from './config';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ads/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGtE,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { default as GoogleAdSense } from './components/GoogleAdSense';
|
|
3
|
+
export { default as AdContainer } from './components/AdContainer';
|
|
4
|
+
export { default as MockAdDisplay } from './components/MockAdDisplay';
|
|
5
|
+
// Types and configuration
|
|
6
|
+
export * from './types';
|
|
7
|
+
export * from './config';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type AdLayout = 'display' | 'in-article';
|
|
2
|
+
export type AdType = 'left-rail-1' | 'left-rail-2' | 'right-rail-1' | 'right-rail-2' | 'leaderboard' | 'in-feed-1' | 'in-feed-2' | 'in-feed-3' | 'in-feed-4' | 'in-feed-5' | 'in-feed-left-1' | 'in-feed-right-1' | 'footer' | 'home-billboard' | 'billboard' | 'mobile-unit-1' | 'mobile-unit-2' | 'mobile-in-feed-1';
|
|
3
|
+
export type AdAlignment = 'left' | 'center' | 'right';
|
|
4
|
+
export interface AdConfig {
|
|
5
|
+
slotId: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
layout: AdLayout;
|
|
9
|
+
alignment?: AdAlignment;
|
|
10
|
+
}
|
|
11
|
+
export interface AdContainerProps {
|
|
12
|
+
adType: AdType;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ads/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,YAAY,CAAC;AAEhD,MAAM,MAAM,MAAM,GAEd,aAAa,GACb,aAAa,GACb,cAAc,GACd,cAAc,GAEd,aAAa,GAEb,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GAEX,gBAAgB,GAChB,iBAAiB,GAEjB,QAAQ,GAER,gBAAgB,GAChB,WAAW,GAEX,eAAe,GACf,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,CAAC;IACjB,SAAS,CAAC,EAAE,WAAW,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|