init
commit
174e809054
|
@ -0,0 +1 @@
|
||||||
|
htdocs/vendor/*
|
|
@ -0,0 +1,2 @@
|
||||||
|
htdocs/cache/*
|
||||||
|
htdocs/vendor/*
|
|
@ -0,0 +1,3 @@
|
||||||
|
htdocs/cache/*
|
||||||
|
htdocs/vendor/*
|
||||||
|
idea/*
|
|
@ -0,0 +1,8 @@
|
||||||
|
FROM php:8-apache
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y curl git unzip && rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN rm /var/www/html/* -rf
|
||||||
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
COPY htdocs/ /var/www/html
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
RUN composer install
|
||||||
|
RUN chown www-data:www-data /var/www/html
|
|
@ -0,0 +1,16 @@
|
||||||
|
.PHONY: install prettier
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
prettier -w .
|
||||||
|
|
||||||
|
|
||||||
|
install: htdocs/vendor
|
||||||
|
|
||||||
|
htdocs/vendor: htdocs/composer.json htdocs/composer.lock
|
||||||
|
cd htdocs ; composer install --no-dev --optimize-autoloader
|
||||||
|
touch htdocs/vendor
|
||||||
|
|
||||||
|
htdocs/composer.lock: htdocs/composer.json
|
||||||
|
cd htdocs ; composer install --no-dev --optimize-autoloader
|
||||||
|
touch htdocs/composer.lock
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
<?php
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
use ICal\Event;
|
||||||
|
use ICal\ICal;
|
||||||
|
|
||||||
|
define("CACHE_DIR", "/tmp/app_data/cache");
|
||||||
|
define("CACHE_TTL", 60 * 60);
|
||||||
|
define("MAX_FILE_SIZE", 1024 * 1024 * 10);
|
||||||
|
|
||||||
|
if (!isset($_REQUEST["url"])) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
echo "No url given";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$url = $_REQUEST["url"];
|
||||||
|
$parsedUrl = parse_url($url);
|
||||||
|
$host = $parsedUrl["host"];
|
||||||
|
$hostIp = gethostbyname($host);
|
||||||
|
if (
|
||||||
|
!filter_var(
|
||||||
|
$hostIp,
|
||||||
|
FILTER_VALIDATE_IP,
|
||||||
|
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 !== strpos($url, "http")) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = isset($_REQUEST["start"]) ? $_REQUEST["start"] : "today";
|
||||||
|
$end = isset($_REQUEST["end"]) ? $_REQUEST["end"] : "tomorrow";
|
||||||
|
$maxItems = isset($_REQUEST["maxitems"]) ? intval($_REQUEST["maxitems"]) : 10;
|
||||||
|
|
||||||
|
$completeCacheFile =
|
||||||
|
CACHE_DIR .
|
||||||
|
"/complete_" .
|
||||||
|
md5(json_encode([$url, $start, $end, $maxItems]));
|
||||||
|
|
||||||
|
$start = strtotime($start);
|
||||||
|
$end = strtotime($end);
|
||||||
|
|
||||||
|
if (false === $start) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
echo "Start value is not a valid strtotime() parameter";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
if (false === $end) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
echo "End value is not a valid strtotime() parameter";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
if ($start > $end) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
echo "Start value is after end value";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end - $start > 60 * 60 * 24 * 90) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
echo "Maximum range is 90 days";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
if ($maxItems > 100) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
echo "Maximum number of items is 100";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
header("X-Debug-used-url: $url");
|
||||||
|
header("X-Debug-start: $start (" . date("Y-m-d", $start) . ")");
|
||||||
|
header("X-Debug-end: $end (" . date("Y-m-d", $end) . ")");
|
||||||
|
header("X-Debug-max-items: $maxItems");
|
||||||
|
|
||||||
|
require_once __DIR__ . "/../vendor/autoload.php";
|
||||||
|
|
||||||
|
if (!is_dir(CACHE_DIR)) {
|
||||||
|
mkdir(CACHE_DIR, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlCacheFile = CACHE_DIR . "/url_" . md5($url);
|
||||||
|
header(
|
||||||
|
"X-Debug-Cache-File-data: " .
|
||||||
|
json_encode([$url, $start, $end, $maxItems], JSON_UNESCAPED_SLASHES)
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
file_exists($completeCacheFile) &&
|
||||||
|
filemtime($completeCacheFile) > time() - CACHE_TTL
|
||||||
|
) {
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
header("X-Debug-Cache-Hit: complete");
|
||||||
|
readfile($completeCacheFile);
|
||||||
|
//use fast response time to allow cache cleaning for 500ms
|
||||||
|
$cacheCleanEnd = microtime(true) + 0.5;
|
||||||
|
$cacheFiles = glob(CACHE_DIR . "/*");
|
||||||
|
do {
|
||||||
|
$cacheFile = array_shift($cacheFiles);
|
||||||
|
if (filemtime($cacheFile) < time() - CACHE_TTL) {
|
||||||
|
unlink($cacheFile);
|
||||||
|
}
|
||||||
|
} while (microtime(true) <= $cacheCleanEnd && count($cacheFiles) > 0);
|
||||||
|
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!file_exists($urlCacheFile) ||
|
||||||
|
filemtime($urlCacheFile) < time() - CACHE_TTL
|
||||||
|
) {
|
||||||
|
$dlStart = microtime(true);
|
||||||
|
$size = 0;
|
||||||
|
$fp_in = fopen($url, "r");
|
||||||
|
$fp_out = fopen($urlCacheFile, "w");
|
||||||
|
while ($fp_in && !feof($fp_in)) {
|
||||||
|
$size += fwrite($fp_out, fread($fp_in, 1024 * 1024));
|
||||||
|
if ($size > MAX_FILE_SIZE) {
|
||||||
|
header("HTTP/1.1 400 Bad Request");
|
||||||
|
echo "File is too big";
|
||||||
|
fclose($fp_in);
|
||||||
|
fclose($fp_out);
|
||||||
|
unlink($urlCacheFile);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($fp_in);
|
||||||
|
fclose($fp_out);
|
||||||
|
$dlEnd = microtime(true);
|
||||||
|
$dlTime = round($dlEnd - $dlStart, 3);
|
||||||
|
header("X-Debug-Download-Time: $dlTime s");
|
||||||
|
$prefixes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
|
$prefix = array_shift($prefixes);
|
||||||
|
while ($size > 1500) {
|
||||||
|
$prefix = array_shift($prefixes);
|
||||||
|
$size = $size / 1024;
|
||||||
|
}
|
||||||
|
$hsize = round($size, 3) . " " . $prefix;
|
||||||
|
header("X-Debug-Download-Size: $hsize");
|
||||||
|
} else {
|
||||||
|
header("X-Debug-Cache-Hit: url");
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$parseStart = microtime(true);
|
||||||
|
$ical = new ICal($urlCacheFile, [
|
||||||
|
"defaultTimeZone" => "UTC",
|
||||||
|
]);
|
||||||
|
$events = $ical->eventsFromRange(
|
||||||
|
date("Y-m-d", strtotime("today")),
|
||||||
|
date("Y-m-d", strtotime("today + 90 days"))
|
||||||
|
);
|
||||||
|
$parseEnd = microtime(true);
|
||||||
|
$parseTime = round($parseEnd - $parseStart, 3);
|
||||||
|
header("X-Debug-Parse-Time: $parseTime s");
|
||||||
|
|
||||||
|
while (count($events) > $maxItems) {
|
||||||
|
array_pop($events);
|
||||||
|
}
|
||||||
|
|
||||||
|
$events = array_map(function (Event $event) {
|
||||||
|
$r = [];
|
||||||
|
@$r["summary"] = $event->summary;
|
||||||
|
@$r["description"] = $event->description;
|
||||||
|
@$r["location"] = $event->location;
|
||||||
|
@$r["start"] = $event->dtstart_array[2];
|
||||||
|
@$r["end"] = $event->dtend_array[2];
|
||||||
|
@$r["duration"] = $event->duration;
|
||||||
|
@$r["url"] = $event->url;
|
||||||
|
@$r["status"] = $event->status;
|
||||||
|
return $r;
|
||||||
|
}, $events);
|
||||||
|
usort($events, function ($a, $b) {
|
||||||
|
return $a["start"] - $b["start"];
|
||||||
|
});
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
header("HTTP/1.1 500 Internal Server Error");
|
||||||
|
echo "Error parsing ical file";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_encode(
|
||||||
|
$events,
|
||||||
|
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
|
||||||
|
);
|
||||||
|
file_put_contents($completeCacheFile, $data);
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
echo $data;
|
||||||
|
|
||||||
|
foreach (glob(CACHE_DIR . "/*") as $file) {
|
||||||
|
if (filemtime($file) < time() - CACHE_TTL) {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
let previewData = [];
|
||||||
|
let getData = function () {
|
||||||
|
let data = {};
|
||||||
|
data.url = document.getElementById("url").value;
|
||||||
|
data.start = document.getElementById("start").value;
|
||||||
|
data.end = document.getElementById("end").value;
|
||||||
|
data.maxitems = document.getElementById("maxitems").value;
|
||||||
|
for (let key in data) {
|
||||||
|
if (data[key] == "") {
|
||||||
|
delete data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("form").onsubmit = function () {
|
||||||
|
// Get data from form
|
||||||
|
let data = getData();
|
||||||
|
// Send data to server as url form encoded
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
let outputEl = document.getElementById("preview");
|
||||||
|
outputEl.innerHTML = "Loading...";
|
||||||
|
outputEl.classList.add("loading");
|
||||||
|
outputEl.classList.remove("error");
|
||||||
|
let url = "/api/?" + new URLSearchParams(data).toString();
|
||||||
|
xhr.open("get", url);
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status == 200) {
|
||||||
|
outputEl.classList.remove("loading");
|
||||||
|
outputEl.classList.remove("error");
|
||||||
|
} else {
|
||||||
|
outputEl.classList.remove("loading");
|
||||||
|
outputEl.classList.add("error");
|
||||||
|
}
|
||||||
|
document.getElementById("preview").innerHTML = xhr.responseText;
|
||||||
|
previewData = JSON.parse(xhr.responseText);
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
document.getElementById("previewUrl").value =
|
||||||
|
window.location.protocol + "//" + window.location.host + url;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let start = document.getElementById("start");
|
||||||
|
start.onchange = function () {
|
||||||
|
let val = this.value;
|
||||||
|
if (val == "") {
|
||||||
|
val = start.getAttribute("placeholder");
|
||||||
|
}
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(
|
||||||
|
"get",
|
||||||
|
"/strtotime/?" +
|
||||||
|
new URLSearchParams({ date: val, format: "Y-m-d" }).toString()
|
||||||
|
);
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status == 200) {
|
||||||
|
document.querySelector(".previewStart").textContent = xhr.responseText;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
};
|
||||||
|
start.onkeyup = start.onchange;
|
||||||
|
start.onchange();
|
||||||
|
|
||||||
|
let end = document.getElementById("end");
|
||||||
|
end.onchange = function () {
|
||||||
|
let val = this.value;
|
||||||
|
if (val == "") {
|
||||||
|
val = end.getAttribute("placeholder");
|
||||||
|
}
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(
|
||||||
|
"get",
|
||||||
|
"/strtotime/?" +
|
||||||
|
new URLSearchParams({ date: val, format: "Y-m-d" }).toString()
|
||||||
|
);
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status == 200) {
|
||||||
|
document.querySelector(".previewEnd").textContent = xhr.responseText;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
};
|
||||||
|
end.onkeyup = end.onchange;
|
||||||
|
end.onchange();
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"johngrogg/ics-parser": "^2.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "10482d28879f2a0ab5cd26452cfd53d5",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "johngrogg/ics-parser",
|
||||||
|
"version": "v2.2.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/u01jmg3/ics-parser.git",
|
||||||
|
"reference": "69c80471a0a99142ebc72b21c2bc084e81a7c4f4"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/69c80471a0a99142ebc72b21c2bc084e81a7c4f4",
|
||||||
|
"reference": "69c80471a0a99142ebc72b21c2bc084e81a7c4f4",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": ">=5.3.9"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4",
|
||||||
|
"squizlabs/php_codesniffer": "~2.9.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"ICal": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": ["MIT"],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jonathan Goode",
|
||||||
|
"role": "Developer/Owner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "John Grogg",
|
||||||
|
"email": "john.grogg@gmail.com",
|
||||||
|
"role": "Developer/Prior Owner"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "ICS Parser",
|
||||||
|
"homepage": "https://github.com/u01jmg3/ics-parser",
|
||||||
|
"keywords": [
|
||||||
|
"iCalendar",
|
||||||
|
"ical",
|
||||||
|
"ical-parser",
|
||||||
|
"ics",
|
||||||
|
"ics-parser",
|
||||||
|
"ifb"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/u01jmg3/ics-parser/issues",
|
||||||
|
"source": "https://github.com/u01jmg3/ics-parser/tree/v2.2.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/sponsors/u01jmg3",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2020-11-02T10:28:33+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-mbstring",
|
||||||
|
"version": "v1.27.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
|
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||||
|
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"ext-mbstring": "*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-mbstring": "For best performance"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "1.27-dev"
|
||||||
|
},
|
||||||
|
"thanks": {
|
||||||
|
"name": "symfony/polyfill",
|
||||||
|
"url": "https://github.com/symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": ["bootstrap.php"],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": ["MIT"],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony polyfill for the Mbstring extension",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": ["compatibility", "mbstring", "polyfill", "portable", "shim"],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2022-11-03T14:55:06+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": [],
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": [],
|
||||||
|
"platform-dev": [],
|
||||||
|
"plugin-api-version": "2.3.0"
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>ICAL2JSON helper</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>URL-Builder</h1>
|
||||||
|
<form id="form">
|
||||||
|
<label><input type="url" name="url" id="url" />ICAL-URL</label>
|
||||||
|
<label
|
||||||
|
><input type="text" name="start" id="start" placeholder="today" />Start
|
||||||
|
Date in a strtotime() compatible format (<span
|
||||||
|
class="previewStart"
|
||||||
|
></span
|
||||||
|
>)</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="text"
|
||||||
|
name="end"
|
||||||
|
id="end"
|
||||||
|
placeholder="last day of next month"
|
||||||
|
/>End Date in a strtotime() compatible format (<span
|
||||||
|
class="previewEnd"
|
||||||
|
></span
|
||||||
|
>)</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
name="maxitems"
|
||||||
|
id="maxitems"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
placeholder="100"
|
||||||
|
/>maximal number of items shown</label
|
||||||
|
>
|
||||||
|
<label><input type="submit" value="Preview" /></label>
|
||||||
|
</form>
|
||||||
|
<h1>Preview data</h1>
|
||||||
|
<label
|
||||||
|
><input type="text" aria-disabled="true" id="previewUrl" value="" /> Your
|
||||||
|
json url</label
|
||||||
|
>
|
||||||
|
<div id="preview">No data</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php
|
||||||
|
$date = strtotime($_REQUEST["date"]);
|
||||||
|
$format = isset($_REQUEST["format"]) ? $_REQUEST["format"] : "U";
|
||||||
|
echo date($format, $date);
|
|
@ -0,0 +1,56 @@
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
label > input {
|
||||||
|
margin: 1em;
|
||||||
|
height: 3em;
|
||||||
|
width: 50em;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
border: 0.3em inset #4c4;
|
||||||
|
min-height: 3em;
|
||||||
|
}
|
||||||
|
#preview.error {
|
||||||
|
border-color: #c44;
|
||||||
|
}
|
||||||
|
#preview.loading {
|
||||||
|
border-color: #cc4;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
display: block;
|
||||||
|
margin: 1em;
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow-wrap {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.grow-wrap::after {
|
||||||
|
content: attr(data-replicated-value) " ";
|
||||||
|
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.grow-wrap > textarea {
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.grow-wrap > textarea,
|
||||||
|
.grow-wrap::after {
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font: inherit;
|
||||||
|
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
}
|
Loading…
Reference in New Issue