aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Card.js225
-rw-r--r--src/components/Settings.js77
-rw-r--r--src/components/WifiCard.js148
-rw-r--r--src/components/style.css46
4 files changed, 246 insertions, 250 deletions
diff --git a/src/components/Card.js b/src/components/Card.js
deleted file mode 100644
index 601b760..0000000
--- a/src/components/Card.js
+++ /dev/null
@@ -1,225 +0,0 @@
1import QRCode from 'qrcode.react';
2import { useEffect, useRef, useState } from 'react';
3import { useTranslation } from 'react-i18next';
4import './style.css';
5
6export const Card = ({ direction = 'ltr' }) => {
7 const firstLoad = useRef(true);
8 const [qrvalue, setQrvalue] = useState('');
9 const [network, setNetwork] = useState({
10 ssid: '',
11 encryptionMode: 'WPA',
12 password: '',
13 hidePassword: false,
14 });
15 const [portrait, setPortrait] = useState(false);
16 const { t } = useTranslation();
17 const escape = (v) => {
18 const needsEscape = ['"', ';', ',', ':', '\\'];
19
20 let escaped = '';
21 for (const c of v) {
22 if (needsEscape.includes(c)) {
23 escaped += `\\${c}`;
24 } else {
25 escaped += c;
26 }
27 }
28 return escaped;
29 };
30
31 const onPrint = () => {
32 if (network.ssid.length > 0) {
33 if (network.password.length < 8 && network.encryptionMode === 'WPA') {
34 alert(t('wifi.alert.password.length.8'));
35 } else if (
36 network.password.length < 5 &&
37 network.encryptionMode === 'WEP'
38 ) {
39 alert(t('wifi.alert.password.length.5'));
40 } else {
41 document.title = 'WiFi Card - ' + network.ssid;
42 window.print();
43 }
44 } else {
45 alert(t('wifi.alert.name'));
46 }
47 };
48
49 const disableHidePassword = () => {
50 const isWEPWithPasswordLengthShorterThat5Characters = () => {
51 return network.encryptionMode === 'WEP' && network.password.length < 5
52 ? true
53 : false;
54 };
55
56 return network.encryptionMode === 'WPA' && network.password.length < 8
57 ? true
58 : isWEPWithPasswordLengthShorterThat5Characters();
59 };
60
61 useEffect(() => {
62 if (firstLoad.current && window.innerWidth < 500) {
63 firstLoad.current = false;
64 setPortrait(true);
65 }
66
67 const ssid = escape(network.ssid);
68 const password =
69 network.encryptionMode === 'nopass' ? '' : escape(network.password);
70 setQrvalue(`WIFI:T:${network.encryptionMode};S:${ssid};P:${password};;`);
71 }, [network]);
72
73 const setEncryptionMode = (e) =>
74 setNetwork({
75 ...network,
76 encryptionMode: e.target.value,
77 });
78
79 const checkDirectionAndSetPadding =
80 direction === 'ltr' ? { paddingRight: '1em' } : { paddingLeft: '1em' };
81
82 return (
83 <div>
84 <fieldset
85 id="print-area"
86 style={{ maxWidth: portrait ? '350px' : '100%' }}
87 >
88 <h1 style={{ textAlign: portrait ? 'center' : 'unset' }}>
89 {t('wifi.login')}
90 </h1>
91
92 <div
93 className="details"
94 style={{ flexDirection: portrait ? 'column' : 'row' }}
95 >
96 <QRCode
97 className="qrcode"
98 style={!portrait && checkDirectionAndSetPadding}
99 value={qrvalue}
100 size={175}
101 />
102
103 <div className="inputs">
104 <label>{t('wifi.name')}</label>
105 <textarea
106 id="ssid"
107 type="text"
108 maxLength="32"
109 placeholder={t('wifi.name.placeholder')}
110 autoComplete="off"
111 autoCorrect="off"
112 autoCapitalize="none"
113 spellCheck="false"
114 value={network.ssid}
115 onChange={(e) => setNetwork({ ...network, ssid: e.target.value })}
116 />
117 <label
118 className={`
119 ${network.hidePassword && 'no-print hidden'}
120 ${network.encryptionMode === 'nopass' && 'hidden'}
121 `}
122 >
123 {t('wifi.password')}
124 </label>
125 <textarea
126 id="password"
127 type="text"
128 className={`
129 ${network.hidePassword && 'no-print hidden'}
130 ${network.encryptionMode === 'nopass' && 'hidden'}
131 `}
132 style={{
133 height:
134 portrait && network.password.length > 40 ? '5em' : 'auto',
135 }}
136 maxLength="63"
137 placeholder={t('wifi.password.placeholder')}
138 autoComplete="off"
139 autoCorrect="off"
140 autoCapitalize="none"
141 spellCheck="false"
142 value={network.password}
143 onChange={(e) => {
144 setNetwork({ ...network, password: e.target.value });
145 }}
146 />
147
148 <div className="no-print">
149 <input
150 type="checkbox"
151 id="hide-password-checkbox"
152 checked={network.hidePassword}
153 disabled={disableHidePassword()}
154 className={network.encryptionMode === 'nopass' ? 'hidden' : ''}
155 onChange={() =>
156 setNetwork({
157 ...network,
158 hidePassword: !network.hidePassword,
159 })
160 }
161 />
162 <label
163 htmlFor="hide-password-checkbox"
164 className={network.encryptionMode === 'nopass' ? 'hidden' : ''}
165 >
166 {t('wifi.password.hide')}
167 </label>
168 </div>
169
170 <div className="no-print">
171 <label>
172 {t('wifi.password.encryption')}:{direction === 'rtl' ? ' ' : ''}
173 </label>
174 <span dir="ltr">
175 <input
176 type="radio"
177 name="encrypt-select"
178 id="encrypt-none"
179 value="nopass"
180 onChange={setEncryptionMode}
181 />
182 <label htmlFor="encrypt-none">
183 {t('wifi.password.encryption.none')}
184 </label>
185 <input
186 type="radio"
187 name="encrypt-select"
188 id="encrypt-wpa-wpa2-wpa3"
189 value="WPA"
190 onChange={setEncryptionMode}
191 defaultChecked
192 />
193 <label htmlFor="encrypt-wpa-wpa2-wpa3">WPA/WPA2/WPA3</label>
194 <input
195 type="radio"
196 name="encrypt-select"
197 id="encrypt-wep"
198 value="WEP"
199 onChange={setEncryptionMode}
200 />
201 <label htmlFor="encrypt-wep">WEP</label>
202 </span>
203 </div>
204 </div>
205 </div>
206 <hr />
207 <p>
208 <span role="img" aria-label="mobile-phone">
209 📸📱
210 </span>
211 {t('wifi.tip')}
212 </p>
213 </fieldset>
214
215 <div className="buttons">
216 <button id="rotate" onClick={() => setPortrait(!portrait)}>
217 {t('button.rotate')}
218 </button>
219 <button id="print" onClick={onPrint}>
220 {t('button.print')}
221 </button>
222 </div>
223 </div>
224 );
225};
diff --git a/src/components/Settings.js b/src/components/Settings.js
new file mode 100644
index 0000000..1c28dab
--- /dev/null
+++ b/src/components/Settings.js
@@ -0,0 +1,77 @@
1import { Checkbox, Pane, RadioGroup, SelectField } from 'evergreen-ui';
2import { useEffect, useState } from 'react';
3import { useTranslation } from 'react-i18next';
4import i18n from '../i18n';
5import './style.css';
6
7export const Settings = (props) => {
8 const { t } = useTranslation();
9 const [encryptionModes] = useState([
10 { label: 'None', value: '' },
11 { label: 'WPA/WPA2/WPA3', value: 'WPA' },
12 { label: 'WEP', value: 'WEP' },
13 ]);
14
15 useEffect(() => {
16 if (props.firstLoad.current && window.innerWidth < 500) {
17 props.onFirstLoad();
18 props.onOrientationChange(true);
19 }
20 });
21
22 return (
23 <Pane id="settings" maxWidth={props.settings.portrait ? '350px' : '100%'}>
24 <SelectField
25 width={300}
26 inputHeight={38}
27 label={t('select')}
28 selected={i18n.language}
29 onChange={(e) => props.onLanguageChange(e.target.value)}
30 >
31 <option value="en-US">English</option>
32 <option value="ar">Arabic - العربية</option>
33 <option value="ca">Catalan - Català</option>
34 <option value="zh-HK">Chinese Hong Kong - 简体中文</option>
35 <option value="zh-CN">Chinese Simplified - 简体中文</option>
36 <option value="nl-NL">Dutch - Nederlands</option>
37 <option value="fr-FR">French - Français</option>
38 <option value="de-DE">German - Deutsch</option>
39 <option value="hi-IN">Hindi - हिन्दी</option>
40 <option value="id-ID">Indonesian</option>
41 <option value="it-IT">Italian</option>
42 <option value="ja">Japanese - 日本語</option>
43 <option value="ko">Korean - 한국어</option>
44 <option value="no-NB">Norwegian - Norsk</option>
45 <option value="oc">Occitan</option>
46 <option value="fa-IR">Persian Iran - فارسی</option>
47 <option value="pl-PL">Polish - Polski</option>
48 <option value="pt">Portuguese - Português</option>
49 <option value="pt-BR">Portuguese - Português brasileiro</option>
50 <option value="ru-RU">Russian - Русский</option>
51 <option value="es">Spanish - Español</option>
52 <option value="tr-TR">Turkish - Türkçe</option>
53 <option value="uk-UA">Ukrainian - Українська</option>
54 </SelectField>
55
56 <Checkbox
57 label={t('button.rotate')}
58 checked={props.settings.portrait}
59 onChange={() => props.onOrientationChange(!props.settings.portrait)}
60 />
61 <Checkbox
62 label={t('wifi.password.hide')}
63 checked={props.settings.hidePassword}
64 onChange={() =>
65 props.onHidePasswordChange(!props.settings.hidePassword)
66 }
67 />
68 <RadioGroup
69 label={t('wifi.password.encryption')}
70 size={16}
71 value={props.settings.encryptionMode}
72 options={encryptionModes}
73 onChange={(e) => props.onEncryptionModeChange(e.target.value)}
74 />
75 </Pane>
76 );
77};
diff --git a/src/components/WifiCard.js b/src/components/WifiCard.js
new file mode 100644
index 0000000..645dfa1
--- /dev/null
+++ b/src/components/WifiCard.js
@@ -0,0 +1,148 @@
1import {
2 CameraIcon,
3 Card,
4 Heading,
5 MobilePhoneIcon,
6 Pane,
7 Paragraph,
8 Text,
9 TextareaField,
10} from 'evergreen-ui';
11import QRCode from 'qrcode.react';
12import { useEffect, useState } from 'react';
13import { useTranslation } from 'react-i18next';
14import logo from '../../src/images/wifi.png';
15import './style.css';
16
17export const WifiCard = (props) => {
18 const { t } = useTranslation();
19 const [qrvalue, setQrvalue] = useState('');
20
21 const escape = (v) => {
22 const needsEscape = ['"', ';', ',', ':', '\\'];
23
24 let escaped = '';
25 for (const c of v) {
26 if (needsEscape.includes(c)) {
27 escaped += `\\${c}`;
28 } else {
29 escaped += c;
30 }
31 }
32 return escaped;
33 };
34
35 useEffect(() => {
36 const ssid = escape(props.settings.ssid);
37 const password = !props.settings.encryptionMode
38 ? ''
39 : escape(props.settings.password);
40 setQrvalue(
41 `WIFI:T:${props.settings.encryptionMode};S:${ssid};P:${password};;`
42 );
43 }, [props.settings]);
44
45 const portraitWidth = () => {
46 const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
47 return isMobile ? '100%' : '280px';
48 };
49
50 const passwordFieldLabel = () => {
51 const hiddenPassword =
52 props.settings.hidePassword || !props.settings.encryptionMode;
53 return hiddenPassword ? '' : t('wifi.password');
54 };
55
56 return (
57 <Pane>
58 <Card
59 id="print-area"
60 elevation={3}
61 style={{ maxWidth: props.settings.portrait ? portraitWidth() : '100%' }}
62 >
63 <Pane display="flex" paddingBottom={12}>
64 <img alt="icon" src={logo} width="24" height="24" />
65 <Heading
66 paddingLeft={10}
67 size={700}
68 textAlign={props.settings.portrait ? 'center' : 'unset'}
69 >
70 {t('wifi.login')}
71 </Heading>
72 </Pane>
73
74 <Pane
75 className="details"
76 style={{ flexDirection: props.settings.portrait ? 'column' : 'row' }}
77 >
78 <QRCode
79 className="qrcode"
80 style={
81 !props.settings.portrait
82 ? props.direction === 'ltr'
83 ? { paddingRight: '1em' }
84 : { paddingLeft: '1em' }
85 : {}
86 }
87 value={qrvalue}
88 size={150}
89 />
90
91 <Pane width={'100%'}>
92 <TextareaField
93 id="ssid"
94 type="text"
95 marginBottom={5}
96 autoComplete="off"
97 autoCorrect="off"
98 autoCapitalize="none"
99 spellCheck={false}
100 maxLength="32"
101 label={t('wifi.name')}
102 placeholder={t('wifi.name.placeholder')}
103 value={props.settings.ssid}
104 onChange={(e) => props.onSSIDChange(e.target.value)}
105 isInvalid={!!props.ssidError}
106 validationMessage={!!props.ssidError && props.ssidError}
107 />
108 <TextareaField
109 id="password"
110 type="text"
111 maxLength="63"
112 autoComplete="off"
113 autoCorrect="off"
114 autoCapitalize="none"
115 spellCheck={false}
116 className={`
117 ${
118 (props.settings.hidePassword ||
119 !props.settings.encryptionMode) &&
120 'hidden'
121 }
122 `}
123 height={
124 props.settings.portrait && props.settings.password.length > 40
125 ? '5em'
126 : 'auto'
127 }
128 label={passwordFieldLabel()}
129 placeholder={t('wifi.password.placeholder')}
130 value={props.settings.password}
131 onChange={(e) => props.onPasswordChange(e.target.value)}
132 isInvalid={!!props.passwordError}
133 validationMessage={!!props.passwordError && props.passwordError}
134 />
135 </Pane>
136 </Pane>
137 <hr />
138 <Paragraph>
139 <CameraIcon />
140 <MobilePhoneIcon />
141 <Text size={300} paddingLeft={8}>
142 {t('wifi.tip')}
143 </Text>
144 </Paragraph>
145 </Card>
146 </Pane>
147 );
148};
diff --git a/src/components/style.css b/src/components/style.css
index c9dbb54..fa931bb 100644
--- a/src/components/style.css
+++ b/src/components/style.css
@@ -2,8 +2,7 @@
2 2
3#print-area { 3#print-area {
4 border-color: #aaa; 4 border-color: #aaa;
5 border-style: dashed; 5 margin-bottom: 1em;
6 margin-bottom: 2em;
7 margin-top: 2em; 6 margin-top: 2em;
8 padding: 1em; 7 padding: 1em;
9} 8}
@@ -16,39 +15,39 @@
16.qrcode { 15.qrcode {
17 margin-bottom: 1em; 16 margin-bottom: 1em;
18 max-width: 175px; 17 max-width: 175px;
19 min-width: 175px;
20} 18}
21 19
22.hidden { 20.hidden {
23 display: none; 21 display: none;
24} 22}
25
26textarea { 23textarea {
27 background-color: #fff; 24 background-color: #fff;
28 border: solid 1px #ddd; 25 border: solid 1px #ddd;
29 font-family: 'Source Code Pro', serif; 26 font-family: 'Source Code Pro', serif !important;
30 font-size: 1.3em; 27 font-size: 1em !important;
31 font-weight: bold; 28 font-weight: bold !important;
32 height: 3em; 29 margin-bottom: 0;
30 height: 40px !important;
31 min-height: 0px !important;
33 overflow: hidden; 32 overflow: hidden;
34 resize: none; 33 resize: none;
35} 34}
35textarea#password {
36 height: 60px !important;
37}
38
39hr {
40 margin-top: 0;
41}
36 42
37button { 43button {
38 height: 50px; 44 height: 50px;
39 width: 180px; 45 width: 180px;
40} 46}
41 47
42button#print { 48#settings {
43 color: #fff; 49 margin-bottom: 1em;
44 background-color: #0074d9; 50 padding: 1em;
45 border-color: #0074d9;
46}
47
48button#rotate {
49 color: #fff;
50 background-color: #6c757d;
51 border-color: #6c757d;
52} 51}
53 52
54@media print { 53@media print {
@@ -59,19 +58,16 @@ button#rotate {
59 #print-area * { 58 #print-area * {
60 visibility: visible; 59 visibility: visible;
61 } 60 }
61 #print-area {
62 border-style: dashed;
63 box-shadow: none;
64 }
62 #print-area textarea { 65 #print-area textarea {
63 border: none; 66 border: none;
64 } 67 }
65 .no-print {
66 display: none;
67 }
68 #print-area { 68 #print-area {
69 position: absolute; 69 position: absolute;
70 left: 0; 70 left: 0;
71 top: 0; 71 top: 0;
72 } 72 }
73} 73}
74
75hr {
76 margin-top: 25px;
77}