For example, if today is January 2nd, then the New Year's Day on January 1st should be calculated using January 1st of the next year.
480 lines
14 KiB
JavaScript
480 lines
14 KiB
JavaScript
let ip = null;
|
|
fetch("https://api.ipify.org?format=json").then((response) => {
|
|
if (response.ok) {
|
|
return response.json();
|
|
}
|
|
|
|
throw new Error("Network response was not ok.");
|
|
}).then((res) => {
|
|
ip = res.ip;
|
|
}).catch((_error) => {
|
|
if ("caches" in window) {
|
|
caches.match("https://api.ipify.org?format=json").then((response) => {
|
|
if (response) {
|
|
return response.json();
|
|
}
|
|
}).then((data) => {
|
|
if (ip === null && data !== undefined) {
|
|
ip = JSON.parse(data).ip;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
let goodFortunes = [];
|
|
let badFortunes = [];
|
|
let special_events = [];
|
|
let fortune_generated = false;
|
|
|
|
// using async and await to prevent fetching the data too late...
|
|
async function fetch_data() {
|
|
await fetch("./json/fortune.json")
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
goodFortunes = data.goodFortunes;
|
|
badFortunes = data.badFortunes;
|
|
});
|
|
|
|
async function fetch_events(path) {
|
|
await fetch(path)
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
special_events.push(...data.special_events);
|
|
});
|
|
}
|
|
|
|
await fetch_events("./json/custom_special.json");
|
|
await fetch_events("./json/static_special.json");
|
|
await fetch_events("./json/cyclical_special.json");
|
|
}
|
|
|
|
const textColorClass = [
|
|
"good-fortune",
|
|
"good-fortune",
|
|
"good-fortune",
|
|
"good-fortune",
|
|
"good-fortune",
|
|
"middle-fortune",
|
|
"bad-fortune",
|
|
"bad-fortune",
|
|
];
|
|
const fortuneStatus = [
|
|
"大吉",
|
|
"中吉",
|
|
"小吉",
|
|
"吉",
|
|
"末吉",
|
|
"中平",
|
|
"凶",
|
|
"大凶",
|
|
];
|
|
const chineseMonth = [
|
|
"一",
|
|
"二",
|
|
"三",
|
|
"四",
|
|
"五",
|
|
"六",
|
|
"七",
|
|
"八",
|
|
"九",
|
|
"十",
|
|
"十一",
|
|
"十二",
|
|
];
|
|
const week = ["日", "一", "二", "三", "四", "五", "六"];
|
|
|
|
const title =
|
|
`<span class="title" style="font-size:8vmin;"><b>今日運勢</b></span>`;
|
|
const allGood =
|
|
`<span class="bad-fortune" style="font-size:6vmin;"><b>萬事皆宜</b></span>`;
|
|
const allBad =
|
|
`<span class="good-fortune" style="font-size:6vmin;"><b>諸事不宜</b></span>`;
|
|
|
|
// date
|
|
const d = new Date();
|
|
const date = d.getDate();
|
|
const day = d.getDay();
|
|
const month = d.getMonth() + 1;
|
|
const year = d.getFullYear();
|
|
|
|
function validateNumber(value, min, max, fieldName, event) {
|
|
value = parseInt(value);
|
|
if (isNaN(value) || value < min || value > max) {
|
|
console.warn(`illegal event: ${fieldName} should be between ${min} and ${max}`, event);
|
|
return null;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function isLeapYear(year) {
|
|
if (year % 400 === 0) return true;
|
|
if (year % 100 === 0) return false;
|
|
if (year % 4 === 0) return true;
|
|
return false;
|
|
}
|
|
|
|
const daysPerMonth = [
|
|
0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
|
];
|
|
const maxDate = new Date(8640000000000000);
|
|
|
|
function daysDiff(eventIndex) {
|
|
// define the date right now and the special event date
|
|
const event = special_events[eventIndex];
|
|
const startDate = new Date(year, month - 1, date);
|
|
let eventYear = -1, eventMonth = -1, eventDate = -1;
|
|
if (!('triggerDate' in event)) {
|
|
console.warn('illegal event: missing `triggerDate` field', event);
|
|
return -1;
|
|
} else if (Object.prototype.toString.call(event.triggerDate) !== "[object Object]") {
|
|
console.warn('illegal event: `triggerDate` field should be a json object', event);
|
|
return -1;
|
|
}
|
|
const triggerDate = event.triggerDate;
|
|
|
|
let isCustomEvent = false;
|
|
eventYear = year;
|
|
if ('year' in triggerDate) {
|
|
eventYear = validateNumber(triggerDate.year, 1, maxDate.getFullYear(), 'triggerDate.year', event);
|
|
if (eventYear === null) {
|
|
return -1;
|
|
}
|
|
isCustomEvent = true;
|
|
}
|
|
|
|
if (!('month' in triggerDate)) {
|
|
console.warn('illegal event: `triggerDate` missing `month` field', event);
|
|
return -1;
|
|
}
|
|
eventMonth = validateNumber(triggerDate.month, 1, 12, 'triggerDate.Month', event);
|
|
if (eventMonth === null) {
|
|
return -1;
|
|
}
|
|
|
|
if (!('date' in triggerDate) && (!('week' in triggerDate) || !('weekday' in triggerDate))) {
|
|
console.warn('illegal event: `triggerDate` require (`week` and `weekday`) or `date` field', event);
|
|
return -1;
|
|
}
|
|
|
|
if ('date' in triggerDate) {
|
|
let days = daysPerMonth[eventMonth];
|
|
if (isLeapYear(eventYear) && eventMonth == 2) days += 1;
|
|
eventDate = validateNumber(triggerDate.date, 1, days, 'triggerDate.date', event);
|
|
if (eventDate === null) {
|
|
return -1;
|
|
}
|
|
} else {
|
|
|
|
triggerDate.week = validateNumber(triggerDate.week, 1, 5, 'triggerDate.week', event);
|
|
triggerDate.weekday = validateNumber(triggerDate.weekday, 1, 7, 'triggerDate.weekday', event);
|
|
if (triggerDate.week === null || triggerDate.weekday === null) {
|
|
return -1;
|
|
}
|
|
|
|
const firstDayOfMonth = new Date(eventYear, eventMonth - 1, 1);
|
|
const firstDayWeekday = firstDayOfMonth.getDay();
|
|
|
|
// Sunday -> 7
|
|
const adjustedFirstDayWeekday = firstDayWeekday === 0 ? 7 : firstDayWeekday;
|
|
const firstTargetDay = 1 + (triggerDate.weekday - adjustedFirstDayWeekday + 7) % 7;
|
|
eventDate = firstTargetDay + (triggerDate.week - 1) * 7;
|
|
}
|
|
|
|
if (!isCustomEvent && (month > eventMonth || (month == eventMonth && date > eventDate))) {
|
|
eventYear += 1;
|
|
}
|
|
|
|
const endDate = new Date(
|
|
eventYear,
|
|
eventMonth - 1,
|
|
eventDate,
|
|
);
|
|
|
|
|
|
// calculate the difference in milliseconds and convert it to days
|
|
const timeDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
|
|
return timeDiff;
|
|
}
|
|
|
|
// pre-search jquery - save to a variable to improve performance
|
|
const J_l_1_event = $("#l-1-event");
|
|
const J_l_1_desc = $("#l-1-desc");
|
|
const J_l_2_event = $("#l-2-event");
|
|
const J_l_2_desc = $("#l-2-desc");
|
|
const J_r_1_event = $("#r-1-event");
|
|
const J_r_1_desc = $("#r-1-desc");
|
|
const J_r_2_event = $("#r-2-event");
|
|
const J_r_2_desc = $("#r-2-desc");
|
|
const J_ip_to_fortune = $("#ip-to-fortune");
|
|
|
|
let special = false;
|
|
let special_events_index = 0;
|
|
|
|
// init page
|
|
async function init_page() {
|
|
// fetch fortune.json and special.json
|
|
await fetch_data();
|
|
|
|
// hide the elements of show fortune page
|
|
$("#result-page").hide();
|
|
|
|
// show date before button pressed
|
|
const showMonth =
|
|
`<span class="date-color" style="font-size:10vmin; -webkit-writing-mode:vertical-lr;"><b>${
|
|
chineseMonth[month - 1] + "月"
|
|
}</b></span>`;
|
|
const showDate = `<span class="date-color" style="font-size:25vmin;"><b>${
|
|
("0" + date).slice(-2)
|
|
}</b></span>`;
|
|
const showDay =
|
|
`<span class="date-color" style="font-size:10vmin; -webkit-writing-mode:vertical-lr; margin-right:10%;"><b>${
|
|
"星期" + week[day]
|
|
}</b></span>`;
|
|
|
|
$("#month").html(showMonth);
|
|
$("#date").html(showDate);
|
|
$("#weekday").html(showDay);
|
|
|
|
const showSpecialEventCount = 2;
|
|
let eventIndexList = Array(showSpecialEventCount).fill(-1);
|
|
let eventDiffDaysIndexList = Array(showSpecialEventCount).fill(Number.MAX_SAFE_INTEGER);
|
|
|
|
// check if there is special event today
|
|
for (let i = 0; i < special_events.length; i++) {
|
|
let diffCount = daysDiff(i);
|
|
if (diffCount > 0) {
|
|
let j = 0;
|
|
for (; j < showSpecialEventCount; j++) {
|
|
if (diffCount < eventDiffDaysIndexList[j]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
eventDiffDaysIndexList[j] = diffCount;
|
|
eventIndexList[j] = i;
|
|
} else if (diffCount === 0) {
|
|
special = true;
|
|
special_events_index = i;
|
|
}
|
|
}
|
|
// if there is upcoming event then show
|
|
for (let eventIndex = 0; eventIndex < showSpecialEventCount; eventIndex++) {
|
|
if (eventIndexList[eventIndex] != -1) {
|
|
const days = daysDiff(eventIndexList[eventIndex]);
|
|
const upcoming_event =
|
|
`<span class="desc" style="font-size:5vmin;">距離<b class="special-event">${
|
|
special_events[eventIndexList[eventIndex]].event
|
|
}</b>還剩<b class="special-event">${days}</b>天</span>`;
|
|
$(`#upcoming-event-${eventIndex + 1}`).html(upcoming_event);
|
|
}
|
|
}
|
|
|
|
// show special event if today is a special day
|
|
if (special) {
|
|
const special_event_today =
|
|
`<span class="desc" style="font-size:9vmin;">今日是<b class="good-fortune">${
|
|
special_events[special_events_index].event
|
|
}</b></span>`;
|
|
$("#special-day").html(special_event_today);
|
|
}
|
|
|
|
const last_date_str = localStorage.getItem("last_date");
|
|
if (last_date_str !== null && last_date_str !== undefined) {
|
|
const now_date = new Date();
|
|
const last_date = new Date(last_date_str);
|
|
|
|
if (
|
|
now_date.getFullYear() === last_date.getFullYear() &&
|
|
now_date.getMonth() === last_date.getMonth() &&
|
|
now_date.getDate() === last_date.getDate()
|
|
) {
|
|
fortune_generated = true;
|
|
Appear();
|
|
}
|
|
}
|
|
}
|
|
|
|
// event bar
|
|
const good_span = (event) =>
|
|
`<span class="good-fortune" style="font-size:5.6vmin;"><b>宜: </b>${event}</span>`;
|
|
const bad_span = (event) =>
|
|
`<span class="bad-fortune" style="font-size:5.6vmin;"><b>忌: </b>${event}</span>`;
|
|
const desc_span = (desc) =>
|
|
`<span class="desc" style="font-size:3.5vmin;">${desc}</span>`;
|
|
|
|
function Appear() {
|
|
$("#title").html(title);
|
|
$("#btn").html("打卡成功");
|
|
// disable the btn
|
|
$("#btn").attr("disabled", "disabled");
|
|
//change page
|
|
$("#init-page").hide();
|
|
$("#result-page").show();
|
|
|
|
// some lengths
|
|
const goodLen = goodFortunes.length;
|
|
const badLen = badFortunes.length;
|
|
const statusLen = fortuneStatus.length;
|
|
|
|
let status_index = -1;
|
|
let seed1 = -1;
|
|
let seed2 = -1;
|
|
|
|
if (!fortune_generated) {
|
|
// transform ip to four numbers
|
|
const num = ip.split(".").map((num) => parseInt(num));
|
|
|
|
// TODO: improve the hash process
|
|
const hashDate = Math.round(
|
|
Math.log10(
|
|
year *
|
|
((month << (Math.log10(num[3]) + day - 1)) *
|
|
(date << Math.log10(num[2] << day))),
|
|
),
|
|
);
|
|
seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) +
|
|
(num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) +
|
|
(year * day) >> 2;
|
|
seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) +
|
|
(num[2] << 1) * (num[3] << 2) +
|
|
(date << (hashDate - 1)) * (month << 4) + year >>
|
|
hashDate + (date * day) >> 1;
|
|
|
|
// decide the status
|
|
status_index = ((seed1 + seed2) % statusLen + statusLen) % statusLen;
|
|
|
|
// update last record
|
|
localStorage.setItem("last_date", d.toISOString());
|
|
localStorage.setItem("last_status_index", status_index.toString());
|
|
localStorage.setItem("last_seed1", seed1.toString());
|
|
localStorage.setItem("last_seed2", seed2.toString());
|
|
} else {
|
|
status_index = parseInt(localStorage.getItem("last_status_index"));
|
|
seed1 = parseInt(localStorage.getItem("last_seed1"));
|
|
seed2 = parseInt(localStorage.getItem("last_seed2"));
|
|
}
|
|
|
|
const status = `<span class=${
|
|
textColorClass[status_index]
|
|
} style="font-size:12vmin;"><b>§ ${fortuneStatus[status_index]} §</b></span>`;
|
|
|
|
if (special) {
|
|
status_index = special_events[special_events_index].status_index;
|
|
const special_status = `<span class=${
|
|
textColorClass[status_index]
|
|
} style="font-size:12vmin;"><b>§ ${
|
|
fortuneStatus[status_index]
|
|
} §</b></span>`;
|
|
J_ip_to_fortune.html(special_status);
|
|
} else {
|
|
J_ip_to_fortune.html(status);
|
|
}
|
|
|
|
// make sure the events won't collide
|
|
const set = new Set();
|
|
const l1 = (seed1 % goodLen + goodLen) % goodLen;
|
|
set.add(goodFortunes[l1].event);
|
|
let l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen;
|
|
while (set.has(goodFortunes[l2].event)) {
|
|
l2 = (l2 + 1) % goodLen;
|
|
}
|
|
set.add(goodFortunes[l2].event);
|
|
let r1 = (((seed1 >> 1) + (d.getMonth() << 3)) % badLen + badLen) % badLen;
|
|
while (set.has(badFortunes[r1].event)) {
|
|
r1 = (r1 + 2) % badLen;
|
|
}
|
|
set.add(badFortunes[r1].event);
|
|
let r2 = ((((((seed1 << 3) + (d.getFullYear() >> 5) * (date << 2)) % badLen) *
|
|
seed2) >> 6) % badLen + badLen) % badLen;
|
|
while (set.has(badFortunes[r2].event)) {
|
|
r2 = (r2 + 1) % badLen;
|
|
}
|
|
|
|
// organize the stuffs below this line...
|
|
const l1_desc_list = goodFortunes[l1].description;
|
|
const l2_desc_list = goodFortunes[l2].description;
|
|
const r1_desc_list = badFortunes[r1].description;
|
|
const r2_desc_list = badFortunes[r2].description;
|
|
const l_1_event = good_span(goodFortunes[l1].event);
|
|
const l_1_desc = desc_span(
|
|
l1_desc_list[Math.abs(seed1) % l1_desc_list.length],
|
|
);
|
|
const l_2_event = good_span(goodFortunes[l2].event);
|
|
const l_2_desc = desc_span(
|
|
l2_desc_list[Math.abs(seed2) % l2_desc_list.length],
|
|
);
|
|
const r_1_event = bad_span(badFortunes[r1].event);
|
|
const r_1_desc = desc_span(
|
|
r1_desc_list[Math.abs(seed1) % r1_desc_list.length],
|
|
);
|
|
const r_2_event = bad_span(badFortunes[r2].event);
|
|
const r_2_desc = desc_span(
|
|
r2_desc_list[Math.abs(seed2) % r2_desc_list.length],
|
|
);
|
|
|
|
if (special) {
|
|
// instead clear variable name, use short variable name for here... cuz it's too repetitive
|
|
const Data = special_events[special_events_index];
|
|
if (status_index == 0) {
|
|
J_r_1_event.html(allGood);
|
|
} else {
|
|
J_r_1_event.html(bad_span(Data.badFortunes.r_1_event));
|
|
J_r_1_desc.html(desc_span(Data.badFortunes.r_1_desc));
|
|
J_r_2_event.html(bad_span(Data.badFortunes.r_2_event));
|
|
J_r_2_desc.html(desc_span(Data.badFortunes.r_2_desc));
|
|
|
|
if (Data.badFortunes.r_1_event.length == 0) {
|
|
J_r_1_event.html(r_1_event);
|
|
J_r_1_desc.html(r_1_desc);
|
|
}
|
|
if (Data.badFortunes.r_2_event.length == 0) {
|
|
J_r_2_event.html(r_2_event);
|
|
J_r_2_desc.html(r_2_desc);
|
|
}
|
|
}
|
|
if (status_index == statusLen - 1) {
|
|
J_l_1_event.html(allBad);
|
|
} else {
|
|
J_l_1_event.html(good_span(Data.goodFortunes.l_1_event));
|
|
J_l_1_desc.html(desc_span(Data.goodFortunes.l_1_desc));
|
|
J_l_2_event.html(good_span(Data.goodFortunes.l_2_event));
|
|
J_l_2_desc.html(desc_span(Data.goodFortunes.l_2_desc));
|
|
|
|
if (Data.goodFortunes.l_1_event.length == 0) {
|
|
J_l_1_event.html(l_1_event);
|
|
J_l_1_desc.html(l_1_desc);
|
|
}
|
|
if (Data.goodFortunes.l_2_event.length == 0) {
|
|
J_l_2_event.html(l_2_event);
|
|
J_l_2_desc.html(l_2_desc);
|
|
}
|
|
}
|
|
} else {
|
|
if (status_index == 0) {
|
|
J_r_1_event.html(allGood);
|
|
} else {
|
|
J_r_1_event.html(r_1_event);
|
|
J_r_1_desc.html(r_1_desc);
|
|
J_r_2_event.html(r_2_event);
|
|
J_r_2_desc.html(r_2_desc);
|
|
}
|
|
|
|
if (status_index == statusLen - 1) {
|
|
J_l_1_event.html(allBad);
|
|
} else {
|
|
J_l_1_event.html(l_1_event);
|
|
J_l_1_desc.html(l_1_desc);
|
|
J_l_2_event.html(l_2_event);
|
|
J_l_2_desc.html(l_2_desc);
|
|
}
|
|
}
|
|
$("#copy-result-button").removeClass("d-none");
|
|
}
|
|
|
|
function getLuck() {
|
|
Update();
|
|
}
|
|
|
|
init_page();
|