Compare commits
12 Commits
4ebcc7de49
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ddcdca156c | |||
| e749386bd5 | |||
| 932931977e | |||
| 4feec19d2c | |||
| 6c0f9074f8 | |||
| 62a7aab9ff | |||
| 24e0d7980f | |||
| e8cd2a8f14 | |||
| f3382717f3 | |||
| 5a277ccc6b | |||
| 55dca5f75f | |||
| 015f1415a1 |
35
.dockerignore
Normal file
35
.dockerignore
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.gitea
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.log
|
||||||
|
.cache
|
||||||
|
|
||||||
@@ -2,48 +2,44 @@ name: Deploy to Private Server
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ["main"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:20-slim
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build Vite app
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: build-output
|
|
||||||
path: dist/
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: homesrv
|
||||||
needs: build
|
|
||||||
container:
|
|
||||||
image: node:20-slim
|
|
||||||
options: -v /var/www/vite-app:/deploy/vite-app --user 1000:1000
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Build Docker image
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: build-output
|
|
||||||
path: dist/
|
|
||||||
|
|
||||||
- name: Deploy to host
|
|
||||||
run: |
|
run: |
|
||||||
rm -rf /deploy/vite-app/timelapse-calc
|
docker build -t timelapse-calc:latest \
|
||||||
mkdir -p /deploy/vite-app/timelapse-calc
|
-f Dockerfile .
|
||||||
cp -r dist/* /deploy/vite-app/timelapse-calc/
|
|
||||||
echo "Deployment completed successfully"
|
- name: Stop old container if exists
|
||||||
echo "Deployed files:"
|
run: |
|
||||||
ls -la /deploy/vite-app/timelapse-calc/
|
if docker ps -a --format '{{.Names}}' | grep -q '^timelapse-calc$'; then
|
||||||
|
docker stop timelapse-calc
|
||||||
|
docker rm timelapse-calc
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run new container
|
||||||
|
run: |
|
||||||
|
docker run -d \
|
||||||
|
--restart always \
|
||||||
|
--name timelapse-calc \
|
||||||
|
-p 3005:80 \
|
||||||
|
timelapse-calc:latest
|
||||||
|
|
||||||
|
- name: Send Deployment Notification
|
||||||
|
uses: https://git.hcmc.online/actions/deploy-notify@main
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
||||||
|
title: "Deploy: ${{ github.repository }}"
|
||||||
|
message: |
|
||||||
|
Ref: ${{ github.ref_name }}
|
||||||
|
Commit: ${{ github.sha }}
|
||||||
|
Message: ${{ github.event.head_commit.message }}
|
||||||
|
Status: ${{ job.status }}
|
||||||
|
Actor: ${{ github.event.head_commit.author.name }}
|
||||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built assets from build stage
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
RUN echo 'server { \
|
||||||
|
listen 80; \
|
||||||
|
server_name _; \
|
||||||
|
root /usr/share/nginx/html; \
|
||||||
|
index index.html; \
|
||||||
|
location / { \
|
||||||
|
try_files $uri $uri/ /index.html; \
|
||||||
|
} \
|
||||||
|
# Cache static assets \
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { \
|
||||||
|
expires 1y; \
|
||||||
|
add_header Cache-Control "public, immutable"; \
|
||||||
|
} \
|
||||||
|
}' > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
156
src/App.jsx
156
src/App.jsx
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Card, ConfigProvider } from 'antd';
|
import { Card, ConfigProvider, Segmented } from 'antd';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import CalculatorMode from './components/CalculatorMode';
|
import CalculatorMode from './components/CalculatorMode';
|
||||||
import TimeInput from './components/TimeInput';
|
import TimeInput from './components/TimeInput';
|
||||||
@@ -13,43 +13,56 @@ function App() {
|
|||||||
|
|
||||||
// State for inputs
|
// State for inputs
|
||||||
const [clipLength, setClipLength] = useState({ hours: 0, minutes: 0, seconds: 20 });
|
const [clipLength, setClipLength] = useState({ hours: 0, minutes: 0, seconds: 20 });
|
||||||
const [eventDuration, setEventDuration] = useState({ hours: 1, minutes: 0, seconds: 0 });
|
const [eventDuration, setEventDuration] = useState({ hours: 0, minutes: 40, seconds: 0 });
|
||||||
|
const [intervalInput, setIntervalInput] = useState(2); // Seconds
|
||||||
const [fps, setFps] = useState(30);
|
const [fps, setFps] = useState(30);
|
||||||
const [imageSize, setImageSize] = useState(12);
|
|
||||||
|
|
||||||
// State for results
|
|
||||||
const [shootingInterval, setShootingInterval] = useState(0);
|
|
||||||
const [numberOfPhotos, setNumberOfPhotos] = useState(0);
|
|
||||||
const [totalMemory, setTotalMemory] = useState(0);
|
|
||||||
|
|
||||||
// Helper function to convert time object to seconds
|
// Helper function to convert time object to seconds
|
||||||
const timeToSeconds = (time) => {
|
const timeToSeconds = (time) => {
|
||||||
return time.hours * 3600 + time.minutes * 60 + time.seconds;
|
return time.hours * 3600 + time.minutes * 60 + time.seconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate results whenever inputs change
|
// Helper to convert seconds to time object (for derived values if needed, but we output seconds)
|
||||||
useEffect(() => {
|
// ...
|
||||||
const clipSeconds = timeToSeconds(clipLength);
|
|
||||||
const eventSeconds = timeToSeconds(eventDuration);
|
|
||||||
|
|
||||||
if (clipSeconds > 0 && eventSeconds > 0 && fps > 0) {
|
// Calculate results
|
||||||
// Calculate shooting interval (in seconds)
|
const calculateResults = () => {
|
||||||
const interval = eventSeconds / (clipSeconds * fps);
|
const clipSec = timeToSeconds(clipLength);
|
||||||
setShootingInterval(interval);
|
const eventSec = timeToSeconds(eventDuration);
|
||||||
|
|
||||||
// Calculate number of photos
|
let photos = 0;
|
||||||
const photos = Math.ceil(eventSeconds / interval);
|
let result = 0;
|
||||||
setNumberOfPhotos(photos);
|
|
||||||
|
|
||||||
// Calculate total memory (in MB)
|
if (mode === 'shooting-interval') {
|
||||||
const memory = photos * imageSize;
|
if (clipSec > 0 && fps > 0) {
|
||||||
setTotalMemory(memory);
|
result = eventSec / (clipSec * fps);
|
||||||
} else {
|
if (result > 0) photos = Math.ceil(eventSec / result);
|
||||||
setShootingInterval(0);
|
}
|
||||||
setNumberOfPhotos(0);
|
} else if (mode === 'clip-length') {
|
||||||
setTotalMemory(0);
|
if (intervalInput > 0 && fps > 0) {
|
||||||
|
result = eventSec / (intervalInput * fps);
|
||||||
|
photos = Math.ceil(eventSec / intervalInput);
|
||||||
|
}
|
||||||
|
} else if (mode === 'event-duration') {
|
||||||
|
if (clipSec > 0 && intervalInput > 0 && fps > 0) {
|
||||||
|
result = clipSec * fps * intervalInput;
|
||||||
|
photos = Math.ceil(result / intervalInput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [clipLength, eventDuration, fps, imageSize]);
|
|
||||||
|
return { resultSeconds: result, numberOfPhotos: photos };
|
||||||
|
};
|
||||||
|
|
||||||
|
const { resultSeconds, numberOfPhotos } = calculateResults();
|
||||||
|
|
||||||
|
const getResultLabel = () => {
|
||||||
|
switch (mode) {
|
||||||
|
case 'shooting-interval': return "Shooting Interval";
|
||||||
|
case 'clip-length': return "Clip Length";
|
||||||
|
case 'event-duration': return "Event Duration";
|
||||||
|
default: return "Result";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
@@ -68,6 +81,11 @@ function App() {
|
|||||||
InputNumber: {
|
InputNumber: {
|
||||||
controlHeight: 44,
|
controlHeight: 44,
|
||||||
},
|
},
|
||||||
|
Segmented: {
|
||||||
|
itemSelectedBg: '#F4D03F',
|
||||||
|
itemSelectedColor: '#2C3E50',
|
||||||
|
trackBg: '#F8F9FA'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -77,44 +95,62 @@ function App() {
|
|||||||
<Card className="calculator-card">
|
<Card className="calculator-card">
|
||||||
<CalculatorMode value={mode} onChange={setMode} />
|
<CalculatorMode value={mode} onChange={setMode} />
|
||||||
|
|
||||||
<TimeInput
|
{mode !== 'clip-length' && (
|
||||||
label="Clip length"
|
<TimeInput
|
||||||
hours={clipLength.hours}
|
label="Clip length"
|
||||||
minutes={clipLength.minutes}
|
hours={clipLength.hours}
|
||||||
seconds={clipLength.seconds}
|
minutes={clipLength.minutes}
|
||||||
onChange={setClipLength}
|
seconds={clipLength.seconds}
|
||||||
/>
|
onChange={setClipLength}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<TimeInput
|
{mode !== 'event-duration' && (
|
||||||
label="Event duration"
|
<TimeInput
|
||||||
hours={eventDuration.hours}
|
label="Event duration"
|
||||||
minutes={eventDuration.minutes}
|
hours={eventDuration.hours}
|
||||||
seconds={eventDuration.seconds}
|
minutes={eventDuration.minutes}
|
||||||
onChange={setEventDuration}
|
seconds={eventDuration.seconds}
|
||||||
/>
|
onChange={setEventDuration}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<NumberInput
|
{mode !== 'shooting-interval' && (
|
||||||
label="Frames per second"
|
<NumberInput
|
||||||
value={fps}
|
label="Shooting Interval (sec)"
|
||||||
onChange={setFps}
|
value={intervalInput}
|
||||||
unit="fps"
|
onChange={setIntervalInput}
|
||||||
min={1}
|
unit="s"
|
||||||
step={1}
|
min={0.1}
|
||||||
/>
|
step={0.1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<NumberInput
|
<div className="form-row">
|
||||||
label="Image size"
|
<label className="form-label">Frames per second</label>
|
||||||
value={imageSize}
|
<Segmented
|
||||||
onChange={setImageSize}
|
options={[
|
||||||
unit="MB"
|
{ label: '24', value: 24 },
|
||||||
min={0.1}
|
{ label: '30', value: 30 },
|
||||||
step={0.1}
|
{ label: '60', value: 60 },
|
||||||
/>
|
]}
|
||||||
|
value={fps}
|
||||||
|
onChange={setFps}
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#F8F9FA',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid #DEE2E6'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Results
|
<Results
|
||||||
shootingInterval={shootingInterval}
|
label={getResultLabel()}
|
||||||
|
resultSeconds={resultSeconds}
|
||||||
numberOfPhotos={numberOfPhotos}
|
numberOfPhotos={numberOfPhotos}
|
||||||
totalMemory={totalMemory}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import { Select } from 'antd';
|
import { Segmented } from 'antd';
|
||||||
|
|
||||||
export default function CalculatorMode({ value, onChange }) {
|
export default function CalculatorMode({ value, onChange }) {
|
||||||
return (
|
return (
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="calculator-mode" className="form-label">
|
<label className="form-label" style={{ display: 'block', marginBottom: 8 }}>
|
||||||
Calculate
|
Calculate
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Segmented
|
||||||
id="calculator-mode"
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
block
|
||||||
size="large"
|
size="large"
|
||||||
style={{ width: '100%' }}
|
|
||||||
options={[
|
options={[
|
||||||
{ value: 'shooting-interval', label: 'Shooting interval' },
|
{ value: 'shooting-interval', label: 'Interval' },
|
||||||
{ value: 'clip-length', label: 'Clip length' },
|
{ value: 'clip-length', label: 'Clip' },
|
||||||
{ value: 'event-duration', label: 'Event duration' },
|
{ value: 'event-duration', label: 'Event' },
|
||||||
]}
|
]}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#F8F9FA',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid #DEE2E6'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InputNumber } from 'antd';
|
import { InputNumber, Button, Flex } from 'antd';
|
||||||
|
|
||||||
export default function NumberInput({ label, value, onChange, unit, min = 0, step = 1 }) {
|
export default function NumberInput({ label, value, onChange, unit, min = 0, step = 1 }) {
|
||||||
const handleChange = (newValue) => {
|
const handleChange = (newValue) => {
|
||||||
@@ -6,20 +6,45 @@ export default function NumberInput({ label, value, onChange, unit, min = 0, ste
|
|||||||
onChange(numValue);
|
onChange(numValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDecrement = () => {
|
||||||
|
handleChange(value - step);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIncrement = () => {
|
||||||
|
handleChange(value + step);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">{label}</label>
|
<label className="form-label">{label}</label>
|
||||||
<InputNumber
|
<Flex gap="small">
|
||||||
value={value}
|
<Button
|
||||||
onChange={handleChange}
|
onClick={handleDecrement}
|
||||||
min={min}
|
size="large"
|
||||||
step={step}
|
disabled={value <= min}
|
||||||
placeholder="0"
|
style={{ width: 44, padding: 0 }}
|
||||||
addonAfter={unit}
|
>
|
||||||
size="large"
|
-
|
||||||
style={{ width: '100%' }}
|
</Button>
|
||||||
controls={false}
|
<InputNumber
|
||||||
/>
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
min={min}
|
||||||
|
step={step}
|
||||||
|
placeholder="0"
|
||||||
|
suffix={<span style={{ color: '#999' }}>{unit}</span>}
|
||||||
|
size="large"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
controls={false}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleIncrement}
|
||||||
|
size="large"
|
||||||
|
style={{ width: 44, padding: 0 }}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Statistic, Row, Col } from 'antd';
|
import { Statistic, Row, Col } from 'antd';
|
||||||
|
|
||||||
export default function Results({ shootingInterval, numberOfPhotos, totalMemory }) {
|
export default function Results({ label, resultSeconds, numberOfPhotos }) {
|
||||||
const formatTime = (seconds) => {
|
const formatTime = (seconds) => {
|
||||||
|
if (!seconds) return '0s';
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
return `${seconds.toFixed(0)}s`;
|
// For interval, we might want decimals. For others, maybe not strictly required but harmless.
|
||||||
|
return `${parseFloat(seconds.toFixed(1))}s`;
|
||||||
} else if (seconds < 3600) {
|
} else if (seconds < 3600) {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
const secs = Math.floor(seconds % 60);
|
const secs = Math.floor(seconds % 60);
|
||||||
@@ -16,38 +18,23 @@ export default function Results({ shootingInterval, numberOfPhotos, totalMemory
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatMemory = (mb) => {
|
|
||||||
if (mb < 1024) {
|
|
||||||
return `${mb.toFixed(2)}MB`;
|
|
||||||
} else {
|
|
||||||
return `${(mb / 1024).toFixed(2)}GB`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="results-section">
|
<div className="results-section">
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={24} sm={8}>
|
<Col xs={24} sm={12}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Shooting interval"
|
title={label}
|
||||||
value={formatTime(shootingInterval)}
|
value={formatTime(resultSeconds)}
|
||||||
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={8}>
|
<Col xs={24} sm={12}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Number of photos"
|
title="Number of photos"
|
||||||
value={numberOfPhotos}
|
value={numberOfPhotos}
|
||||||
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={8}>
|
|
||||||
<Statistic
|
|
||||||
title="Total memory usage"
|
|
||||||
value={formatMemory(totalMemory)}
|
|
||||||
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function TimeInput({ label, hours, minutes, seconds, onChange })
|
|||||||
onChange={(value) => handleChange('hours', value)}
|
onChange={(value) => handleChange('hours', value)}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
addonAfter="h"
|
suffix={<span style={{ color: '#999' }}>h</span>}
|
||||||
size="large"
|
size="large"
|
||||||
style={{ width: '33.33%' }}
|
style={{ width: '33.33%' }}
|
||||||
controls={false}
|
controls={false}
|
||||||
@@ -31,7 +31,7 @@ export default function TimeInput({ label, hours, minutes, seconds, onChange })
|
|||||||
min={0}
|
min={0}
|
||||||
max={59}
|
max={59}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
addonAfter="m"
|
suffix={<span style={{ color: '#999' }}>m</span>}
|
||||||
size="large"
|
size="large"
|
||||||
style={{ width: '33.33%' }}
|
style={{ width: '33.33%' }}
|
||||||
controls={false}
|
controls={false}
|
||||||
@@ -42,7 +42,7 @@ export default function TimeInput({ label, hours, minutes, seconds, onChange })
|
|||||||
min={0}
|
min={0}
|
||||||
max={59}
|
max={59}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
addonAfter="s"
|
suffix={<span style={{ color: '#999' }}>s</span>}
|
||||||
size="large"
|
size="large"
|
||||||
style={{ width: '33.34%' }}
|
style={{ width: '33.34%' }}
|
||||||
controls={false}
|
controls={false}
|
||||||
|
|||||||
Reference in New Issue
Block a user