Skip to content

Commit cba87af

Browse files
committed
feat: implements iframe widget
1 parent 84c852d commit cba87af

File tree

6 files changed

+269
-0
lines changed

6 files changed

+269
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import React, { Component } from 'react';
2+
import styled from 'styled-components';
3+
import { FirebaseDatabaseMutation } from '@react-firebase/database'
4+
import {
5+
TextField,
6+
Button,
7+
Typography,
8+
} from '@material-ui/core';
9+
import type { IFrameWidgetProps } from './types';
10+
11+
type Props = {
12+
id: string;
13+
props: IFrameWidgetProps;
14+
};
15+
16+
const FormGroup = styled.div`
17+
display: flex;
18+
margin-bottom: 1rem;
19+
min-width: 480px;
20+
& > div {
21+
flex-grow: 1;
22+
margin-left: 0.25rem;
23+
}
24+
`
25+
class IFrameWidgetEditor extends Component<Props, IFrameWidgetProps> {
26+
constructor(props) {
27+
super(props);
28+
this.state = this.props.props;
29+
}
30+
31+
render() {
32+
return (
33+
<div>
34+
<Typography variant="h6">
35+
IFrameWidget : {this.props.id}
36+
</Typography>
37+
<FormGroup>
38+
<TextField
39+
type="text"
40+
label="url"
41+
fullWidth
42+
variant="outlined"
43+
onChange={(e) => {
44+
this.setState({ ...this.state, url: e.target.value });
45+
}}
46+
value={this.state.url}
47+
/>
48+
<TextField
49+
type="number"
50+
label="retry time(sec)"
51+
fullWidth
52+
variant="outlined"
53+
onChange={(e) => {
54+
this.setState({ ...this.state, retry_time: parseFloat(e.target.value) });
55+
}}
56+
/>
57+
<TextField
58+
type="number"
59+
label="retry time(sec)"
60+
fullWidth
61+
variant="outlined"
62+
onChange={(e) => {
63+
this.setState({ ...this.state, retry_count: parseInt(e.target.value) });
64+
}}
65+
/>
66+
</FormGroup>
67+
<FormGroup>
68+
<TextField
69+
type="number"
70+
label="width"
71+
fullWidth
72+
variant="outlined"
73+
onChange={(e) => {
74+
this.setState({ ...this.state, width: parseFloat(e.target.value) });
75+
}}
76+
value={this.state.width}
77+
/>
78+
<TextField
79+
type="number"
80+
label="height"
81+
fullWidth
82+
variant="outlined"
83+
onChange={(e) => {
84+
this.setState({ ...this.state, height: parseFloat(e.target.value) });
85+
}}
86+
value={this.state.height}
87+
/>
88+
</FormGroup>
89+
<FormGroup>
90+
<TextField
91+
type="number"
92+
label="position top"
93+
fullWidth
94+
variant="outlined"
95+
onChange={(e) => {
96+
const pos = this.state.position || {};
97+
if (e.target.value !== "") {
98+
const v = parseInt(e.target.value);
99+
if (isNaN(v)) {
100+
delete pos.top;
101+
} else {
102+
pos.top = v;
103+
}
104+
} else {
105+
delete pos.top;
106+
}
107+
this.setState({ ...this.state, position: pos });
108+
}}
109+
value={this.state?.position?.top}
110+
/>
111+
<TextField
112+
type="number"
113+
label="position right"
114+
fullWidth
115+
variant="outlined"
116+
onChange={(e) => {
117+
const pos = this.state.position || {};
118+
if (e.target.value !== "") {
119+
const v = parseInt(e.target.value);
120+
if (isNaN(v)) {
121+
delete pos.right;
122+
} else {
123+
pos.right = v;
124+
}
125+
} else {
126+
delete pos.right;
127+
}
128+
this.setState({ ...this.state, position: pos });
129+
}}
130+
value={this.state.position?.right}
131+
/>
132+
<TextField
133+
type="number"
134+
label="position bottom"
135+
fullWidth
136+
variant="outlined"
137+
onChange={(e) => {
138+
const pos = this.state.position || {};
139+
if (e.target.value !== "") {
140+
const v = parseInt(e.target.value);
141+
if (isNaN(v)) {
142+
delete pos.bottom;
143+
} else {
144+
pos.bottom = v;
145+
}
146+
} else {
147+
delete pos.bottom;
148+
}
149+
this.setState({ ...this.state, position: pos });
150+
}}
151+
value={this.state.position?.bottom}
152+
/>
153+
<TextField
154+
type="number"
155+
label="position left"
156+
fullWidth
157+
variant="outlined"
158+
onChange={(e) => {
159+
const pos = this.state.position || {};
160+
if (e.target.value !== "") {
161+
const v = parseInt(e.target.value);
162+
if (isNaN(v)) {
163+
delete pos.left;
164+
} else {
165+
pos.left = v;
166+
}
167+
} else {
168+
delete pos.left;
169+
}
170+
this.setState({ ...this.state, position: pos });
171+
}}
172+
value={this.state.position?.left}
173+
/>
174+
</FormGroup>
175+
176+
<FirebaseDatabaseMutation
177+
type="set"
178+
path={`/widgets/${this.props.id}/props`}
179+
>
180+
{({ runMutation }) => {
181+
return (
182+
<FormGroup>
183+
<Button
184+
type="button"
185+
color="primary"
186+
variant="contained"
187+
onClick={async (e: any) => {
188+
e.preventDefault();
189+
await runMutation(this.state);
190+
}}
191+
>
192+
Save
193+
</Button>
194+
</FormGroup>
195+
);
196+
}}
197+
</FirebaseDatabaseMutation>
198+
</div>
199+
);
200+
}
201+
}
202+
203+
export { IFrameWidgetEditor };

src/components/IFrameWidget/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { IFrameWidget } from './widget';
2+
export { IFrameWidgetEditor } from './editor';

src/components/IFrameWidget/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type Position = {
2+
top?: number; // px
3+
right?: number; // px
4+
bottom?: number; // px
5+
left?: number; // px
6+
};
7+
8+
type IFrameWidgetProps = {
9+
url: string;
10+
retry_time: number;
11+
retry_count: number;
12+
width: number;
13+
height: number;
14+
position?: Position;
15+
}
16+
17+
export type { Position, IFrameWidgetProps };
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { VFC, useState, CSSProperties } from 'react';
2+
import styled from 'styled-components';
3+
import type { IFrameWidgetProps } from './types';
4+
5+
const IFrameWidget: VFC<IFrameWidgetProps> = ({ url, retry_time, retry_count, width, height, position }) => {
6+
const [count, setCount] = useState(0);
7+
8+
const handleLoaded = () => {
9+
console.log(`iframe: ${url} loaded`);
10+
};
11+
12+
const handleError = (e) => {
13+
console.warn(`iframe: ${url}`, e);
14+
if (count < retry_count) {
15+
setTimeout(() => {
16+
setCount(count + 1);
17+
}, retry_time * 1000);
18+
}
19+
};
20+
21+
const style: CSSProperties = {
22+
position: 'absolute',
23+
};
24+
25+
if (position?.top !== undefined) style.top = position.top;
26+
if (position?.right !== undefined) style.right = position.right;
27+
if (position?.bottom !== undefined) style.bottom = position.bottom;
28+
if (position?.left !== undefined) style.left = position.left;
29+
30+
return (
31+
<iframe
32+
key={count}
33+
src={url}
34+
width={`${width}px`}
35+
height={`${height}px`}
36+
onLoad={handleLoaded}
37+
onError={handleError}
38+
style={style}
39+
/>
40+
);
41+
};
42+
43+
export { IFrameWidget };

src/components/Preview.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { FirebaseDatabaseNode } from '@react-firebase/database';
33

44
import { TextWidget } from '@/components/TextWidget';
55
import { TimeWidget } from '@/components/TimeWidget';
6+
import { IFrameWidget } from '@/components/IFrameWidget';
67

78
const Widgets = {
89
'text': TextWidget,
910
'time': TimeWidget,
11+
'iframe': IFrameWidget,
1012
};
1113

1214
const Preview: VFC = () => {

src/components/admin/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import { auth } from '@/lib/firebase';
1616
import { Signin } from '@/components/admin/signin';
1717
import { TextWidgetEditor } from '@/components/TextWidget';
1818
import { TimeWidgetEditor } from '@/components/TimeWidget';
19+
import { IFrameWidgetEditor } from '@/components/IFrameWidget';
1920

2021
const Editors = {
2122
text: TextWidgetEditor,
2223
time: TimeWidgetEditor,
24+
iframe: IFrameWidgetEditor,
2325
};
2426

2527
const useStyles = makeStyles((theme) => ({

0 commit comments

Comments
 (0)