Skip to content

Commit d58b02e

Browse files
Merge pull request guardian#7624 from guardian/sudoku
Sudokus
2 parents 63dc221 + 602ee71 commit d58b02e

File tree

20 files changed

+728
-0
lines changed

20 files changed

+728
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package controllers
2+
3+
import common.ExecutionContexts
4+
import conf.Switches
5+
import play.api.mvc.{Action, Controller}
6+
import sudoku.{SudokuPage, SudokuApi}
7+
import views.html.sudoku
8+
9+
import scala.concurrent.Future
10+
11+
object SudokusController extends Controller with ExecutionContexts {
12+
def render(id: String) = Action.async { implicit request =>
13+
if (Switches.SudokuSwitch.isSwitchedOn) {
14+
SudokuApi.getData(id) map {
15+
case Some(sudokuData) =>
16+
Ok(sudoku(new SudokuPage(sudokuData)))
17+
18+
case None => NotFound(s"No Sudoku with id $id")
19+
}
20+
} else {
21+
Future.successful(NotFound("Sudokus off"))
22+
}
23+
}
24+
}
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package sudoku
2+
3+
import play.api.libs.json.{JsString, JsObject, Json}
4+
5+
import scala.concurrent.Future
6+
7+
object Sudoku {
8+
implicit val jsonFormat = Json.format[Sudoku]
9+
}
10+
11+
case class Sudoku(
12+
id: String,
13+
title: String,
14+
grid: Seq[Seq[Option[Int]]]
15+
)
16+
17+
object SudokuApi {
18+
def getData(id: String) = {
19+
/** Here's one I prepared earlier ...
20+
*
21+
* TODO: connect up with our actual Sudoku source of data.
22+
*/
23+
Future.successful(
24+
Json.fromJson[Sudoku](JsObject(Seq(
25+
"id" -> JsString("sudoku/123"),
26+
"title" -> JsString("Sudoku Easy 123"),
27+
"grid" -> Json.parse(
28+
"""
29+
|[
30+
| [null, 2, null, 5, null, null, null, 4, null],
31+
| [8, null, null, null, null, null, null, null, null],
32+
| [null, null, 4, null, 7, null, null, null, null],
33+
| [null, null, 3, null, 8, null, null, 2, null],
34+
| [null, null, null, null, 6, null, 9, 5, null],
35+
| [7, null, null, 4, 3, 5, 6, null, null],
36+
| [null, null, null, 3, null, null, 1, 6, 9],
37+
| [null, 3, null, null, 9, null, null, 8, null],
38+
| [null, 1, null, 2, null, null, null, 7, 5]
39+
|]
40+
""".stripMargin
41+
)
42+
))
43+
).asOpt
44+
)
45+
}
46+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package sudoku
2+
3+
import model.MetaData
4+
import play.api.libs.json.{JsString, JsValue}
5+
6+
class SudokuPage(val sudoku: Sudoku) extends MetaData {
7+
override def id: String = sudoku.id
8+
9+
override def section: String = "sudokus"
10+
11+
override def analyticsName: String = id
12+
13+
override def webTitle: String = sudoku.title
14+
15+
override def metaData: Map[String, JsValue] = super.metaData ++ Map(
16+
"section" -> JsString("lifeandstyle"),
17+
"series" -> JsString("Sudoku")
18+
)
19+
}
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@(sudokuPage: _root_.sudoku.SudokuPage)(implicit request: RequestHeader)
2+
3+
@import common.LinkTo
4+
@import play.api.libs.json._
5+
6+
@main(sudokuPage) { } {
7+
<div class="l-side-margins">
8+
<article id="crossword" class="content content--article tonal tonal--tone-news" role="main">
9+
<header class="content__head tonal__head tonal__head--tone-news">
10+
<div class="gs-container">
11+
<div class="content__main-column">
12+
<div class="content__labels">
13+
<div class="content__section-label">
14+
<a class="tone-colour" data-link-name="article section" href="@LinkTo("type/sudoku")">Sudokus</a>
15+
</div>
16+
</div>
17+
<h1 itemprop="headline" class="content__headline js-score">@Html(sudokuPage.sudoku.title)</h1>
18+
</div>
19+
</div>
20+
</header>
21+
22+
<div class="content__main tonal__main tonal__main--tone-news">
23+
<div class="gs-container">
24+
<div class="js-content-main-column">
25+
<div class="js-sudoku sudoku__container" data-sudoku-data="@Json.stringify(Json.toJson(sudokuPage.sudoku.grid))">
26+
</div>
27+
</div>
28+
</div>
29+
</div>
30+
</article>
31+
</div>
32+
}

applications/conf/routes

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ GET /$path<.+>/$year<\d\d\d\d>/$month<\w\w\w>/$day<\d\d>/all
2929
GET /$path<.+>/$year<\d\d\d\d>/$month<\w\w\w>/$day<\d\d>/newer controllers.AllIndexController.newer(path, day, month, year)
3030
GET /$path<.+>/all controllers.AllIndexController.all(path)
3131

32+
# Sudokus
33+
GET /sudokus/:id controllers.SudokusController.render(id)
34+
3235
# Gallery paths
3336
GET /$path<[\w\d-]*(/[\w\d-]*)?/gallery/.*>/lightbox.json controllers.GalleryController.lightboxJson(path)
3437
GET /$path<[\w\d-]*(/[\w\d-]*)?/gallery/.*>.json controllers.GalleryController.renderJson(path)

common/app/conf/switches.scala

+5
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,11 @@ object Switches {
472472
safeState = Off, sellByDate = never
473473
)
474474

475+
val SudokuSwitch = Switch("Feature", "sudoku",
476+
"If switched on, sudokus will be available",
477+
safeState = Off, sellByDate = never
478+
)
479+
475480
val CricketScoresSwitch = Switch("Feature", "cricket-scores",
476481
"If switched on, cricket score and scorecard link will be displayed",
477482
safeState = Off, sellByDate = never

common/app/views/fragments/javaScriptLaterSteps.scala.html

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
@if(Configuration.environment.isPreview) {
3333
'bootstraps/preview': '@Static("javascripts/bootstraps/preview.js")',
3434
}
35+
'bootstraps/sudoku': '@Static("javascripts/bootstraps/sudoku.js")',
3536
'bootstraps/video-player': '@Static("javascripts/bootstraps/video-player.js")'
3637
}
3738
};

grunt-configs/requirejs.js

+11
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ module.exports = function(grunt, options) {
8080
]
8181
}
8282
},
83+
sudoku: {
84+
options: {
85+
name: 'bootstraps/sudoku',
86+
out: options.staticTargetDir + 'javascripts/bootstraps/sudoku.js',
87+
exclude: [
88+
'core',
89+
'bootstraps/app',
90+
'text'
91+
]
92+
}
93+
},
8394
facia: {
8495
options: {
8596
name: 'bootstraps/facia',

static/src/javascripts/bootstraps/app.js

+6
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ define([
6262
});
6363
}
6464

65+
if (config.page.section === 'lifeandstyle' && config.page.series === 'Sudoku') {
66+
require(['bootstraps/sudoku'], function (sudoku) {
67+
bootstrapContext('sudoku', sudoku);
68+
});
69+
}
70+
6571
if (config.page.contentType === 'Article') {
6672
bootstrapContext('article', article);
6773
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
define([
2+
'common/modules/sudoku/main'
3+
], function (init) {
4+
return {
5+
init: init
6+
};
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* jshint newcap: false */
2+
define([
3+
'react',
4+
'common/utils/_',
5+
'common/modules/sudoku/constants',
6+
'common/modules/sudoku/utils'
7+
], function (
8+
React,
9+
_,
10+
constants,
11+
utils
12+
) {
13+
return React.createClass({
14+
onClick: function (event) {
15+
this.props.onClick(this.props.x, this.props.y);
16+
event.preventDefault();
17+
},
18+
19+
render: function () {
20+
var self = this,
21+
value = this.props.value,
22+
x = utils.position(this.props.x),
23+
y = utils.position(this.props.y),
24+
jottingX = function (n) {
25+
return x + constants.jottingXOffset + ((n - 1) % 3) * constants.jottingWidth;
26+
},
27+
jottingY = function (n) {
28+
return y + constants.jottingYOffset + Math.floor((n - 1) / 3) * constants.jottingHeight;
29+
},
30+
innerCells = _.compact([
31+
React.DOM.rect({
32+
key: 'background',
33+
x: x,
34+
y: y,
35+
width: constants.cellSize,
36+
height: constants.cellSize,
37+
onClick: this.onClick
38+
}),
39+
value ? React.DOM.text({
40+
key: 'value',
41+
x: x + constants.textXOffset,
42+
y: y + constants.textYOffset,
43+
className: 'sudoku__cell-text',
44+
onClick: this.onClick
45+
}, value) : null
46+
]).concat(_.map(this.props.jottings, function (n) {
47+
return React.DOM.text({
48+
key: 'jotting_' + n,
49+
x: jottingX(n),
50+
y: jottingY(n),
51+
className: 'sudoku__cell-jotting',
52+
onClick: self.onClick
53+
}, n);
54+
}));
55+
56+
return React.DOM.g({
57+
className: React.addons.classSet({
58+
'sudoku__cell': true,
59+
'sudoku__cell--not-editable': !this.props.isEditable,
60+
'sudoku__cell--highlighted': this.props.isHighlighted,
61+
'sudoku__cell--focussed': this.props.isFocussed,
62+
'sudoku__cell--same-value': this.props.isSameValue,
63+
'sudoku__cell--error': this.props.isError
64+
})
65+
}, innerCells);
66+
}
67+
});
68+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
define([], function () {
2+
return {
3+
cellSize: 40,
4+
borderSize: 1,
5+
buttonBorderRadius: 8,
6+
buttonSize: 45,
7+
buttonMargin: 9,
8+
buttonTopMargin: 37,
9+
controlsLeftMargin: 2,
10+
controlsTopMargin: 10,
11+
controlsHeight: 100,
12+
jottingXOffset: 8,
13+
jottingYOffset: 11,
14+
jottingWidth: 12,
15+
jottingHeight: 13,
16+
textXOffset: 20,
17+
textYOffset: 33,
18+
keyLeft: 37,
19+
keyUp: 38,
20+
keyRight: 39,
21+
keyDown: 40,
22+
key0: 48,
23+
key9: 57,
24+
keyPad0: 96,
25+
keyPad9: 105,
26+
keyBackspace: 8
27+
};
28+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* jshint newcap: false */
2+
define([
3+
'common/utils/_',
4+
'react',
5+
'common/modules/sudoku/constants'
6+
], function (
7+
_,
8+
React,
9+
constants
10+
) {
11+
var Button = React.createClass({
12+
render: function () {
13+
return React.DOM.g({
14+
className: 'sudoku__button',
15+
onClick: this.props.onClick
16+
},
17+
React.DOM.rect({
18+
className: 'sudoku__button-background',
19+
x: this.props.x,
20+
y: this.props.y,
21+
rx: constants.buttonBorderRadius,
22+
ry: constants.buttonBorderRadius,
23+
width: constants.buttonSize,
24+
height: constants.buttonSize
25+
}), React.DOM.text({
26+
className: 'sudoku__button-text',
27+
x: this.props.x + constants.buttonSize / 2,
28+
y: this.props.y + constants.buttonTopMargin
29+
}, this.props.text)
30+
);
31+
}
32+
});
33+
34+
return React.createClass({
35+
render: function () {
36+
var self = this,
37+
x = this.props.x,
38+
y = this.props.y,
39+
buttonsPerRow = 7,
40+
buttonOffset = function (n) {
41+
return n * (constants.buttonSize + constants.buttonMargin);
42+
},
43+
numberButtons = _.map(_.range(9), function (n) {
44+
var col = n % buttonsPerRow,
45+
row = Math.floor(n / buttonsPerRow),
46+
buttonX = x + buttonOffset(col),
47+
buttonY = y + buttonOffset(row);
48+
49+
return Button({
50+
key: 'button_' + n,
51+
x: buttonX,
52+
y: buttonY,
53+
text: n + 1 + '',
54+
onClick: function () {
55+
self.props.onClickNumber(n + 1);
56+
}
57+
});
58+
});
59+
60+
return React.DOM.g({
61+
className: 'sudoku__controls'
62+
},
63+
Button({
64+
key: 'button_erase',
65+
x: x + buttonOffset(2),
66+
y: y + buttonOffset(1),
67+
text: '-',
68+
onClick: function () {
69+
self.props.onClickDelete();
70+
}
71+
}),
72+
numberButtons
73+
);
74+
}
75+
});
76+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
define([
2+
'common/utils/_'
3+
], function (
4+
_
5+
) {
6+
return function (xs, f) {
7+
return Array.prototype.concat.apply([], _.map(xs, f));
8+
};
9+
});

0 commit comments

Comments
 (0)