init
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# OI Contest Schedule WordPress Plugin
|
||||
|
||||
This subproject packages the OI contest feed as a WordPress plugin. It follows the repository layout and release pattern used by `wp-translate`: the installable plugin lives in its slug directory, while `build.sh` creates a versioned ZIP archive under `build/`.
|
||||
|
||||
## Features
|
||||
|
||||
- Adds an **Upcoming OI Contests** widget to the WordPress dashboard.
|
||||
- Provides the `[oi_contest_schedule]` shortcode for posts and pages.
|
||||
- Formats timestamps in the visitor's browser time zone.
|
||||
- Shows live countdowns and distinguishes currently running contests.
|
||||
- Caches the public GitHub JSON feed with a five-minute WordPress transient.
|
||||
|
||||
## Requirements
|
||||
|
||||
- WordPress 6.4 or newer
|
||||
- PHP 7.4 or newer
|
||||
|
||||
## Usage
|
||||
|
||||
Activate the plugin and add a Shortcode block containing:
|
||||
|
||||
```text
|
||||
[oi_contest_schedule]
|
||||
```
|
||||
|
||||
The shortcode accepts `limit` from 1 to 50 and a compact layout flag:
|
||||
|
||||
```text
|
||||
[oi_contest_schedule limit="20" compact="true"]
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Run from this directory:
|
||||
|
||||
```bash
|
||||
chmod +x build.sh
|
||||
./build.sh build
|
||||
```
|
||||
|
||||
The archive is written to `build/oi-contest-schedule-<version>.zip`.
|
||||
@@ -0,0 +1,41 @@
|
||||
# OI Contest Schedule WordPress 插件
|
||||
|
||||
该子项目将 OI 赛程数据封装为 WordPress 插件。目录和发布方式参考 `wp-translate`:可安装插件位于同名 slug 目录中,`build.sh` 在 `build/` 下生成带版本号的 ZIP 包。
|
||||
|
||||
## 功能
|
||||
|
||||
- 在 WordPress 仪表盘添加“即将到来的 OI 赛事”小组件。
|
||||
- 提供 `[oi_contest_schedule]` 短代码,可插入文章或页面。
|
||||
- 根据访客浏览器时区显示比赛时间。
|
||||
- 显示实时倒计时,并标记正在进行的比赛。
|
||||
- 使用 WordPress transient 将 GitHub 上的公开 JSON 数据缓存五分钟。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- WordPress 6.4 或更新版本
|
||||
- PHP 7.4 或更新版本
|
||||
|
||||
## 使用方式
|
||||
|
||||
启用插件后,在页面中添加“短代码”区块并输入:
|
||||
|
||||
```text
|
||||
[oi_contest_schedule]
|
||||
```
|
||||
|
||||
`limit` 可设置 1 到 50 条比赛,`compact` 可启用紧凑布局:
|
||||
|
||||
```text
|
||||
[oi_contest_schedule limit="20" compact="true"]
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
在本目录运行:
|
||||
|
||||
```bash
|
||||
chmod +x build.sh
|
||||
./build.sh build
|
||||
```
|
||||
|
||||
安装包将生成到 `build/oi-contest-schedule-<版本>.zip`。
|
||||
@@ -0,0 +1,188 @@
|
||||
.oics-schedule {
|
||||
--oics-ink: #20303c;
|
||||
--oics-muted: #687782;
|
||||
--oics-line: #dbe3e8;
|
||||
--oics-surface: #ffffff;
|
||||
--oics-accent: #d94b38;
|
||||
box-sizing: border-box;
|
||||
color: var(--oics-ink);
|
||||
font-family: "Aptos", "Segoe UI", sans-serif;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.oics-schedule *,
|
||||
.oics-schedule *::before,
|
||||
.oics-schedule *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.oics-schedule__header {
|
||||
align-items: end;
|
||||
border-bottom: 2px solid var(--oics-ink);
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.oics-schedule__heading {
|
||||
color: var(--oics-ink);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.25;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.oics-schedule__meta,
|
||||
.oics-schedule__updated {
|
||||
color: var(--oics-muted);
|
||||
font-size: 12px;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.oics-schedule__count {
|
||||
background: #edf2f5;
|
||||
border-radius: 4px;
|
||||
color: #42535f;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.oics-schedule__list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.oics-contest {
|
||||
align-items: center;
|
||||
background: var(--oics-surface);
|
||||
border: 1px solid var(--oics-line);
|
||||
border-left: 3px solid #8b99a3;
|
||||
border-radius: 6px;
|
||||
color: inherit;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: minmax(88px, auto) minmax(0, 1fr) auto;
|
||||
padding: 13px 14px;
|
||||
text-decoration: none !important;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.oics-contest:hover,
|
||||
.oics-contest:focus-visible {
|
||||
border-color: #9eabb4;
|
||||
box-shadow: 0 5px 16px rgba(31, 48, 60, 0.1);
|
||||
color: inherit;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.oics-contest:focus-visible {
|
||||
outline: 2px solid #2271b1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.oics-contest__platform {
|
||||
background: #657681;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.oics-contest__platform--codeforces { background: #2176ae; }
|
||||
.oics-contest__platform--atcoder { background: #527a52; }
|
||||
.oics-contest__platform--uoj { background: #b33b32; }
|
||||
.oics-contest__platform--nowcoder { background: #008c8c; }
|
||||
|
||||
.oics-contest__info {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.oics-contest__title {
|
||||
color: var(--oics-ink);
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.oics-contest__time {
|
||||
color: var(--oics-muted);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
gap: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.oics-contest__countdown {
|
||||
color: var(--oics-accent);
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
min-width: 116px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.oics-schedule__updated {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.oics-schedule__notice {
|
||||
background: #f6f8f9;
|
||||
border-left: 3px solid #8b99a3;
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.oics-schedule__notice--error {
|
||||
background: #fcf0ef;
|
||||
border-left-color: #b32d2e;
|
||||
color: #8a2424;
|
||||
}
|
||||
|
||||
.oics-schedule--compact .oics-schedule__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.oics-schedule--compact .oics-contest {
|
||||
gap: 10px;
|
||||
grid-template-columns: minmax(72px, auto) minmax(0, 1fr);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.oics-schedule--compact .oics-contest__countdown {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.oics-schedule__header {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.oics-schedule__heading {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.oics-contest {
|
||||
gap: 10px;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.oics-contest__countdown {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
( function () {
|
||||
'use strict';
|
||||
|
||||
var strings = window.oicsScheduleL10n || {};
|
||||
|
||||
function formatTime( timestamp, includeSeconds ) {
|
||||
var options = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
|
||||
if ( includeSeconds ) {
|
||||
options.second = '2-digit';
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat( document.documentElement.lang || undefined, options ).format( new Date( timestamp * 1000 ) );
|
||||
}
|
||||
|
||||
function updateTimes() {
|
||||
document.querySelectorAll( '[data-oics-time]' ).forEach( function ( element ) {
|
||||
var timestamp = Number.parseInt( element.dataset.oicsTime, 10 );
|
||||
if ( timestamp ) {
|
||||
element.textContent = formatTime( timestamp, element.hasAttribute( 'data-oics-seconds' ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function countdownText( startTimestamp, endTimestamp ) {
|
||||
var now = Date.now();
|
||||
if ( endTimestamp * 1000 <= now ) {
|
||||
return strings.ended || 'Ended';
|
||||
}
|
||||
if ( startTimestamp * 1000 <= now ) {
|
||||
return strings.running || 'Running';
|
||||
}
|
||||
|
||||
var remaining = startTimestamp * 1000 - now;
|
||||
var days = Math.floor( remaining / 86400000 );
|
||||
var hours = Math.floor( ( remaining % 86400000 ) / 3600000 );
|
||||
var minutes = Math.floor( ( remaining % 3600000 ) / 60000 );
|
||||
var seconds = Math.floor( ( remaining % 60000 ) / 1000 );
|
||||
|
||||
return days + ( strings.day || 'd' ) + ' ' + hours + ( strings.hour || 'h' ) + ' ' + minutes + ( strings.minute || 'm' ) + ' ' + seconds + ( strings.second || 's' );
|
||||
}
|
||||
|
||||
function updateCountdowns() {
|
||||
document.querySelectorAll( '[data-oics-start]' ).forEach( function ( element ) {
|
||||
var startTimestamp = Number.parseInt( element.dataset.oicsStart, 10 );
|
||||
var endTimestamp = Number.parseInt( element.dataset.oicsEnd, 10 );
|
||||
element.textContent = countdownText( startTimestamp, endTimestamp );
|
||||
} );
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
updateTimes();
|
||||
updateCountdowns();
|
||||
window.setInterval( updateCountdowns, 1000 );
|
||||
}
|
||||
|
||||
if ( 'loading' === document.readyState ) {
|
||||
document.addEventListener( 'DOMContentLoaded', initialize );
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
}() );
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
final class OICS_Contest_Client {
|
||||
const DATA_URL = 'https://raw.githubusercontent.com/hanyixuanten/OI-contest-fetch/master/contests_all.json';
|
||||
const TRANSIENT_KEY = 'oics_contest_payload_v1';
|
||||
const CACHE_TTL = 5 * MINUTE_IN_SECONDS;
|
||||
|
||||
public function get_payload() {
|
||||
$cached = get_transient( self::TRANSIENT_KEY );
|
||||
if ( is_array( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$response = wp_safe_remote_get(
|
||||
apply_filters( 'oics_contest_data_url', self::DATA_URL ),
|
||||
array(
|
||||
'timeout' => 10,
|
||||
'user-agent' => 'OI Contest Schedule/' . OICS_VERSION . '; ' . home_url( '/' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return new WP_Error( 'oics_http_error', __( 'The contest data source returned an unexpected response.', 'oi-contest-schedule' ) );
|
||||
}
|
||||
|
||||
$payload = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( ! $this->is_valid_payload( $payload ) ) {
|
||||
return new WP_Error( 'oics_invalid_payload', __( 'The contest data source returned invalid data.', 'oi-contest-schedule' ) );
|
||||
}
|
||||
|
||||
$normalized = array(
|
||||
'generated_at' => isset( $payload['generated_at'] ) ? absint( $payload['generated_at'] ) : 0,
|
||||
'contests' => $this->normalize_contests( $payload['contests'] ),
|
||||
);
|
||||
|
||||
set_transient( self::TRANSIENT_KEY, $normalized, self::CACHE_TTL );
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function is_valid_payload( $payload ) {
|
||||
return is_array( $payload ) && isset( $payload['contests'] ) && is_array( $payload['contests'] );
|
||||
}
|
||||
|
||||
private function normalize_contests( $contests ) {
|
||||
$normalized = array();
|
||||
$now = time();
|
||||
|
||||
foreach ( $contests as $contest ) {
|
||||
if ( ! is_array( $contest ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start_time = isset( $contest['start_time'] ) ? absint( $contest['start_time'] ) : 0;
|
||||
$end_time = isset( $contest['end_time'] ) ? absint( $contest['end_time'] ) : 0;
|
||||
$url = isset( $contest['url'] ) ? esc_url_raw( $contest['url'] ) : '';
|
||||
if ( ! $start_time || ! $end_time || $end_time <= $now || ! $url ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized[] = array(
|
||||
'platform' => isset( $contest['platform'] ) ? sanitize_text_field( $contest['platform'] ) : '',
|
||||
'title' => isset( $contest['title'] ) ? sanitize_text_field( $contest['title'] ) : '',
|
||||
'start_time' => $start_time,
|
||||
'end_time' => $end_time,
|
||||
'status' => $start_time <= $now ? 'running' : 'upcoming',
|
||||
'url' => $url,
|
||||
);
|
||||
}
|
||||
|
||||
usort(
|
||||
$normalized,
|
||||
static function ( $left, $right ) {
|
||||
return $left['start_time'] <=> $right['start_time'];
|
||||
}
|
||||
);
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
final class OICS_Plugin {
|
||||
private static $instance;
|
||||
private $renderer;
|
||||
|
||||
public static function instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->renderer = new OICS_Schedule_Renderer( new OICS_Contest_Client() );
|
||||
|
||||
add_action( 'wp_dashboard_setup', array( $this, 'register_dashboard_widget' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
|
||||
add_shortcode( 'oi_contest_schedule', array( $this, 'render_shortcode' ) );
|
||||
}
|
||||
|
||||
private function register_assets() {
|
||||
wp_register_style( 'oics-schedule', OICS_URL . 'assets/css/schedule.css', array(), OICS_VERSION );
|
||||
wp_register_script( 'oics-schedule', OICS_URL . 'assets/js/schedule.js', array(), OICS_VERSION, true );
|
||||
wp_localize_script(
|
||||
'oics-schedule',
|
||||
'oicsScheduleL10n',
|
||||
array(
|
||||
'running' => __( 'Running', 'oi-contest-schedule' ),
|
||||
'ended' => __( 'Ended', 'oi-contest-schedule' ),
|
||||
'day' => __( 'd', 'oi-contest-schedule' ),
|
||||
'hour' => __( 'h', 'oi-contest-schedule' ),
|
||||
'minute' => __( 'm', 'oi-contest-schedule' ),
|
||||
'second' => __( 's', 'oi-contest-schedule' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function enqueue_frontend_assets() {
|
||||
$post = get_post();
|
||||
if ( ! $post || ! has_shortcode( $post->post_content, 'oi_contest_schedule' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_assets();
|
||||
}
|
||||
|
||||
public function enqueue_admin_assets( $hook_suffix ) {
|
||||
if ( 'index.php' !== $hook_suffix ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_assets();
|
||||
}
|
||||
|
||||
private function enqueue_assets() {
|
||||
$this->register_assets();
|
||||
wp_enqueue_style( 'oics-schedule' );
|
||||
wp_enqueue_script( 'oics-schedule' );
|
||||
}
|
||||
|
||||
public function register_dashboard_widget() {
|
||||
wp_add_dashboard_widget(
|
||||
'oics_contest_schedule',
|
||||
esc_html__( 'Upcoming OI Contests', 'oi-contest-schedule' ),
|
||||
array( $this, 'render_dashboard_widget' )
|
||||
);
|
||||
}
|
||||
|
||||
public function render_dashboard_widget() {
|
||||
echo $this->renderer->render( 8, true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
public function render_shortcode( $attributes ) {
|
||||
$attributes = shortcode_atts(
|
||||
array(
|
||||
'limit' => 10,
|
||||
'compact' => 'false',
|
||||
),
|
||||
$attributes,
|
||||
'oi_contest_schedule'
|
||||
);
|
||||
|
||||
$limit = min( 50, max( 1, absint( $attributes['limit'] ) ) );
|
||||
$compact = filter_var( $attributes['compact'], FILTER_VALIDATE_BOOLEAN );
|
||||
|
||||
return $this->renderer->render( $limit, $compact );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
final class OICS_Schedule_Renderer {
|
||||
private $client;
|
||||
|
||||
public function __construct( OICS_Contest_Client $client ) {
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function render( $limit = 10, $compact = false ) {
|
||||
$payload = $this->client->get_payload();
|
||||
if ( is_wp_error( $payload ) ) {
|
||||
return $this->render_notice( __( 'Contest data is temporarily unavailable. Please try again later.', 'oi-contest-schedule' ), 'error' );
|
||||
}
|
||||
|
||||
$contests = array_slice( $payload['contests'], 0, $limit );
|
||||
if ( empty( $contests ) ) {
|
||||
return $this->render_notice( __( 'No upcoming contests.', 'oi-contest-schedule' ), 'empty' );
|
||||
}
|
||||
|
||||
$classes = 'oics-schedule';
|
||||
if ( $compact ) {
|
||||
$classes .= ' oics-schedule--compact';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="<?php echo esc_attr( $classes ); ?>" data-oics-schedule>
|
||||
<div class="oics-schedule__header">
|
||||
<div>
|
||||
<h2 class="oics-schedule__heading"><?php esc_html_e( 'Upcoming OI Contests', 'oi-contest-schedule' ); ?></h2>
|
||||
<p class="oics-schedule__meta">
|
||||
<?php esc_html_e( 'Times use your device time zone.', 'oi-contest-schedule' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<span class="oics-schedule__count"><?php echo esc_html( sprintf( _n( '%d contest', '%d contests', count( $contests ), 'oi-contest-schedule' ), count( $contests ) ) ); ?></span>
|
||||
</div>
|
||||
<div class="oics-schedule__list">
|
||||
<?php foreach ( $contests as $contest ) : ?>
|
||||
<?php $this->render_contest( $contest ); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ( ! empty( $payload['generated_at'] ) ) : ?>
|
||||
<p class="oics-schedule__updated">
|
||||
<?php esc_html_e( 'Data updated', 'oi-contest-schedule' ); ?>:
|
||||
<time data-oics-time="<?php echo esc_attr( $payload['generated_at'] ); ?>" data-oics-seconds></time>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
private function render_contest( $contest ) {
|
||||
$platform_class = sanitize_html_class( strtolower( $contest['platform'] ) );
|
||||
?>
|
||||
<a class="oics-contest" href="<?php echo esc_url( $contest['url'] ); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<span class="oics-contest__platform oics-contest__platform--<?php echo esc_attr( $platform_class ); ?>"><?php echo esc_html( $contest['platform'] ); ?></span>
|
||||
<span class="oics-contest__info">
|
||||
<strong class="oics-contest__title"><?php echo esc_html( $contest['title'] ); ?></strong>
|
||||
<span class="oics-contest__time">
|
||||
<time data-oics-time="<?php echo esc_attr( $contest['start_time'] ); ?>"></time>
|
||||
<span aria-hidden="true">–</span>
|
||||
<time data-oics-time="<?php echo esc_attr( $contest['end_time'] ); ?>"></time>
|
||||
</span>
|
||||
</span>
|
||||
<span class="oics-contest__countdown" data-oics-start="<?php echo esc_attr( $contest['start_time'] ); ?>" data-oics-end="<?php echo esc_attr( $contest['end_time'] ); ?>">
|
||||
<?php echo 'running' === $contest['status'] ? esc_html__( 'Running', 'oi-contest-schedule' ) : esc_html__( 'Upcoming', 'oi-contest-schedule' ); ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function render_notice( $message, $type ) {
|
||||
return sprintf(
|
||||
'<div class="oics-schedule"><p class="oics-schedule__notice oics-schedule__notice--%1$s">%2$s</p></div>',
|
||||
esc_attr( $type ),
|
||||
esc_html( $message )
|
||||
);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OI Contest Schedule 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/hanyixuanten/OI-contest-fetch/issues\n"
|
||||
"POT-Creation-Date: 2026-08-06 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-08-06 00:00+0000\n"
|
||||
"Last-Translator: hanyixuanten\n"
|
||||
"Language-Team: Simplified Chinese\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Domain: oi-contest-schedule\n"
|
||||
|
||||
msgid "Upcoming OI Contests"
|
||||
msgstr "即将到来的 OI 赛事"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "进行中"
|
||||
|
||||
msgid "Ended"
|
||||
msgstr "已结束"
|
||||
|
||||
msgid "Upcoming"
|
||||
msgstr "即将开始"
|
||||
|
||||
msgid "d"
|
||||
msgstr "天"
|
||||
|
||||
msgid "h"
|
||||
msgstr "时"
|
||||
|
||||
msgid "m"
|
||||
msgstr "分"
|
||||
|
||||
msgid "s"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "The contest data source returned an unexpected response."
|
||||
msgstr "赛事数据源返回了异常响应。"
|
||||
|
||||
msgid "The contest data source returned invalid data."
|
||||
msgstr "赛事数据源返回了无效数据。"
|
||||
|
||||
msgid "Contest data is temporarily unavailable. Please try again later."
|
||||
msgstr "暂时无法加载赛事数据,请稍后再试。"
|
||||
|
||||
msgid "No upcoming contests."
|
||||
msgstr "暂无即将开始的赛事。"
|
||||
|
||||
msgid "Times use your device time zone."
|
||||
msgstr "时间按设备时区显示。"
|
||||
|
||||
msgid "%d contest"
|
||||
msgid_plural "%d contests"
|
||||
msgstr[0] "%d 场赛事"
|
||||
|
||||
msgid "Data updated"
|
||||
msgstr "数据更新时间"
|
||||
@@ -0,0 +1,56 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OI Contest Schedule 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/hanyixuanten/OI-contest-fetch/issues\n"
|
||||
"POT-Creation-Date: 2026-08-06 00:00+0000\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Domain: oi-contest-schedule\n"
|
||||
|
||||
msgid "Upcoming OI Contests"
|
||||
msgstr ""
|
||||
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ended"
|
||||
msgstr ""
|
||||
|
||||
msgid "Upcoming"
|
||||
msgstr ""
|
||||
|
||||
msgid "d"
|
||||
msgstr ""
|
||||
|
||||
msgid "h"
|
||||
msgstr ""
|
||||
|
||||
msgid "m"
|
||||
msgstr ""
|
||||
|
||||
msgid "s"
|
||||
msgstr ""
|
||||
|
||||
msgid "The contest data source returned an unexpected response."
|
||||
msgstr ""
|
||||
|
||||
msgid "The contest data source returned invalid data."
|
||||
msgstr ""
|
||||
|
||||
msgid "Contest data is temporarily unavailable. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "No upcoming contests."
|
||||
msgstr ""
|
||||
|
||||
msgid "Times use your device time zone."
|
||||
msgstr ""
|
||||
|
||||
msgid "%d contest"
|
||||
msgid_plural "%d contests"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Data updated"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: OI Contest Schedule
|
||||
* Plugin URI: https://github.com/hanyixuanten/OI-contest-fetch
|
||||
* Description: Display upcoming OI contests in a dashboard widget or on any page with a shortcode.
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 7.4
|
||||
* Author: hanyixuanten
|
||||
* Author URI: https://www.vblg.top
|
||||
* License: GPL-3.0-only
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
* Text Domain: oi-contest-schedule
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'OICS_VERSION', '0.1.0' );
|
||||
define( 'OICS_FILE', __FILE__ );
|
||||
define( 'OICS_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'OICS_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
require_once OICS_PATH . 'includes/class-contest-client.php';
|
||||
require_once OICS_PATH . 'includes/class-schedule-renderer.php';
|
||||
require_once OICS_PATH . 'includes/class-plugin.php';
|
||||
|
||||
function oics_load_textdomain() {
|
||||
load_plugin_textdomain( 'oi-contest-schedule', false, dirname( plugin_basename( OICS_FILE ) ) . '/languages' );
|
||||
}
|
||||
|
||||
add_action( 'init', 'oics_load_textdomain' );
|
||||
add_action( 'plugins_loaded', array( 'OICS_Plugin', 'instance' ) );
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
=== OI Contest Schedule ===
|
||||
Contributors: hanyixuanten
|
||||
Requires at least: 6.4
|
||||
Tested up to: 6.9
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 0.1.0
|
||||
License: GPL-3.0-only
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Display upcoming OI contests in the WordPress dashboard or on a page.
|
||||
|
||||
== Description ==
|
||||
|
||||
OI Contest Schedule reads the public contest feed generated by OI-contest-fetch and displays active contests with local times and live countdowns.
|
||||
|
||||
The plugin adds an Upcoming OI Contests widget to the WordPress dashboard. Use the `[oi_contest_schedule]` shortcode in a post, page, or Shortcode block to show the schedule on the front end.
|
||||
|
||||
Shortcode options:
|
||||
|
||||
* `[oi_contest_schedule limit="10"]` controls the number of contests, from 1 to 50.
|
||||
* `[oi_contest_schedule compact="true"]` uses the compact dashboard-style layout.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `oi-contest-schedule` directory to `/wp-content/plugins/` or install the generated ZIP archive.
|
||||
2. Activate OI Contest Schedule through the Plugins screen.
|
||||
3. Open the WordPress dashboard to see the widget, or insert `[oi_contest_schedule]` into a page.
|
||||
|
||||
== External Services ==
|
||||
|
||||
This plugin requests the public `contests_all.json` file from GitHub Raw Content when its five-minute WordPress transient cache expires. The request is required to obtain the latest contest schedule. No visitor content, account data, or cookies are sent by the plugin. Standard request metadata, such as the website server IP address and the plugin user-agent string, is visible to GitHub.
|
||||
|
||||
* Data source: https://raw.githubusercontent.com/hanyixuanten/OI-contest-fetch/master/contests_all.json
|
||||
* GitHub Terms of Service: https://docs.github.com/en/site-policy/github-terms/github-terms-of-service
|
||||
* GitHub Privacy Statement: https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= Which contests are shown? =
|
||||
|
||||
The plugin shows contests whose end time is still in the future, including contests currently running. Finished contests are omitted.
|
||||
|
||||
= Which time zone is used? =
|
||||
|
||||
Dates are formatted in the visitor's browser time zone using the browser internationalization API.
|
||||
|
||||
= Can I use another data URL? =
|
||||
|
||||
Developers can replace the URL with the `oics_contest_data_url` filter.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 0.1.0 =
|
||||
|
||||
* Add the upcoming contest dashboard widget.
|
||||
* Add the `[oi_contest_schedule]` shortcode with limit and compact options.
|
||||
* Add local-time formatting, countdowns, platform colors, and transient caching.
|
||||
Reference in New Issue
Block a user