mirror of
https://github.com/chinchang/web-maker.git
synced 2025-08-04 20:37:29 +02:00
add layout option in share modal
This commit is contained in:
@@ -18,6 +18,55 @@ const TOGGLE_VISIBILITY_API =
|
|||||||
? 'http://127.0.0.1:5001/web-maker-app/us-central1/toggleVisibility'
|
? 'http://127.0.0.1:5001/web-maker-app/us-central1/toggleVisibility'
|
||||||
: */ 'https://togglevisibility-ajhkrtmkaq-uc.a.run.app';
|
: */ 'https://togglevisibility-ajhkrtmkaq-uc.a.run.app';
|
||||||
|
|
||||||
|
// Layout mode definitions with icons
|
||||||
|
const LAYOUT_MODES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Preview on right',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
|
||||||
|
<use xlinkHref="#mode-icon" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Preview on bottom',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 100 100">
|
||||||
|
<use xlinkHref="#mode-icon" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Preview on left',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
|
||||||
|
<use xlinkHref="#mode-icon" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Full screen preview',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 100 100">
|
||||||
|
<rect x="0" y="0" width="100" height="100" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Three-pane horizontal',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 100 100">
|
||||||
|
<use xlinkHref="#vertical-mode-icon" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export function Share({
|
export function Share({
|
||||||
user,
|
user,
|
||||||
item,
|
item,
|
||||||
@@ -26,6 +75,8 @@ export function Share({
|
|||||||
onProBtnClick
|
onProBtnClick
|
||||||
}) {
|
}) {
|
||||||
const [publicItemCount, setPublicItemCount] = useState();
|
const [publicItemCount, setPublicItemCount] = useState();
|
||||||
|
const [selectedLayout, setSelectedLayout] = useState(2); // Default to layout 2
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
window.db.getPublicItemCount(user.uid).then(c => {
|
window.db.getPublicItemCount(user.uid).then(c => {
|
||||||
@@ -75,10 +126,16 @@ export function Share({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getShareUrl = () => {
|
||||||
|
const baseUrl = `${BASE_URL}/create/${item.id}`;
|
||||||
|
return selectedLayout ? `${baseUrl}?layout=${selectedLayout}` : baseUrl;
|
||||||
|
};
|
||||||
|
|
||||||
const copyUrl = () => {
|
const copyUrl = () => {
|
||||||
navigator.clipboard.writeText(`${BASE_URL}/create/${item.id}`);
|
navigator.clipboard.writeText(getShareUrl());
|
||||||
alertsService.add('URL copied to clipboard');
|
alertsService.add('URL copied to clipboard');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<HStack justify="center" gap={2}>
|
<HStack justify="center" gap={2}>
|
||||||
@@ -106,19 +163,70 @@ export function Share({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{item.isPublic && (
|
{item.isPublic && (
|
||||||
<p>
|
<VStack gap={2} align="stretch">
|
||||||
Public at{' '}
|
<p>
|
||||||
<a href={`${BASE_URL}/create/${item.id}`} target="_blank">
|
Public at{' '}
|
||||||
{BASE_URL}/create/{item.id}
|
<a href={getShareUrl()} target="_blank">
|
||||||
</a>{' '}
|
{getShareUrl()}
|
||||||
<Button
|
</a>{' '}
|
||||||
class="btn btn--dark hint--bottom hint--rounded"
|
<Button
|
||||||
onClick={copyUrl}
|
class="btn btn--dark hint--bottom hint--rounded"
|
||||||
aria-label="Copy"
|
onClick={copyUrl}
|
||||||
>
|
aria-label="Copy"
|
||||||
<Icon name="copy" /> Copy
|
>
|
||||||
</Button>
|
<Icon name="copy" /> Copy
|
||||||
</p>
|
</Button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Layout Mode Selector */}
|
||||||
|
<HStack gap={3} align="center">
|
||||||
|
<label style="display: block; font-weight: 500;">
|
||||||
|
Layout Mode:
|
||||||
|
</label>
|
||||||
|
<HStack gap={1} align="center">
|
||||||
|
{LAYOUT_MODES.map(layout => (
|
||||||
|
<label
|
||||||
|
key={layout.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '8px',
|
||||||
|
border:
|
||||||
|
selectedLayout === layout.id
|
||||||
|
? '2px solid var(--clr-brand-2)'
|
||||||
|
: '2px solid transparent',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="layout-mode"
|
||||||
|
value={layout.id}
|
||||||
|
checked={selectedLayout === layout.id}
|
||||||
|
onChange={e =>
|
||||||
|
setSelectedLayout(parseInt(e.target.value, 10))
|
||||||
|
}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fill:
|
||||||
|
selectedLayout === layout.id
|
||||||
|
? 'var(--clr-brand-2)'
|
||||||
|
: 'currentColor',
|
||||||
|
width: '26px',
|
||||||
|
height: '26px',
|
||||||
|
display: 'flex'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{layout.icon}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
--color-text-light: #b0a5d6;
|
--color-text-light: #b0a5d6;
|
||||||
--color-text-lightest-final: #787090;
|
--color-text-lightest-final: #787090;
|
||||||
--clr-brand: purple;
|
--clr-brand: purple;
|
||||||
|
--clr-brand-2: #e3ba26;
|
||||||
|
|
||||||
--color-pro-1: #1fffb3;
|
--color-pro-1: #1fffb3;
|
||||||
--color-pro-2: #f2ff00;
|
--color-pro-2: #f2ff00;
|
||||||
@@ -808,7 +809,7 @@ li.CodeMirror-hint-active {
|
|||||||
/*color: white;*/
|
/*color: white;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-type="css"] .cm-error {
|
[data-type='css'] .cm-error {
|
||||||
background: inherit !important;
|
background: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -54,15 +54,48 @@ describe('Layout Parameter Tests', () => {
|
|||||||
const params = new URLSearchParams(global.window.location.search);
|
const params = new URLSearchParams(global.window.location.search);
|
||||||
global.window.codeHtml = params.get('html') || '';
|
global.window.codeHtml = params.get('html') || '';
|
||||||
global.window.codeCss = params.get('css') || '';
|
global.window.codeCss = params.get('css') || '';
|
||||||
global.window.codeLayout = params.get('layout')
|
global.window.codeLayout = (() => {
|
||||||
? parseInt(params.get('layout'), 10)
|
const layout = params.get('layout');
|
||||||
: null;
|
if (!layout) return null;
|
||||||
|
if (layout === 'full') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const _val = parseInt(layout, 10);
|
||||||
|
if (_val >= 1 && _val <= 5) {
|
||||||
|
return _val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(global.window.codeLayout).toBe(3);
|
expect(global.window.codeLayout).toBe(3);
|
||||||
expect(global.window.codeHtml).toBe('<div>test</div>');
|
expect(global.window.codeHtml).toBe('<div>test</div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should handle "full" alias for layout mode 4', () => {
|
||||||
|
// Set up URL with "full" layout parameter
|
||||||
|
global.window.location.search = '?layout=full&html=<div>test</div>';
|
||||||
|
|
||||||
|
// Simulate the URL parsing logic
|
||||||
|
if (global.window.location.search) {
|
||||||
|
const params = new URLSearchParams(global.window.location.search);
|
||||||
|
global.window.codeLayout = (() => {
|
||||||
|
const layout = params.get('layout');
|
||||||
|
if (!layout) return null;
|
||||||
|
if (layout === 'full') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const _val = parseInt(layout, 10);
|
||||||
|
if (_val >= 1 && _val <= 5) {
|
||||||
|
return _val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(global.window.codeLayout).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
test('should handle invalid layout parameter gracefully', () => {
|
test('should handle invalid layout parameter gracefully', () => {
|
||||||
// Set up URL with invalid layout parameter
|
// Set up URL with invalid layout parameter
|
||||||
global.window.location.search = '?layout=invalid&html=<div>test</div>';
|
global.window.location.search = '?layout=invalid&html=<div>test</div>';
|
||||||
@@ -70,12 +103,21 @@ describe('Layout Parameter Tests', () => {
|
|||||||
// Simulate the URL parsing logic
|
// Simulate the URL parsing logic
|
||||||
if (global.window.location.search) {
|
if (global.window.location.search) {
|
||||||
const params = new URLSearchParams(global.window.location.search);
|
const params = new URLSearchParams(global.window.location.search);
|
||||||
global.window.codeLayout = params.get('layout')
|
global.window.codeLayout = (() => {
|
||||||
? parseInt(params.get('layout'), 10)
|
const layout = params.get('layout');
|
||||||
: null;
|
if (!layout) return null;
|
||||||
|
if (layout === 'full') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const _val = parseInt(layout, 10);
|
||||||
|
if (_val >= 1 && _val <= 5) {
|
||||||
|
return _val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(global.window.codeLayout).toBe(NaN);
|
expect(global.window.codeLayout).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle layout parameter out of range', () => {
|
test('should handle layout parameter out of range', () => {
|
||||||
@@ -87,13 +129,22 @@ describe('Layout Parameter Tests', () => {
|
|||||||
|
|
||||||
if (global.window.location.search) {
|
if (global.window.location.search) {
|
||||||
const params = new URLSearchParams(global.window.location.search);
|
const params = new URLSearchParams(global.window.location.search);
|
||||||
global.window.codeLayout = params.get('layout')
|
global.window.codeLayout = (() => {
|
||||||
? parseInt(params.get('layout'), 10)
|
const layout = params.get('layout');
|
||||||
: null;
|
if (!layout) return null;
|
||||||
|
if (layout === 'full') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const _val = parseInt(layout, 10);
|
||||||
|
if (_val >= 1 && _val <= 5) {
|
||||||
|
return _val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The layout should be parsed but may be outside valid range
|
// The layout should be null for out-of-range values
|
||||||
expect(global.window.codeLayout).toBe(layoutValue);
|
expect(global.window.codeLayout).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,9 +171,18 @@ describe('Layout Parameter Tests', () => {
|
|||||||
const params = new URLSearchParams(global.window.location.search);
|
const params = new URLSearchParams(global.window.location.search);
|
||||||
global.window.codeHtml = params.get('html') || '';
|
global.window.codeHtml = params.get('html') || '';
|
||||||
global.window.codeCss = params.get('css') || '';
|
global.window.codeCss = params.get('css') || '';
|
||||||
global.window.codeLayout = params.get('layout')
|
global.window.codeLayout = (() => {
|
||||||
? parseInt(params.get('layout'), 10)
|
const layout = params.get('layout');
|
||||||
: null;
|
if (!layout) return null;
|
||||||
|
if (layout === 'full') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const _val = parseInt(layout, 10);
|
||||||
|
if (_val >= 1 && _val <= 5) {
|
||||||
|
return _val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(global.window.codeLayout).toBe(2);
|
expect(global.window.codeLayout).toBe(2);
|
||||||
@@ -138,11 +198,47 @@ describe('Layout Parameter Tests', () => {
|
|||||||
// Simulate the URL parsing logic
|
// Simulate the URL parsing logic
|
||||||
if (global.window.location.search) {
|
if (global.window.location.search) {
|
||||||
const params = new URLSearchParams(global.window.location.search);
|
const params = new URLSearchParams(global.window.location.search);
|
||||||
global.window.codeLayout = params.get('layout')
|
global.window.codeLayout = (() => {
|
||||||
? parseInt(params.get('layout'), 10)
|
const layout = params.get('layout');
|
||||||
: null;
|
if (!layout) return null;
|
||||||
|
if (layout === 'full') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const _val = parseInt(layout, 10);
|
||||||
|
if (_val >= 1 && _val <= 5) {
|
||||||
|
return _val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(global.window.codeLayout).toBe(null);
|
expect(global.window.codeLayout).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should handle all valid layout values', () => {
|
||||||
|
// Test all valid layout values (1-5)
|
||||||
|
const validLayouts = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
|
validLayouts.forEach(layoutValue => {
|
||||||
|
global.window.location.search = `?layout=${layoutValue}`;
|
||||||
|
|
||||||
|
if (global.window.location.search) {
|
||||||
|
const params = new URLSearchParams(global.window.location.search);
|
||||||
|
global.window.codeLayout = (() => {
|
||||||
|
const layout = params.get('layout');
|
||||||
|
if (!layout) return null;
|
||||||
|
if (layout === 'full') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
const _val = parseInt(layout, 10);
|
||||||
|
if (_val >= 1 && _val <= 5) {
|
||||||
|
return _val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(global.window.codeLayout).toBe(layoutValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user